Find us

mobileforming,
611 North Brand, Boulevard, 11th floor
Glendale, CA 91203

Google Maps
Get in touch

Have a question for the mobileforming team?
T: 818-649-3299
E: info@mobileforming.com

Careers

Interested in joining our
amazing team? Email us on: recruiter@mobileforming.com

Current vacancies
New business

For new business enquiries please contact:
Jonathan Arnott
T: 951-229-5790
E: jonathan.arnott@mobileforming.com

© 2018 mobileforming LLC. All rights reserved.

featured post

How to Write Testable Code: One weird trick to make it easy

read post

How to Write Testable Code: One weird trick to make it easy

By Wesley St. John

Posted on: 17 January 2020

Do you hate writing unit tests? Do they seem hard to write and maintain? Do you avoid writing them whenever possible?

hateTests

 

If you do, I feel your pain. I, too, hated writing tests. And it took me way too long to figure out this one weird trick to make it all OK. After too many years of struggling with writing unit tests, I realized that they are not inherently awful. It was just that the code I'd been writing was inherently hard to test. But there is a solution...

The goal of this article is to demonstrate a simple approach to writing code that makes it easier to test, easier to verify that your code works the way it should, and just feel good.
"Unit testing is a level of software testing where individual units/components of the software are tested. The purpose is to validate that each unit of the software performs as designed. A unit is the smallest testable part of any software. It usually has one or a few inputs and usually a single output.”

 

Software testing fundamentals

Benefits of Unit tests:

  • They give you confidence that your code works.
  • Unit tests can tell you when your code breaks from other changes in the codebase.
  • Writing unit tests encourages good software design.

Example

Let's say we're writing a class called DataSource that has a function getDayName that returns the name of today in a given language.

For example, if today is Monday and we pass in "FR" for the language, the function would return "Lundi."

Here's the function signature:


    func getDayName(language: String, completion: ((String?, Error?) -> Void)?)
    

Now let's say our DataSource class can either get the info from local storage, if present, or fetch it from a remote server.

To do this, it has two let properties: a LocalDataSource class and a RemoteDataSource class.


    let localSource = LocalDataSource()
    let remoteSource = RemoteDataSource()
    

Here's the implementation of the function:


    func getDayName(language: String, completion: DayCompletion) {
        
        // 1: check local storage first
        if let info = localSource.readInfoFromStorage(language: language) {
            completion?(info, nil)
            
        } else {
            // 2: if no local result, ask the remote
            remoteSource.fetchInfoFromAPI(language: language) { (remoteInfo, error) in
                // 3: store result in local data
                if let remoteInfo = remoteInfo {
                    self.localSource.writeInfoToStorage(language: language, day: remoteInfo)
                }
                // 4: return the fetched info
                completion?(remoteInfo, error)
            }
        }
    }
    

Here's what's happening:

  1. We first check the localSource property to see if there is any cached data. If there is, return it.
  2. If there is no cached data, we have to use the remoteSource to fetch it.
  3. If the fetch is successful, store the result in the localSource.
  4. Last, we return the fetched info.

That looks reasonable, and it appears to work when we run it and debug it. So let's write some unit tests!

cant-wait-gif-19

 

Writing Tests

We created DataSourceTests.swift as an XCTestCase class, so now we’ll attempt to test our getDayName function.

Disclaimer:

 

This article assumes that you are at least somewhat familiar with writing an XCTestCase, but even if you're not, it can still be useful. So keep reading!

 

For more information about the mechanics of unit testing, try this handy raywenderlich tutorial.

