Dependency Injection on iOS — part 4/4

An article about architecture, tests and much more

Fernando del Rio
12 min readNov 26, 2018

Hello again. I started writing a series of articles about Dependency Injection, and now it's time for this series to end.

The series ending

A quick recap for the part 3:

  • We talked about the issues of using UIViewController as the controller of the application. We decided to move it to the view layer and ended adopting the MVVM pattern.
  • We improved our architecture to use Dependency Injection in order to decouple our components, improving the testability of our code. That's the final solution:
MVVM + DI solution

In part 4, we will focus on the actual tests, trying to write as many tests as we can figure out. We will try to cover unit tests and ui tests. We will test our business rules and we will test our view as well. We will do integration tests. We will check the code coverage and ensure, our tests are useful, telling us when something breaks.

Summary

1. Initial steps
1.1. Setting up target and code coverage
1.2. Mocking to test the business rules
1.3. Mocking to test the view
1.4. The containers
2. Writing unit tests (business rules)
2.1. Contact mock unit tests
2.2. Testing if contact and image are retrieved with success
2.3. Testing if some request fails
2.4. Favorite mock unit tests
2.5. Network code
3. Writing unit tests (view testing)
3.1. Creating the test
3.2. The baseline
3.3. Checking a failing test
4. Writing ui tests (integration tests)
4.1. Setting up target
4.2. Testing our code integrated
5. TL;DR

1. Initial steps

1.1. Setting up target and code coverage

Before anything, let's create a Unit Test target inside our app. Set up it to gather code coverage, like in the image below:

Let's do a quick hack to better understand the code coverage:

  • On the AppDelegate, remove the UIApplicationMain attribute before the class declaration (we want the app delegate to be more dynamic)
  • Create a file called main.swift with the following code:
  • This file will check if the app is running a unit test or not. If it's a unit test, it doesn't place an app delegate. By doing that we will see an empty code coverage the first time we ran it:
Empty code coverage

1.2. Mocking to test the business rules

As we discussed on the other article, we want to place some mock classes to make easier for us to test our code:

Mock network provider

This class has some cool features:

  • It allows us to return in-memory data
  • It allows us to set the request to fail, so we can test failing scenarios
  • It has short text or long text (we will use this later to test our view)
Mock contact view controller

By mocking the contact view controller, we now added the capability of checking if the loading is displayed or not.

By mocking the favorite view controller, we got the capability of checking if favorite button image was set. As we discussed earlier, with these approach on the view controller, we don't really test our view but this ensures our view model works fine.

1.3. Mocking to test the view

In order to test our view we want to place some dumb data on our view controllers, we want to test device orientations, to see if the constraints are working as expected.

There are many approaches possible, but we will use an library called FBSnapshotTestCase.

With FBSnapshotTestCase it's possible to take snapshots of views, create a baseline of "correct images" and create tests that verify if the views are correct by doing a diff between then. This is something called snapshot testing.

Our approach here should be simple. We will take some snapshots to validate some "states" of our view. We will ensure to change the view frame to try to replicate many devices dimensions and orientations. With that set, it should be possible to see how things are working and understand if the constraints are correct, if the text is not overlapping things, and stuff like that.
And in order to test our view without the business rules, we will just mock the view model side.

First of all, let define an enum with device screen sizes:

Let's now to create the mock on the view model side:

This class became large. But we don't need to focus too much on that now. It isn't a code that is part of our production code, it's just something we will use to better test our view now.

