iOS testing — 4 crossroads technique — Part 2

Krzysztof Brawański
TechTalks@Vattenfall
9 min readJan 5, 2022

This is the second part of the two-part series. The first part is:

Credits to Jacek Gzel, co-creator of the series.

Stage 2 — View’s actions

In the second round, we want to focus on creating view actions and dynamically changing data depending on the state of the station. For this purpose, we will use a view model that will preserve the state of our stations for us. Firstly, the view model will be responsible for orchestrating the station data and providing information to display. The view model, on the other hand, will be used to handle user actions. As in the first round, our work will be test-driven, so we are going to add a test case to it before implementing the activities. Are we going to write those unit tests again? No, we will test more than just make sure the element is visible on the screen. This time we will focus on testing the integration between the view model and the view. We will verify that user actions are affecting the view model data and that the view is re-rendered when the view model is changed. The integration test will work perfectly in this scenario.

In this scenario, we will use a new test structure function — context. The context will be used to precisely define what scenario we will be testing in, to keep our tests even more readable. Check out an example below in which we are splitting our test between two situations — when the station is in an available state and charging state.

Let’s move on to our first integration test. Consider a scenario where the user clicks a button to start charging the electric vehicle. What are our expectations? We want our station to start charging and to display it in view correctly.

Firstly, we are going to add a new describe() function to create space for new assertions. In the beforeEach() function we are going to simulate the user’s click by calling the tap method on the start charging button. Finally, we are ready to create some test cases with our expectations. In the first step, we can check if the station image is changed to the charging one.

To make our integration test pass, we need to introduce the view model, which we mentioned before. The view model will have information about the station state and will be able to manage the state of the station, which is necessary to handle user clicks. Simple implementation of view model below.

For now view model takes station state from the init method. This will allow us to create different view model states for testing purposes. Station state is published to view.

The next step is to inject our view model into view and mark it as an observed object, so the view will react to view model changes. Finally, we are going to add a condition that will decide which station image should be displayed. Last but not least is button action. Without it, our test case will still not pass, because the station state will not be changed by the view model. Let’s add button action!

We are ready to run our integration test again. This time test is passing.

The next step is to implement the rest of the changes, like changing the status of the station with description and button label. When everything will be done our test should look like this.

In this short example, we introduced the view model which is capable of station state management and view, which is ready to react to changes. Additionally, the user can start charging on his/her charging station to get the vehicle ready for the tour. The most important is that we are sure that, everything is working fine in our application. How cool is it, huh?

Stage 3 — API integration

In this round, we should have view with a view model able to hold a state that can be changed by the user. Now is the time to communicate with external API or other data sources to get and store some data.

Service building block

To achieve the goal we need to introduce a new building block which is service. In our approach service is an abstract layer that allows us to play with data and actions that should be handled somewhere else eg. in a storage system, API, or so on. Services separate business logic from technical details, so generally it unlocks us to be more focused on behaviors and writing good tests instead of thinking about how to connect to the API, what headers should be used, and so on. The biggest value is that we can change the implementation without touching the business logic, so for example we can connect to a new API, change storage system, and so on without the need of changing our test cases. One could ask, what about testing concrete implementation, and of course, this is a valid question, nevertheless in this article, we are going to focus only on core/business logic scenarios. In real development, You could consider how to approach this topic. One solution might be the usage of a mock in-memory HTTP server that launches only in tests and gives some responses. Another, that we chose, is to test it only manually.

Technologies — Cuckoo

Behind the funny Cuckoo name, we can find a really powerful tool that can help us mock dependencies that we are not going to test or rather test only interactions with the dependency without taking care about the concrete implementation. So, for instance, we can use it to inject some test data and not necessarily read it from real API or we can check if a particular function that is responsible for eg. Sending data to backend was invoked with correct data when the user taped button.

