iOS testing — 4 crossroads technique — Part 1

Jacek Gzel
TechTalks@Vattenfall
8 min readJan 5, 2022

This article is the first part of two. The second part is:

Credits to Krzysztof Brawański, co-creator of the series

Long story short — why we created this article

In Vattenfall, we are working in a cross-functional team responsible for the whole smart-charging electric vehicles feature. We have started doing backend development in the JVM world, then we have been doing the frontend part eventually, it turned out that our business required also the mobile app.

So, we’ve started iOS development from scratch. Our main idea was to keep quality standards that we followed in other technologies, so we were looking for good documentation of testing on iOS. However, we quickly figured out that there is not any complete documentation on how to test the iOS app. We spent days experimenting with frameworks and approaches. Eventually, we found the best compilation of test frameworks that allow us to write code in the “Behavior/Test Driven Development” way, and in this article, we would like to share with You our experience.

Why testing iOS is important for us

Google search why iOS developers don’t write unit tests

Probably many of You have heard opinions about iOS developers that they don’t write unit tests, and for sure many of You would disagree with such hurting opinion because You are real test fighters (kudos for You). However, it could be something bigger than just an ordinary rumor, because we have started iOS development in a project without tests. We were wondering why there are no tests, and one of the possible answers could be that the views were implemented in Storyboards. And only those, who have ever tried to write lightweight tests for Storyboards know, how difficult this task is. So, in this article, we are going to describe only the approach for SwiftUI because in our opinion this is the only way how to write lightweight, focused-on behaviors, tests in iOS.

You could ask why we need unit tests though? And it would be a nice idea of a separate article or even a whole series. But to not leave questions without answers let’s look at our main drivers.

  • Future code changes/refactors are safer and easier to conduct.
  • Less code to write (just as much as is required to pass the test) so, building a codebase is easier to maintain in the future.
  • The solution is tested against corner cases, which are difficult to reproduce in manual tests.

4 Crossroads Technique — foundations

Our approach is based on 4 rounds of Test Driven Development (TDD). Every round You are focusing on a different scope like static view, view’s actions, API integration, and user interface experience. The goal is to have after four rounds of coding a decent view integrated with API that users can interact. This approach we called the 4 Crossroads Technique. This fancy name came to our minds because we compared the TTD technique to street lights RED GREEN REFACTOR(orange) and every round to different crossroads. So, if You want to deliver a new view to the user You need to travel through 4 crossroads. The approach also fits with the pair programming technique. You can adapt it as You like eg. change each other every TDD round/stage or crossroad.

A few words about code examples and context

We created a small app that displays a simple view of the charging station that gives the possibility to interact with users by starting or stopping the charging sessions. So, the use case is user comes to the charging station, connects the car, and uses a mobile app to start the charging session. When the car is charged, the user can stop the charging session. We use an abstract layer of HTTP API because we would like to focus on behaviors in the app (business cases) not the infrastructure that might be additionally tested as well with mock HTTP servers or manually.

All code is available on GitHub. There are PRs for every stage so You can easily switch between them and check what are the differences.

Stage 1 — Static view

In the first round, we want to focus on creating a simple static view, without any actions, dynamically changing data or so on. Our work should be driven by tests, so before adding a new element to the view like Text or Button we will create a unit test case for it that doesn’t pass, and then we will satisfy it by adding an appropriate view element. We are following this way until each required UI element is shown on the screen.

Technologies

At the very beginning, we would like to briefly discuss the frameworks we will be using in this stage. Let’s start with Quick. Quick is a behavior-based programming framework for Swift and Objective-C that has a clean, self-explanatory syntax so you can write tests easily. Quick comes in conjunction with Nimble — a matcher framework for your tests. These two can do a great job and make your testing very enjoyable. Last but not least is ViewInspector. ViewInspector is a library for unit testing SwiftUI views. It allows for inspecting a view hierarchy at runtime providing direct access to the underlying View structs.

BDD test structure

Here is a short example of the test structure with setup of initial view and check if the correct label is shown. We are using Behavior Driven Development (BDD) so the description is very verbose and could be read by a non-technical person as well.

Now let’s spend some time understanding all elements.

The describe function is used to group related test cases and define a scope. Usually, each test file has one at the top level. The string parameter is for naming and will be combined with test cases to form the full name of the test. If you name them correctly, your specifications will be read as full sentences in the traditional BDD style.

Example of BDD test description

The it function defines a single test case. The body should contain one or more expectations that test the state of the code. In our approach, it is the smallest atomic unit in the structure. So generally, we have one expectation per it or there could be more only if there is a strong connection between them. Why do we keep such rigor? Because we would like to have a verbose and accurate message when a test case fails. If there are one or two expectations, it is easy to figure out what failed. Also, during refactor or code changes you have a detailed list of what doesn’t work.

Expectations are built with the function expect which takes a value, called the actual. It is chained with a matcher function, which takes the expected value. If getting actual value requires more actions like invoking ViewInspector it should be moved to the separated function that returns the correct value. The benefit is that if You are refactoring the view You don’t have to adjust every test case, only one function, responsible for seeking elements.

The beforeEach function is called before each it. We use it to create a view, set up all dependencies, and simulate the state for test context. Creating the SwiftUI views is very cheap so we can afford on creating a new view with dependencies before every test case, in runtime it takes only a few milliseconds per unit and brings the benefit that every test case is atomic and doesn’t have an impact on others, literally You can say that each test case has own runtime sandbox.

Test first and see the view

So now equipped with some knowledge let’s start our development. Following the TDD approach first, we need to create the specification and implement the first test case where we expect the station’s image on view.

To achieve the goal we are defining ChargingStationView?. The view might be empty because we are going to instantiate it afterward in the beforeEach section, just to have a new fresh instantiation for each test case. Eventually, we create well-described expectations. Seeking view element logic is moved to the private function chargingStationImage() because of two reasons. First, the expectation is more verbose, and second, it allows us to map the outcome from ViewInspector into an optional String value that contains the name of the image. That move helps check if the image is visible.

When the test case is done we need to write minimal code just to allow build and see the red stage. Generally, it means that we need to create ChargingStationView with empty VStack in the body and launch the test. This stage seems to be silly but it shows that our test case is valid and can be launched.

Red stage in TDD

Now is the time to write some code to satisfy our test case. According to the TDD rules, we should use minimal effort, just to have the green state as soon as possible.

Green state TDD

Now we are repeating the steps above until we have all elements on the screen.

Appendix — how to seek view elements

There are two ways of seeking view elements. The first one is done by using indexes of elements in the view hierarchy. In the below example, we are getting the value of Text struct which is placed in VStack at index 1 (At index 0 is an image of charging station). This solution is very prone to changes of view under tests. If we change the order of elements we will need to adjust indexes in our private methods.

The second way of seeking is dynamic search. You can query for a specific view with find or findAll to get an array of all matching views of a given type. In this case, we can adjust the order of elements in view under test and this function will return still the same Text struct. The function below should find the first view of type Text, so we don’t have to care about views of different types like Image which is placed at index 0.

When seeking specific elements of the view under the test, such as Text or Button, we recommend using a dynamic search. Thanks to this, our tests will be more resistant to all kinds of changes in the tested views and it also ease our job in the next stages.

Summary

In this article, we covered 4 Crossroads Technique foundations and we described the first stage (crossroad). In the next article, we will describe the remaining 3 stages.

Links

Quick — https://github.com/Quick/Quick
Nimble — https://github.com/Quick/Nimble
ViewInspector — https://github.com/nalexn/ViewInspector

--

--