Handling Instantiation Issues in iOS Testing: Two Effective Approaches

abdul ahad
3 min readJan 22, 2025

--

Photo by Ben Mullins on Unsplash

When writing iOS tests, especially for lifecycle methods like sceneWillResignActive, developers often encounter challenges when dealing with UIScene. This happens because certain objects, such as UIScene, are tightly coupled to the system and lack public initializers, making them difficult to instantiate directly for testing purposes. This article explores two effective approaches to handle this scenario: simulating UIScene with NSClassFromString and extracting logic into a testable function.

The Problem: Testing sceneWillResignActive

Consider a typical method in your SceneDelegate that handles cache validation when the app enters the background:

 // SceneDelegate.swift
func sceneWillResignActive(_ scene: UIScene) {
store.deleteExpiredCache()
}

You want to write a test for this behavior to ensure that expired feed cache is deleted when the app transitions to the background. However, you face the following challenges:

1. No Public Initializer: UIScene cannot be instantiated directly, making it impossible to call sceneWillResignActive with a real UIScene instance.
2. System Dependency: Tests relying on UIApplication.shared.connectedScenes may fail unpredictably, especially in CI environments, where connectedScenes.first might be nil due to timing or resource constraints.

To address these issues, we can use one of two approaches.

Approach 1: Simulating UIScene with NSClassFromString

NSClassFromString is a powerful Objective-C API that allows you to dynamically access and instantiate classes by name at runtime. By using this technique, you can create a mock UIScene instance for testing purposes.

Implementation

Here’s how you can use NSClassFromString to simulate UIScene:

 
private func enterBackground(with store: InMemoryFeedStore) throws {
let sut = SceneDelegate(httpClient: HTTPClientStub.offline, store: store)
// Dynamically create a UIScene instance
let sceneClass = NSClassFromString("UIScene") as? NSObject.Type
let scene = try XCTUnwrap(sceneClass?.init() as? UIScene)
// Call sceneWillResignActive with the mock scene
sut.sceneWillResignActive(scene)
}

Test Example

 
func test_onEnteringBackground_deletesExpiredFeedCache() throws {
let store = InMemoryFeedStore()

// Simulate entering background
try enterBackground(with: store)
// Assert that the expired cache was deleted
XCTAssertTrue(store.didDeleteExpiredCache, "Expected expired feed cache to be deleted when entering background")
}

Why Use This Approach?

  • Tests Real-World Behavior: By calling sceneWillResignActive, you simulate the app’s actual behavior during lifecycle transitions.
  • No Code Refactoring Needed: The production code remains unchanged, as the dynamic creation of UIScene is isolated to the test.

Downsides

  • Dynamic Instantiation: Relies on runtime behavior, which might feel less explicit.
  • Less Readable: Developers unfamiliar with NSClassFromString might find it harder to understand.

Approach 2: Extracting Logic into a Testable Function

An alternative approach is to refactor the sceneWillResignActive method to extract its core logic into a separate, testable function. This allows you to test the business logic directly without needing to simulate the UIScene.

Refactoring

Modify the SceneDelegate to include a validateCache function:

// SceneDelegate.swift

// refactor logic into public method
func validateCache() {
store.deleteExpiredCache()
}

func sceneWillResignActive(_ scene: UIScene) {
validateCache()
}

Test Example

 
func test_validateCache_deletesExpiredFeedCache() {
let store = InMemoryFeedStore()
let sut = SceneDelegate(httpClient: HTTPClientStub.offline, store: store)
// Directly test validateCache
sut.validateCache()
// Assert that the expired cache was deleted
XCTAssertTrue(store.didDeleteExpiredCache, "Expected expired feed cache to be deleted")
}

Why Use This Approach?

  • Simplifies Testing: Focuses on testing the core business logic without relying on system objects like UIScene.
  • Improves Code Clarity: Refactoring often leads to cleaner, more modular code.

Downsides

  • Indirect Testing: Does not test the actual lifecycle method, so you must rely on integration tests to verify the full behavior.
  • Requires Refactoring: Changes production code to support testing, which might not always be desirable.

Comparing the Two Approaches

Which Approach Should You Choose?

  • Choose Simulating UIScene: When you want to test the actual app behavior during lifecycle transitions without modifying production code.
  • Choose Extracting Logic: When you aim to isolate and test business logic directly, or if refactoring the code aligns with your project’s goals.

Conclusion

Testing lifecycle events in iOS can be challenging due to the tight coupling with system objects. By either simulating `UIScene` with `NSClassFromString` or extracting core logic into a testable function, you can ensure robust and reliable tests while maintaining clean code. Choose the approach that best fits your use case and project requirements.

By leveraging these strategies, you can overcome the limitations of untestable objects and build confidence in your app’s lifecycle handling.

--

--

abdul ahad
abdul ahad

Written by abdul ahad

A software developer dreaming to reach the top and also passionate about sports and language learning

No responses yet