Unit tests help us to make sure our app works as intended. Without having to start the app and navigating manually through it after every change. In comparison with UI tests they test single views in isolation instead of a full user flow. They are a lot faster and focused.
To start off easy we use
ContentView. It’s initially generated when creating a new project and draws the String
"Hello, world!" in the center of the screen. It’s SwiftUI code.
How would we test whether it draws the String
"Hello, world!"? One way is to create a UI Test as one could have done for UIKit.
Another approach is to use the library ViewInspector.
ViewInspector is a library for unit testing SwiftUI views. It allows for traversing a view hierarchy at runtime providing direct access to the underlying View structs.
In test test we first create the subject under test. In our case
ContentView. We access the
text of the
Text view via
inspect().text().string() and are able to assert on it. The test passes.
ContentView adopts the
Inspectable protocol to enable the magic of the library to do its work.
Let’s add some additional elements to our
ContentView. Aside from
"Hello, world!" there will be a
If we rerun our initial test it will fail.
inspect().text() will fail. The view hierarchy has changed. It should be
text(0) means the
Text view is the first element in our
NavigationView. The test passes.
Let’s take a step back. The user was still greeted with
"Hello, world!", but the test needed to be changed because the view hierarchy has changed. The test is tightly coupled to the view. The act of changing the test when the view changes is something often used as an argument against testing.
To alleviate the issue, we extract a
Breaking up our views into smaller building blocks is a great practice — for readability, composability, and as we will soon see, for testing purposes.
We add a test:
It looks pretty much the same as our initial
ContentViewTests. We start using the new
GreetingsView inside of
Text view is now part of
GreetingsView. It’s time to adapt the tests for
ContentViewTests pass. One issue though: We test the same thing twice. Both
GreetingsViewTests test whether the user is greeted with
"Hello, world!”, with a test
It’s test duplication. Instead, we can test whether the
GreetingView is present in
ContentViewTests. Then we can test the wording the user is greeted with in
GreetingsViewTests. We change the
ContentViewTests to reflect that change.
To sum it up, we:
Until now we have only tested static views. There might be an argument to make that testing static views makes no sense. After all, we can trust SwiftUI to display the text
"Hello, world!" when using the view
Text("Hello, world"). We could still use the tests as specifications to tell us what the view does, though. Instead of rendering the view or reading the implementation, we can read the names of the tests.
Besides, the views might not stay static for long. Logic is introduced and the view becomes dynamic.
Say we need two greetings, one for logged-in users and another for guests.
UserState is created to store whether the user is
loggedIn and the
We use the new
GreetingsView. Let’s start with passing it into the constructor for testability.
And we change the tests to adapt to the new
If we have a
loggedIn user with the name
Peter, he will be greeted with
Hello, Peter!. If the app is used by a non-authenticated user, a guest, we will display
What if we move the state into an
@EnvironmentObject because we will use it in multiple views in our app?
EnvironmentObject is created in our
Then it’s used in
We need to add the
didAppear functionality to allow
ViewInspector to test the view. A synchronous test would not work. The tests look as follows:
Having to use asynchronous test syntax seems weird at first but is required. We would need to use an asynchronous test for
@Environment too. It’s not required for
@Binding but should still be used for test consistency reasons.
Some might have an issue with having extra code for testability in the
GreetingsView. Personally I think testability is a lot more important than the implementation aesthetics of the view.
Maybe Apple will offer a native testing solution for SwiftUI views in the future? Maybe we will have new tooling creating an initial test for every new view we create? Let’s see!
The post has been cross-posted on Medium