Unit Tests with Dependency Injection

      1 Comment on Unit Tests with Dependency Injection

A few weeks back I wrote an article on dependency injection and how to use it. Here I’ll cover how to use dependency injection in your unit tests.

Dependency Injection

I won’t talk a lot about dependency injection in this article, there’s a whole article on the subject, and I don’t want to copy/paste a lot of text from the previous article 🙂 But let’s just reiterate what we learned previously.

Dependency Injection is a way of injecting the dependencies into your class. Your class declares it’s dependencies (e.g. via a constructor parameter) and whoever is using your class can provide those dependencies. This way we don’t hard-code dependencies into our classes and our codebase is a lot more flexible. We demonstrated this flexibility in the article on dependency injection where we injected a different implementation of the networking layer in the runtime. If you’re still unclear about the topic or would like to find out more, go ahead and read ‘Using Dependency Injection‘.

Unit Tests

I assume you’re familiar with the concepts of unit testing, and that you know how to create a unit test class and run the tests. I won’t cover that here.

Some people are very religious around unit test, and they consider a unit test to be a test that’s testing one method of a class, and nothing else. I’ll admit, I’m a bit more flexible on the subject, my tests spill over the class boundaries, and I usually test 2 or 3 classes together. This would be considered an integration test. If you don’t include your database layer in your integration tests they’re pretty fast, and if you don’t spill over too many classes, it’s not difficult to figure out why the test failed.

If your test spills over into your networking layer, and you need to make a networking call in order to run your test, that’s not a unit test. Unit tests, integration tests, module tests, they’re all fine, but if the test fails because you’re offline, then it’s not fine 🙂 Your tests should run offline. Here we’ll see how to mock your networking layer. And if you start just with that, you’ll be on a good way to mock all the dependencies for your class.

Mock Data

Before we start writing the tests, we need some mock data. I went on to themoviedb.org and fetched one of the JSON responses my example app was using. I saved it as a .json file and added it to my test target. I created one utility class called ‘TestData’ that will take in a file name, and have two properties, one that would return a Data representation of the file, and another that would return a JSON representation. We’ll use this in our mock networking provider.

‘dataRepresentation’ computed property will take a file from the test bundle and return a ‘Data’ representation of it, like so:

    public var dataRepresentation: Data? {
        get {
            let testBundle = Bundle(for: type(of: self))
            let filePath = testBundle.path(forResource: fileName.deletingPathExtension, ofType: fileName.pathExtension)
            
            if let path = filePath {
                let result = FileManager.default.contents(atPath: path)
                return result
            }
            
            return nil
        }
    }

‘jsonRepresentation’ will convert that file into a JSON object:

    public var jsonRepresentation: Any? {
        get {
            guard
                let data = dataRepresentation,
                let jsonObject = try? JSONSerialization.jsonObject(with: data, options: [JSONSerialization.ReadingOptions.allowFragments]) else {
                    return nil
            }
            
            return jsonObject
        }
    }

Now we have everything ready for our mock networking implementation.

Mock Network Provider

We’ll create a new class that conforms to our Networking Provider protocol. And we’ll inject that class into our movie data source to mock the networking layer. The diagram will look like this:

Our mock network provider will use the ‘TestData’ utility class to read a file from the file system and return that file instead of making an actual networking call. It might seem complicated, but it’s actually quite simple to implement, let’s see:

class MockNetworkProvider {
    
    fileprivate let dataProvider: TestDataProvider
    
    init(withDataProvider dataProvider: TestDataProvider) {
        self.dataProvider = dataProvider
    }
}

// MARK: NetworkingProvider
extension MockNetworkProvider: NetworkingProvider {
    func restCall(urlString: String, onCompleted: ((Data?) -> ())?) {
        onCompleted?(self.dataProvider.dataRepresentation)
    }
}

The constructor takes in the test data provider (our ‘TestData’ class conforms to this protocol) and it returns the data representation of that file in the networking callback, and that’s it, all our tests will use this simple networking mock. All we have to do now is get a few JSON files and we can mock any network call, we can even try to feed in invalid or malformatted JSON or a JSON that’s missing some of the keys… We’ll just do a quick happy path test, and test the ‘popularMovies’ API endpoint.

Testing

Now that we have everything ready, we can do a few quick tests, just to show you how it all fits together. Let’s test our ‘MoviesDataSource’:

class MoviesDataSourceTests: XCTestCase {
    
    func testGetMovies_Normal_ListOfMovies() {
        let testDataProvider = TestData(withFileName: TestFileNames.PopularMovies)
        let networkingProvider = MockNetworkProvider(withDataProvider: testDataProvider)
        let dataSource = MoviesDataSource(withNetworkingProvider: networkingProvider, andFactory: MoviesFactory())
        var movies: [MovieItem]?
        
        let getMoviesExpectation = self.expectation(description: "Get Movies Expectation")
        dataSource.getMovies { (items) in
            movies = items
            getMoviesExpectation.fulfill()
        }
        self.waitForExpectations(timeout: 0.1) { (error) in
            guard error == nil else {
                XCTFail("Expectation error: \(String(describing: error?.localizedDescription))")
                return
            }
            
            XCTAssertNotNil(movies)
            XCTAssertTrue(movies?.count == 20)
            
            let firstMovie = movies?.sorted(){ $0.title > $1.title }.first
            XCTAssertEqual(firstMovie?.title, "The Fate of the Furious")
        }
    }
}

Just to be clear here, this test spills over two classes, ‘MoviesDataSource’ and ‘MoviesFactory’, so technically it’s an integration test 🙂 But it’s quick enough, and those two classes work in tandem anyway.

