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.
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)
}
}