Search code examples
iosswiftunit-testingappdelegate

Test different behaviours in AppDelegate for each or separated unit and integration tests


I want to test my application's behavior that decided on app launch. For example: In a tab bar controller, how many and which tabs will be created is been decided on app launch where the root window has been created so I want to test these behaviors for each test case.

This new feature is set via A/B service and the value retrieved only during app launching. Based on that value, the tab bar's view controllers are set.

For example:

var viewControllers: [UIViewController] = [ tabOne, tabTwo]
if Config.isNewFeatureEnabled {
    viewControllers.append(self._menuCoordinator.rootViewController)
} else {
    viewControllers.append(self._anotherTabBarController)
    viewControllers.append(self._anotherCoordinator.rootViewController)
    viewControllers.append(self._someOtherCoordinator.rootViewController)
}
_tabBarController.viewControllers = viewControllers

Let me put in code, in order to make tests easy I created a protocol (not necessarily but better approach for injection)

protocol FeatureFlag {
    var isNewFeatureEnabled: Bool { get set }
}

// Implementation
class FeatureFlagService: FeatureFlag {
   var isNewFeatureEnabled = false
   // Bunch of other feature flags
}

In my test cases I want to switch the config with out effecting other side of the app. Something like this:

class NewFeatureVisibilityTests: XCTestCase {
    func test_TabBar_has_threeTabs_when_NewFeature_isEnabled() {
        // Looking for a way to inject the config

        let tabBar = getKeyWindow()?.rootViewController as? UITabBarController

        guard let tabBar = appDel.currentWindow?.rootViewController as? UITabBarController else {
            return XCTFail("Expected root view controller to be a tab bar controller")
        }

        XCTAssertEqual(tabBar.viewControllers?.count, 3)
    }

    func test_TabBar_has_fiveTabs_when_NewFeature_isDisabled() {
        // Looking for a way to inject the config

        let tabBar = getKeyWindow()?.rootViewController as? UITabBarController

        guard let tabBar = appDel.currentWindow?.rootViewController as? UITabBarController else {
            return XCTFail("Expected root view controller to be a tab bar controller")
        }

        XCTAssertEqual(tabBar.viewControllers?.count, 5)
    }
}

What I want is set application's behaviour through injection (a config etc) for each test case.

One test the feature will be enabled, other test will assert the feature disabled state.


Solution

  • Create a config property in AppDelegate using existing type of FeatureFlag along with a default value on override init.

    extension UIApplication {
        var currentWindow: UIWindow {
            return (connectedScenes
                .filter({$0.activationState == .foregroundActive})
                .compactMap({$0 as? UIWindowScene})
                .first?.windows
                .filter({$0.isKeyWindow}).first!)!
        }
    }
    
    @main
    class AppDelegate: UIResponder, UIApplicationDelegate {
        
        var window: UIWindow?
        let config: FeatureFlag!
        
        override init() {
            config = FeatureFlagService()
        }
        
        init(config: FeatureFlag!) {
            self.config = config
        }
        
        func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
            // Override point for customization after application launch.
            
            // Create a tabBar with 3 tabs
            let tabBarController = UITabBarController()
            let firstViewController = UIViewController()
            let secondViewController = UIViewController()
            let thirdViewController = UIViewController()
            let fourthViewController = UIViewController()
            let fifthViewController = UIViewController()
            
            firstViewController.tabBarItem = UITabBarItem(tabBarSystemItem: .favorites, tag: 0)
            secondViewController.tabBarItem = UITabBarItem(tabBarSystemItem: .downloads, tag: 1)
            thirdViewController.tabBarItem = UITabBarItem(tabBarSystemItem: .more, tag: 2)
            fourthViewController.tabBarItem = UITabBarItem(tabBarSystemItem: .bookmarks, tag: 3)
            fifthViewController.tabBarItem = UITabBarItem(tabBarSystemItem: .contacts, tag: 4)
            
            var viewControllers = [firstViewController, secondViewController]
            if config.isNewFeatureEnabled {
                viewControllers.append(thirdViewController)
            } else {
                viewControllers.append(fourthViewController)
                viewControllers.append(fifthViewController)
            }
            
            tabBarController.viewControllers = viewControllers
            
            // Create a window and set the root view controller
            let window = UIWindow(frame: UIScreen.main.bounds)
            window.rootViewController = tabBarController
            window.makeKeyAndVisible()
            self.window = window
            
            return true
        }
    }
    

    And in tests, I set my config, create an instance of AppDelegate, inject the config, and launching the application through appDelegate.application(UIApplication.shared, didFinishLaunchingWithOptions: nil) function of AppDelegate.

    let appDelegate = AppDelegate(config: config)
    
            // This is the key function
            _ = appDelegate.application(UIApplication.shared, didFinishLaunchingWithOptions: nil)
    

    Tests:

    import XCTest
    @testable import ExampleApp
    
    
    final class NewFeatureVisibilityTests: XCTestCase {
       
        func test_app_can_start_with_isNewFeatureEnabled(){
            let config = FeatureFlagService()
            config.isNewFeatureEnabled = true
            let appDelegate = AppDelegate(config: config)
    
            // This is the key function
            _ = appDelegate.application(UIApplication.shared, didFinishLaunchingWithOptions: nil)
            
            guard let rootVC = UIApplication.shared.currentWindow.rootViewController as? UITabBarController else {
                return XCTFail("RootViewController is nil")
            }
            
            XCTAssertEqual(rootVC.viewControllers?.count, 3)
        }
        
        func test_app_can_start_with_isNewFeatureDisabled(){
            let config = FeatureFlagService()
            config.isNewFeatureEnabled = false
            let appDelegate = AppDelegate(config: config)
    
            // This is the key function
            _ = appDelegate.application(UIApplication.shared, didFinishLaunchingWithOptions: nil)
            
            guard let rootVC = UIApplication.shared.currentWindow.rootViewController as? UITabBarController else {
                return XCTFail("RootViewController is nil")
            }
            
            XCTAssertEqual(rootVC.viewControllers?.count, 4)
        }
    }