Let’s go over the code example. We create our ‘testDataSource’ with the file name of our JSON file, and we feed this into our mock network provider. Then we create our data source with our mock network and call the ‘getMovies’ on it. We know that our mock network provider will return that JSON every time it’s called, so we expect that our class will parse and return an array of movies back. It’s an async call, so we use an expectation when we call the method, we store the result into a local variable, and in the expectation completion block, we perform our tests. I wrote some silly tests in there, just to prove that everything works. Of course, you will perform some meaningful tests there.

Now we have a unit test that’s using a mock networking layer. The test runs offline, obviously, but more importantly, it is repeatable, we control it completely, every time we call it we know exactly what the result will be, and if the test fails, the failure will be in the class that we’re testing. If we weren’t mocking the networking layer our tests would be very brittle, and every time a test fails we would have to run it again to see if the server was unavailable when the test was run, in other words, we would have to focus on dependencies, instead of the class that we’re testing.

Integration Tests

You can test entire modules by injecting a mock networking provider. For example, we could test the ‘MoviesManager’ and see how it integrates with all the other modules. You should write integration tests because they test how your whole system works, but they will run a bit slower than unit tests and assertion failure in an integration test will take a minute or two to figure out. As a general rule, most of your tests should be unit tests, where you test one class. You should have some component tests where you test how a few classes work together (like the ‘MoviesDataSource’ in our example. And you should have a couple of integration tests to see how the whole thing works (‘MoviesManager’ in our example). So write the most unit tests, and the least integration tests (but write some integration tests).

Let’s see an example of an integration test. Here’s the code:

class MoviesManagerTests: XCTestCase {
    
    func testGetListItemsDisplayable_Normal_ListItemsArray() {
        let networkProvider = MockNetworkProvider(withDataProvider: TestData(withFileName: TestFileNames.PopularMovies))
        let dataProvider = MoviesDataSource(withNetworkingProvider: networkProvider, andFactory: MoviesFactory())
        let manager = MoviesManager(withDataProvider: dataProvider)
        
        var result: [ListDisplayable]?
        
        let getItemsExpectation = self.expectation(description: "Get List Items Displayable Expectation")
        manager.getListItems { (items) in
            result = items
            getItemsExpectation.fulfill()
        }
        self.waitForExpectations(timeout: 0.1) { (error) in
            guard error == nil else {
                XCTFail("Expectation error: \(String(describing: error?.localizedDescription))")
                return
            }
            
            XCTAssertNotNil(result)
            
            XCTAssertTrue(result?.count == 20)
            
            let firstItem = result?.sorted(){ $0.listItemTitle > $1.listItemTitle }.first
            XCTAssertEqual(firstItem?.listItemTitle, "The Fate of the Furious")
            XCTAssertEqual(firstItem?.listItemSubtitle, "When a mysterious woman seduces Dom into the world of crime and a betrayal of those closest to him, the crew face trials that will test them as never before.")
        }
    }
}

First few lines of this test should look familiar, we create our mock network provider, inject it into our data provider, and then we create our movies manager and inject it with our data provider. Now we can start testing. We call the ‘getListItems’ method, and in the expectation callback, we perform the tests. It’s the same pattern that we saw before. This is just an example project, so there isn’t a lot of business logic in this layer, the objects only get converted into the view model objects, and that’s it, but in your case, you’ll have something to test here (sorting, searching, filtering… for example).

Unit Tests

I couldn’t write an article on unit testing without an example of a proper unit test, it would just be wrong 🙂 Let’s quickly go through a test for the ‘MoviesFactory’ class:

class MoviesFactoryTests: XCTestCase {
    
    func testMovieItems_Normal_ItemsArray() {
        let testDataProvider = TestData(withFileName: TestFileNames.PopularMovies)
        
        guard let testJSON = testDataProvider.jsonRepresentation else {
            XCTFail("Failed creating a JSON")
            return
        }
        
        let moviesFactory = MoviesFactory()
        let movies = moviesFactory.movieItems(withJSON: testJSON)
        
        XCTAssertTrue(movies.count == 20)
        
        let firstMovie = movies.sorted(){ $0.title > $1.title }.first
        XCTAssertEqual(firstMovie?.title, "The Fate of the Furious")
    }
}

This is an example of a proper unit test. We create our test JSON, we create an instance of the class that we’re testing, and we test a method on that class, the test does not spill over to other classes, and it tests only one class. We feed our test JSON into the ‘movieItems’ method and we test the results, we expect the JSON to be parsed correctly, so we assert on it. Since the order is not guaranteed, we first sort the array and test the first item. This is just a happy path test, you will test a few edge cases here (malformed JSON, missing elements in the JSON…). And that pretty much rounds it up.

Conclusion

In this article, we saw how we can use dependency injection to inject a mock networking layer into our app and perform some tests on it. Being able to inject a networking layer like this is pretty awesome because we ensure that our tests are consistent and repeatable, not to mention that they work offline 🙂 And because they don’t connect to a server, they will be much faster. Here we just scratched the surface, but you should have some solid foundation about dependency injection, and you should now understand how it all fits together and why it’s such a useful tool to have in your arsenal.

In one of the future posts, I’ll cover some DI frameworks out there that will keep a lot of the boilerplate code hidden. Before I go, you can find all the code on my GitHub repo. I hope I helped you out with this article and that you’ll find something useful in it 🙂

As always, have a nice day 🙂

Dejan.

More resources

One thought on “Unit Tests with Dependency Injection

  1. Pingback: Writing Unit Tests With Quick | agostini.tech

Leave a Reply

Your email address will not be published. Required fields are marked *

This site uses Akismet to reduce spam. Learn how your comment data is processed.