In our setup function, create an instance of DataSource and set it to our variable.


    class DataSourceTests: XCTestCase {
        var dataSource: DataSource!
    
        override func setUp() {
            dataSource = DataSource()
        }
    

Now let's write a test function testGetDayName—where we call DataSource.getDayName() with a specific input and try to verify the output.


    func testGetDay() {
        // 1
        let exp = expectation(description: "wait for result")
        dataSource.getDayName(language: "FR") { (result, error) in
            // 2
            XCTAssertEqual(result, "Lundi")
            XCTAssertNil(error)
            exp.fulfill()
        }
        // 3
        waitForExpectations(timeout: 1.0)
    }
    

Here's what's happening:

  1. Because the getDayName function is asynchronous, we will have to force the test function to wait until the completion closure is called, so we create an XCTestExpectation and call getDayName.
  2. Once the completion block is called, we try to verify that the result is "Lundi" and that there is no error.
  3. Then, we hold execution until the expectation is fulfilled in the completion block.

 

Test Results

OK, so let's run the test!

failedMeme

 

Oh crap, it failed. Why? Because today is (random day), not Monday.

testFailed1

 

We could change the test to assert that result is equal to "Mardi" or "Mercredi" or whatever today is in French.

When we change the expected output to "Mardi," voila, the test passes!

But we know it won't pass tomorrow… Let's address this problem later.

Let's say we run the test again, but this time it fails. After digging around we find that there was a server error and the remoteSource returned an error.

testFailed2

 

This is not good because the bug exists on the back end, but the test says that the DataSource class has a bug. This means that DataSource has a dependency on remoteSource that could muck up our DataSource test results.

If we think about it more, there could also be a similar situation where the localSource fails, perhaps due to a database read error, which would cause a failed test for DataSource.

Before we address these problems, let's take a look at the code coverage for our test.

coverage1

 

We are covering the completion of the remoteSource, which is nice. But if there is no local cache, we won't get coverage of the localSource's completion block. And unfortunately, there is no easy way to ensure that we cover both scenarios.

So our test is kind of OK, but it has some drawbacks:

  • If today is not Monday, or if there is a problem on the back end, or if there is a problem reading local data; then our test will tell us that DataSource has a bug, which is not necessarily true.
  • In addition, it's hard for us to know if DataSource will behave properly when there is local data to return or in other edge cases.

However, if we take a closer look, we will find that the problem doesn't really lie in our unit tests. The problem arises from having code that is inherently hard to test.

But…

There is a better way.

We can build a better mousetrap!

 

Example: Scientific Method

scientific

 

Let's think back to middle school when we learned about the Scientific Method. The scientific method can help us write better code, but before we get to that, here’s a refresher:

A few main steps of the method are to construct a hypothesis and design an experiment to test the hypothesis.

Let's say we want to design an experiment that tests the effect of fertilizer on plant growth.

 

plants

 

Our hypothesis can be: if plant A is fertilized and plant B is not, plant A will grow faster.

To design an experiment, there are two key steps:

One is to identify the variables or all the things that can influence the output. here are several things that can influence how much a plant grows: fertilizer, sunlight, water, the type of soil, etc.

The second key step is to identify the independent variable (the one we are testing for) and control for all other variables.

In this case, the independent variable is the amount of fertilizer given to plants. In order to know how much of an effect the fertilizer has, we must control for the other variables. So, we can set up an experiment where all plants receive the same amount and quality of sunlight, water, and soil. Then, we can assume the amount of plant growth is due only (or at least, mostly) to the amount of fertilizer received.

 

Analogy: Unit Testing

Now we can approach the writing our code and tests using the scientific method. It is called computer science, after all.

What's our hypothesis?

Based on a certain language input, we can expect a certain day String result.

What are the variables (dependencies)?

  • The input language
  • The current day of week
  • The local data source
  • The remote data source

The independent variable is the input language that our tests will pass into the getDayName function.

The other variables should be controlled.

But how???

 

Example: Scientific Method for Datasource Class

Currently DataSource creates its own LocalDataSource and RemoteDataSource, and there is no way to change them.

A great way to gain control over our code is to solve this problem using Dependency Injection.

 

Dependency Injection

 

dependencyInjection

 

Dependency injection is a technique whereby one object supplies the dependencies of another object. A "dependency" is an object that can be used, for example as a service. Instead of a client specifying which service it will use, something tells the client what service to use. The "injection" refers to the passing of a dependency into the object that would use it. Passing the service to the client, rather than allowing a client to build or find the service, is the fundamental requirement of the pattern.

In other words, if a class A calls a function of B, dependency injection refers to passing in (injecting) an instance of B into object A.

OK, so let's see how we can use dependency injection in our DataSource class.

One straightforward way is to add the dependencies to the init method.


    init(localSource: LocalDataSource, remoteSource: RemoteDataSource) {
        self.localSource = localSource
        self.remoteSource = remoteSource
    }
    

Now we can control the localSource and remoteSource members of the DataSource class.

However, in order to have complete control over those variables, we can add a layer of abstraction using protocols.

 

Protocols

Let's create two protocols to represent the LocalDataSource and RemoteDataSource classes:


protocol LocalDataSourceProtocol {
    func readInfoFromStorage(language: String) -> String?
}
protocol RemoteDataSourceProtocol {
    func fetchInfoFromAPI(language: String, completion: DayCompletion)
}
    

And let's update the init function of DataSource to take in these protocols rather than the classes:


    init(localSource: LocalDataSourceProtocol, remoteSource: RemoteDataSourceProtocol)
    

So why is this better?

Well, first of all, it doesn't affect DataSource's functionality. From the DataSource class's perspective, nothing has changed. It still interacts with the local and remote properties in exactly the same way.

But from the perspective of our unit tests, we can now have complete control over the dependencies.

evilGenius

 

Test Results (version 2)

Let's create stub classes in our testing target that implement these protocols:


class StubLocalDataSource: LocalDataSourceProtocol {
    var stubDay: String?
    
    func readInfoFromStorage(language: String) -> String? {
        return stubDay
    }
}

class StubRemoteDataSource: RemoteDataSourceProtocol {    
    var stubDay: String?
    var stubError: Error?
    
    func fetchInfoFromAPI(language: String, completion: DayCompletion) {
        completion?(stubDay, stubError)
    }
}
    

These stub classes have accessible variables where we can explicitly set the return values of the functions.

Now, we can update our tests to control for all the variables.

Let's update the test properties to include our stub classes and update our setup method to pass them into DataSource.


    class DataSourceTests: XCTestCase {

    var dataSource: DataSource!
    var stubLocal: StubLocalDataSource!
    var stubRemote: StubRemoteDataSource!
    
    override func setUp() {
        stubLocal = StubLocalDataSource()
        stubRemote = StubRemoteDataSource()
        dataSource = DataSource(localSource: stubLocal, remoteSource: stubRemote)
    }
    

Now let's update our first test to force the local and remote sources to give the output we want to the DataSource class.


    func testGetDay() {
        let expectedDay = "Today"
        stubLocal.stubDay = nil             // 1
        stubRemote.stubDay = expectedDay    // 2
        stubRemote.stubError = nil          // 3
        
        let exp = expectation(description: "wait for result")
        
        dataSource.getDayName(language: "FR") { (result, error) in
            // 4
            XCTAssertEqual(result, expectedDay)
            XCTAssertNil(error)
            exp.fulfill()
        }
        
        waitForExpectations(timeout: 1.0)
    }
    

So what's happening here?

  1. We are forcing the local source to return nil.
  2. We are forcing the remote source to return "Today" (Note that the exact string is irrelevant to DataSource. We are only trying to ensure that DataSource returns what remoteSource returns).
  3. We are forcing the remote source to return nil for the error parameter.
  4. We are testing that we get back "Today" from the getDayName function.

So why is this better?

First of all, let's remember that in the DataSourceTests.swift file, we are only testing the DataSource class. We are not testing the LocalDataSource or RemoteDataSource classes. Those classes should have their own test files.

Harking back to the scientific method, we are controlling for all variables (except the independent variable): the local source, the remote source, and the day of the week.

So, even if there is a bug inside the LocalDataSource class or the RemoteDataSource class or if this test runs on any day of the week, it won't matter. We have verified that DataSource is working as intended.

Another bonus is that now we can increase our code coverage.

Whereas before it was very difficult to test scenarios like:

  • The local source has data
  • The remote source returns an error

Now, we can write more test functions where we can stub those scenarios and get close to 100% coverage.

Here's an example of a different scenario that was difficult or impossible to test previously:


    func testGetDayLocalSuccess() {
        let expectedDay = "Tomorrow"
        stubLocal.stubDay = expectedDay			// 1
        stubRemote.stubDay = "Some other day"	// 2
        stubRemote.stubError = nil
        
        let exp = expectation(description: "wait for result")
        
        dataSource.getDayName(language: "FR") { (result, error) in
            XCTAssertEqual(result, expectedDay)	// 3
            XCTAssertNil(error)
            exp.fulfill()
        }
        
        waitForExpectations(timeout: 1.0)
    }
    

What's happening in this test function?

  1. Now, we are telling the localSource to return "Tomorrow" for its output.
  2. We are telling the remoteSource to return "Some other day" for its output. But if DataSource is working correctly, it should return "Tomorrow" instead because it checks the local data source first.
  3. We expect "Tomorrow" to be returned from DataSource.

Let's check our code coverage now.

coverage2

 

Alright, we have upped our code coverage! This is great because our boss will love it!

But more importantly, we have increased our confidence that DataSource will function correctly in all scenarios.

 

Conclusion

What did we (hopefully) learn?

  • Unit tests are not so bad! If unit tests are hard to write, it is probably because the code we are trying to test is not designed to be tested easily.
  • Remember the scientific method. Most units of code have dependencies on other units of code that can affect their output, just like experiments have variables that can affect their output.
  • If we can inject alternate implementations of the dependencies into the code we are testing, we can control for these variables, which makes testing easier.
  • Using protocols and stub implementations allow our unit tests to have maximum control over the variables/dependencies.
    • This allows us to actually test only the units of code that we are intending to test and ensures that the tested code behaves properly regardless of what's happening "outside of the box."
  • We can also stub or "design experiments" to test multiple scenarios or use cases for our code.
    • This ensures that we have high code coverage, which makes us look really good.
loveTests

 

So hopefully we don’t hate writing unit tests anymore! But we can't forget that unit testing is just one part of testing code. We still need integration or UI testing to ensure that dependencies work well together, but having the confidence to easily write unit tests will only help us in our journey to high code quality and high code coverage.