Give your simulator superpowers

RocketSim: An Essential Developer Tool
as recommended by Apple

Win a copy of The macOS App Icon Book with thisfree giveaway

Test-Driven Development (TDD) for bug fixes in Swift

Test-driven development (TDD) is a technique that requires you first to write a failing test before you start implementing a solution. While it’s a technique developers use during general development, there’s a way to apply it only for bug fixes.

Finding out about a bug can already be disappointing, but finding out about a bug that re-occurred will be even worse. It’s an essential development skill to prevent a bug from returning after you’ve once fixed it.

What is Test-Driven Development (TDD)?

The Test-driven development (TDD) process requires you to write a failing test before you start implementing the solution. You can use it to define all specifications for a new feature and ensure you do not forget about any expected outcomes.

For example, imagine having the following article struct:

struct Article {
    let title: String
    let author: String
    let link: URL
}

We might want to create a new computed property to return the blog domain for this article. Before diving into writing the actual computed property, we would first define the following test case:

func testArticleDomain() {
    let article = Article(
        title: "Async await in Swift explained with code examples",
        author: "Antoine van der Lee",
        link: URL(string: "https://www.avanderlee.com/swift/async-await/")!
    )
    
    XCTAssertEqual(article.blogDomain, "avanderlee.com")
}

This code will not compile since the blogDomain property doesn’t exist. Since we want the test to fail first, we can add a computed property that returns an empty string:

extension Article {
    var blogDomain: String {
        ""
    }
}

We can now successfully run the unit test and conclude that it indeed fails:

Using Test-driven development, we've first written a failing test.
Using Test-driven development, we’ve first written a failing test.

Finally, we implement the actual logic, and re-run the test to ensure it succeeds:

extension Article {
    var blogDomain: String {
        link.host()?.replacingOccurrences(of: "www.", with: "") ?? ""
    }
}

The workflow looks similar to what many developers do, but the main difference is starting by writing a test instead of jumping directly into feature development.

Benefits of TDD

Developers tend to hesitate to start using test-driven development in their workflow. It feels redundant to write a failing test and a waste of time. Before I start listing a few benefits of TDD, I think it’s important to emphasize you can use this technique as an opt-in toolset when it fits your current task.

I use it occasionally when I want to clear my mind of expected outcomes. By writing tests, you ensure you’ll remember all edge cases and expected outcomes. Secondly, you’ll enable yourself to focus entirely on developing the solution without having to think about the outcomes you have to support constantly: you can run the tests and know that you’re done when they all succeed.

Another benefit of writing failing tests first is to ensure your test actually fails. This sounds stupid, but I’ve seen many tests that would always succeed, even if the implementation changed. In other words, the test wouldn’t catch any wrong implementations.

Lastly, by starting with tests, you’ll ensure your new code becomes testable from the start. When you write new code without tests in mind, writing tests afterward is likely more challenging. Since it becomes harder, it’s easier to skip writing tests at all, resulting in lower test coverage.

Stay updated with the latest in Swift & SwiftUI

The 2nd largest newsletter in the Apple development community with 19,031 developers. Don't miss out – Join today:

You can always unsubscribe, no hard feelings.

The win-wins of solving bugs using tests

The main reason I started writing about Test-driven development is that I’m using it mostly when I’m solving bugs.

Reproducing a bug using a test instead of manual interactions

When I start solving a bug, I first want to know how to reproduce it. The first thing you might do is open your application and start navigating around. However, in my experience, looking at the specific code and writing a test with a similar input as the bug report states is much easier. Ideally, you’ll have enough logs (for example, by using Diagnostics) to know what’s causing the bug. If not, you could read the specific lines of code and use a test to find the trigger.

An extra benefit of writing the test is that you’ll likely better understand the cause of the bug. It won’t surprise me if you’ll be able to solve the bug faster as well. Therefore, TDD works great together with bug fixes.

Solving a bug with the test in place

Once you have the test in place, you can start solving the bug. Depending on the bug, this can be challenging and time-consuming. Having a way to quickly validate your solution can drastically improve the time to fix it. Instead of having to manually perform all kinds of steps, you can simply re-run the test and validate.

I’ve seen developers solving bugs using manual app interactions, where they weren’t even able to reproduce the bug constantly in the first place. In other words, with a bit of luck, you think you solved the bug while you actually didn’t. It’s essential to have a constant reproduction in place before you can consider a fix to be solid.

Higher code-coverage and more confidence

Once you’ve solved the bug, you’ve implemented a new test and increased code coverage simultaneously. You can also be more confident about your codebase as you’ve ensured the bug can’t return without failing your test. Codebase confidence can not be underestimated and is crucial for apps that want to ship updates regularly to a large audience.

Conclusion

Test-driven development is a great technique to increase your test code coverage while increasing your code quality at the same time. It can be tedious to write failing tests at first, but the result will be a more complete feature implementation that covers all expected outcomes. When used in combination with bug fixing, you’ll improve your confidence and ensure a bug won’t return in the future.

If you like to improve your workflow, even more, check out the workflow category page. Feel free to contact me or tweet to me on Twitter if you have any additional tips or feedback.

Thanks!