Before writing the test Cuckoo requires some configuration. We need to set up the compiler to use the Cuckoo command line tool to generate mocks and stubs. Unfortunately, it is the biggest disadvantage of this library in our opinion, because requires maintaining an additional file with a list of Swift files that should be used to generate mocks. However, is not the problem with the library but rather with the Swift language and lack of proper reflection in this language. Authors of the library described it as:

“Unfortunately Swift does not have a proper reflection, so we decided to use a compile-time generator to go through files you specify and generate supporting structs/classes that will be used by the runtime in your test target.”

Nevertheless, it is still more profitable to maintain one file instead of writing mock manually for every protocol that You need for the test. In this stage, we are not going to add new test cases because almost all cases should be defined in previous stages. Here we would like to refactor existing test cases to ensure that charging station data was read from service and request, when the user taps the start/stop charging button, was passed to the service with correct data.

Create service and refactor tests

In the first step, we need to rebuild the view model to load data from the service. So let’s define service and generate mock.

Add a new service to the script which generates mocks.

And refactor the first test case to allow injecting service into the view model.

Let’s examine what is done here. So cuckoo command line tool is based on script generated mock of the service. Now we can create a new instance of the mock, define stubs on it and eventually inject it into the view model. Generally, in this step, we are going to teach our mock how it should behave. So we are saying when method loadStation() will be invoked on mocked service it should publish object with charging station in a free state. Here You can also find examples of how to handle Combine in tests. Method loadStation() returns AnyPublisher<ChargingStation, Never> to satisfy this requirement in test we created CurrentValueSubject with the charging station data. When the test case is launched with prepared stub in such a way, every time if there is a request to load station, the production code will get our test data.

Verify service interactions

Above we covered the topic of how to provide test data to the code now let’s focus on a slightly different topic. We would like to verify if the method save() was invoked with correct data. For this purpose, we can use verify() method delivered by Cuckoo and ArgumentCaptor structure.

First of all, we need to define the mock for save method that is doing just nothing.

Then we need to define empty argument captor. It should be defined in scope when we are testing interaction with the user.

Eventually, we are ready to verify interactions and data captured

Verify method is an interesting structure, We can use it to check how many times a certain method was invoked by production code, and also in the same line, we capture arguments and then write expectations for captured values. Worth mentioning is that if You don’t need to capture arguments You can use generic methods like any(), anyString(), or so on provided by the Cuckoo framework to just ensure that the method was invoked with the expected argument type.

Stage 4 — User interface experience

In the last three steps, we managed to create an application that has all the necessary view elements, can respond to user actions, and has a connection to the backend application from which it derives data. We only miss one thing — the appearance of our application. We must admit that the current design will not satisfy our users.

In the last and fourth steps, we want to focus on the styles of our application. Perhaps you are wondering why we are doing this now? We prefer an approach in which we focus on the functionality of our software rather than the appearance of the application itself.

This time, styling the view will not affect the test cases. We will not prepare tests for application styles. Why? If we tested all styles given to our view individually, we would reach the point where every change of color, padding, etc would force us to change the test. To make the styling of the application very heavy and long-lasting. Imagine a situation where a UX designer says to redo the view a bit. We change font sizes, create a new view structure, and introduce new colors. Suddenly 30 tests stop working. Do not do this!

I think it is better to test the functionality of the application and whether all elements are displayed to the user. This should be enough for us and ensure that we are working properly. Thanks to this approach, changes to the tests will only be required when we change the operation of our application.

So how do we test our views? We put styles into our application and run it to see what it looks like, just. Style, run, repeat. So let’s go to work!

After adding a few styles to our application, the view of the station is much more pleasing to the eye.

Summary

4 Crossroads Technique could bring benefits to Your codebase but remember that in the development process there are no golden hammers, and the best approach is just a compromise. Here is similar, we described just some foundations that You can adopt to Your field. In some cases, it might bring more benefits if You merge some steps or just change the order. So don’t hesitate to play with this method and tweak it to Your needs to achieve the best performance.

Links

Cuckoo — https://github.com/Brightify/Cuckoo

--

--