A quick overview of what we're doing there:

  • We created this Settings enum that contains some possible states for our view. We have states for the view displaying loading, not displaying loading, short name, long name, etc. There's a state that will put a gree background arount the UILabels. This will help out snapshot test to validate the boundaries of these UI elements.
  • We created an array of settings and it configures the view on the didSet. So basically, every time you place some settings the view is configured with that state. For instance, by setting the loadingState the view should be set to display the loading. There's no business rule involved it's only our view showing something to the user.
  • The init() creates the data binding with the proper view and map the proper screen sizes and orientations, using the DeviceScreen enum we created earlier.
  • The load() sets an initial state for the view
  • The verifyView is main API we want to use on our tests. After setting the view state we call it with an closure as argument (in the closure we want to pass the call that takes the snapshot using FBSnapshotTestCase. The method itself just keeps changing the view's frame, in order to test all possible screen sizes and orientations.

1.4. The containers

As shown in the previous article, we create these containers for the unit tests:

Also, as we discussed in the last article, the visual representation of the technique:

test target: mock container
test target: snapshot container

2. Writing unit tests (business rules)

The time has come, let's try to write some unit tests.

2.1. Contact mock unit tests

We want to create a class called ContactMockUnitTests:

The setUp() get's the instance of a ContactViewable according to the definition set in the mockContainer.

The tearDown() do some reset in the view and model in order for each test run independently.

2.2. Testing if contact and image are retrieved with success

If all requests work fine:

  • The data should appear in the UI
  • The favorite button should appear
  • The loading needs to be hidden in the end

Let's create a test like that:

We check the initial state (loading, name, description and favorite), call the load method of the viewModel, then check the final state.

You may not be confident on our approach involving the “loadingPresented" flag. It isn't a production code after all, how valid the test is? Let's make a test. Remove the call to self?.view?.hideLoading() on the ContactViewModel class. Run the test again. You will see that the test fails. Why it fails? It's because we expect our model to call this method in the view. As it doesn't call, the flag isn't updated. That's the beauty of the mocking, by using this non-production code (the mock class that doesn't matter for us) we are easily testing the view model (that is a production code and is a code that really matters).

Our test covers the view model and some classes of the model as we can see in the code coverage:

Code coverage after our first test

2.3. Testing if some request fails

As you can see, the contact view model and the contact provider isn't fully covered. What we are missing? We are missing the failing scenarios, checkout these screenshots:

Contact view model not fully covered
Contact provider not fully covered

To test the failing scenarios, we will just update some flags in our mock network provider:

Now we added two new tests, where the contact request fails and where the image request fails. Now the code coverage increases a little bit:

Contact view model and Contact provider fully covered

2.4. Favorite mock unit tests

Let's now test our favorite code. We can create a class called FavoriteMockUnitTests:

Pretty straightforward. We test if the tapping logic is working. When we call the tapFavorite method in the view model. It should switch the image in the view. Notice that, there's no UI involved, we're just testing the logic of switching the images. We're just testing the view model.

2.5. Network code

Let's test the actual network code. To that let's create a networkContainer. We don't really need to map our entire dependency graph, for now we just want to map Providable into an instance of NetworkProvider. Let's write the following code:

This test make a request over the network, waits 30 second expecting the assertions to complete. This is useful to see if the network integration is working. It's not fast neither predictable though, it's something nice to have. If it impacts our test suite, we can leave it disabled.

With that, our test coverage reach more than 50% of our code, which is good. The class Network provider probably won't reach 100%: we can't test its failing scenarios (it won't be feasible to shutdown the server for the test). Check out the code coverage:

Code coverage with almost all business rules covered

3. Writing unit tests (view testing)

We tested most of our business rules independently of the UI, what is good. We could stop here, knowing that at least the core of our app is tested. But there's a lot of things we could still cover. Let's try to do some snapshot testing using FBSnapshotTestCase.

We should set this environment variables so the test can take the snapshot in the proper folder:

3.1. Creating the test

Let's start by creating the ContactSnapshotTests class:

In the test above, recordMode = true is telling we want to start creating a baseline of "correct" snapshots. Once we create this baseline we can remove/comment that line and the class will start to validate if the screen matches the baseline. If there's some difference, it will fail the test and generate the failing snapshots.

Let's write our first test:

We set the state of our view on the settings (.dataLoaded puts the loading hidden and favorite button appearing. longName and longDescription fill the UI with long content).

Then we call verifyView. The handler we pass, call FBSnapshotVerifyView, this method takes a snapshot of the view in that particular state. It will be called multiple times on all possible sizes and orientations.

3.2. The baseline

Some of the screenshots set as the baseline:

testLongText_iPhone8-Portrait@2x.png
testLongText_iPhone8Plus-Landscape@2x.png
testLongText_iPadPro9_7inch-Landscape@2x.png

As you can see we can validate how the UI will looks like in the iPhone and iPad. We can test how it looks in portrait or landscape. I added an additional state in order to validate the boundaries of the UI elements. Check it out:

testBackgroundActive_iPhone8-Portrait@2x.png

3.3. Checking a failing test

Let's consider this is correct UI we want. The texts are centralized. The image is centralized. The name won't exceed 1 line. The description can have multiple lines, but it doesn't pass the end of the screen. The favorite button should always appear in the screen under the description. Our tests helps us to validate if the constraints are followed.

Let’s make a pure UI change. For instance, change the constraint/property that makes the name centered horizontally. Let's make it a little bit off. Ensure the test is not in the record mode. Run it again. The test will fail and we will get this image diff output, stating what's going on:

Image diff showing that the name should be centered, but it isn’t

We keep adding tests for the other states of the app: testDataLoading, testShortText, testLongText, testFavoriteSelected, testBackgroundActive. With the view testing we unit tested most of our code and we now have over 70% code coverage as you can see below:

Code coverage increased with the view testing

4. Writing ui tests

We tested the pieces of our code individually, only using Unit Tests. It's time for now trying some UI Tests

4.1. Setting up target

Let's start by creating a UI Test target. UI Tests are a little bit slower than the Unit Tests, as it needs the simulator to run.

4.2. Testing our code integrated

In the UI test we don't access our code directly. But we are able to test the app as a tester would do. Let's write this code to test a simple "flow" in the application:

In order to check things in the UI Test, we use the accessibility identifiers set for each UI elements. There's no much thing to do here:

  • We open the app
  • We wait for the data to load
  • We check if the data appears
  • We assert if the name and description, matches the correct outputs
  • We interact with the button by tapping on it

And that's it, there's less value on this specific test, but we were able to test our components integrated. By doing that we achieved the following code coverage:

Code coverage after the UI tests

Pretty cool right? We passed through almost all possible scenarios in our code and also create really useful tests reaching 93,74% of code coverage.

The remaining code not tested refers to the failing scenarios of the NetworkProvider (that we know we can't test) and also some guard lets we know they will never be nil. The only way to really increase the coverage in a scenario like that would be force unwrapping the optionals, but we shouldn't do that. Force unwrap is usually a code smell.

Besides that, our app is in a really good shape in terms of tests, we don't really need to achieve 100% if we know that what is pending doesn't matter.

Please take a time to check all the code in detail. The code for the tests we implemented here is available on Github in the following link:
https://github.com/fernandodelrio/dependency-injection-article
The relevant code is in the folder:
4. Unit Tests + UI Tests

5. TL;DR

In this last part of the article we got our Contact app refactored to use MVVM and dependency injection and we now wrote unit tests and ui tests to cover many aspects of the application.

With unit tests, we were able to cover business rules and view testing, covering the most important aspects of our application individually.

With ui tests, we were able to test our code integrated, checking if everything works when the app is running as one.

We gathered code coverage and checked the numbers increasing from zero to over 93,74% of coverage.

With that said, this series of articles is coming to an end. Hope you enjoyed the information I presented here. Feel free to leave some feedbacks, I will try to improve the articles based on them. I learnt a lot when writing these articles, so I hope I can learn from you too.

Thanks

Links to all parts:
Part 1: https://medium.com/@fernandodelrio/dependency-injection-on-ios-part-1-4-8847f302b3d9
Part 2: https://medium.com/@fernandodelrio/dependency-injection-on-ios-part-2-4-359fe6800e90
Part 3: https://medium.com/@fernandodelrio/dependency-injection-on-ios-part-3-4-e85fe7e20de6
Part 4: https://medium.com/@fernandodelrio/dependency-injection-on-ios-part-4-4-ce3723d819d

--

--