Here we are going to take a look at one approach we can use to give our View Controllers a different ViewModel for UITests. Afterall, We don’t always care to hit a real service for our UI tests, we just care that it displays correctly when it’s provided with state and/or data change.
So let’s jump in; 🏊♂️
Let’s have a View Model for fetching a Todo Item from some web service, yay.
My VM takes an object conforming to TodoAPIProtocol who is responsible for getting the data, and thats all my VM really cares about.
My TodoAPIProtocol isn’t concise for this example, but it’s clear that anything that conforms to it will take and give in return the following.
Now, let’s mock the hell out of it.
Using Postman, a browser, free- hand, or even a curl request, get a valid response body to your API call and copy/paste over to a text file that you are now going to add to your project.
Perfect, let’s create a mock API Client, that is going to parse this text file at run time, decode it and spit it back out as a TODO item. 🙌🏻
A couple of things happening here, our static function, mockAPIClient, is responsible for giving us a MockTodoAPI object for which the ViewModel will call ‘getTodoItem’. Note: mockAPIClient is open to extension greatly here, take the factory pattern and churn out as many MockTodoAPI objects you wish with varied responses. Happy Path / Sad Path — the choice is yours.
Now, with a factory responsible for giving us MockTodoAPI objects, where said objects are conforming to TodoAPIProtocol, we can now inject them into the ViewModel’s initialiser, giving us the great power of delivering mocked responses (good and bad) to the View Model. The VM doesn’t care if its live data, or mocked, it just deals with what the APIClient gives it. 💪
Over to our View Controller, let’s give it an initialiser, with a viewModel parameter for our TodoViewModel. (NB: It’s not even optional, something cool is happening)
By now you are probably wondering how do we get this into a Storyboard based View Controller. Follow me, i’ll show you. 👀
In Our SceneDelegate (Or App Delegate’s didFinishLaunching depending on your projects age) let’s pounce on the InstantiateViewController where we can provide custom initialiser code to call the View Controllers new initialiser we made above.
From the Apple Docs:
func instantiateViewController<ViewController>(identifier: String, creator: ((NSCoder) -> ViewController?)? = nil) -> ViewController where ViewController : UIViewControllerCreates the specified view controller from the storyboard and initializes it using your custom initialization code.
Now that’s quite a chunk of code to just throw up, alot of it will look familiar, but take a look inside the instantiateViewController method. Here we can initialise our VM, and pass that into a Storyboard based View Controller’s initialiser. Quite something isn’t it, no optional viewModel property on the View Controller either, which is always a nice to have rather than guarding our way around a view Controller.
Now to give a different TodoApiClient when running UITests. Todo that, when running our UITests, we need to set a Launch Environment variable that we can evaluate at runtime, to then inject a viewModel with the required dependencies.
It’s relatively simple, in our UITest, set a launchEnvironment variable as above :) Magic. That’s done.
Back to the instantiation of our Storyboard Based ViewController code in the SceneDelegate.
At runtime, we can evaluate the Launch Environment variable “XCUI”, and if “1” we are aware that we are running UITests. At this point, we can instantiate our TodoViewModel with our mockAPIClient factory, inject that into the ViewController and then go about our UITest recording with the XCUITest Suite.
I am yet to evaluate how easy this solution is to scale with a multitude of UI Tests, but I would go as far as using String valued enums, which the MockTodoAPI Client can evaluate and provide the relevant response back to the ViewModel.
Enjoy, comments as always welcome.
@TheIOSDude — Twitter.