In the software development industry, there’s a problematic amount of emphasis placed on the ability to get features, updates, and bug fixes out to customers at the lowest possible time-to-market.
While such speed is necessary to keep up with the very competitive market of software, we often forget that in the long run, internal codebase quality, if not equally prioritized, can affect our abilities to deliver. It’s like factory operations. If machinery is not regularly maintained and kept in top condition, it becomes unreliable and the factory will eventually grind to a standstill.
This is why test-driven development (TDD) is such a valuable technique. It provides a disciplined method to address the internal quality of a codebase, and it is also an excellent method for training yourself and your team to design better software.
In this article, we take an objective look at test-driven development, its benefits, downsides, and best use cases. This will help you understand the concept and also determine if it’s a path worth taking for you and your team.
If you don’t understand what TDD is for, you’re unlikely to practice it effectively.
What is test-driven development?
Test-driven development, or TDD, is a paradigm shift from traditional testing, where rather than testing after the code/feature is complete, you first convert the feature requirements to tests and then write code to make the tests pass.
Assume that we have an email input field and need to write a function to validate email addresses. We’ll first plan/list out requirements for a valid email address. We might end up with a basic list like this:
- The email address is not empty.
- The email address should have the “@” character.
- The email address should have a recipient name before the “@,” so the address can’t start with “@.”
- The email address contains at least one ‘.’ dot and should have a domain name after the “@,” so the address can’t end with “@” either such as (.mydomain).
- The email address must not contain slashes (forward and back).
Following the TDD process, we would convert these requirements into unit tests and then write a validateEmailAddress function that passes those tests
In a typical TDD setting, the process of developing software goes as follows:
- Write a test: the TDD approach for developing a new feature begins with developing and writing tests for each functionality. These tests are nothing but requirement conditions that should be fulfilled by the corresponding future code. This is the most critical part of the TDD process because if you don’t write/plan sufficient tests that really cover the whole ‘picture,’ you can end up writing some crappy code that passes a crappy test. A key benefit of writing tests first is that it compels developers to properly think about how a feature might fail/succeed before they ever start writing code.
- Run the test and see it fail: ensuring that the test fails at this stage eliminates the risk that the new test is flawed and will always pass. It also confirms that new code is actually needed for the desired feature.
- Write code to make the test pass: no matter how inelegant the code is, the goal is for it to pass the test. If there’s any failure, the new code must be revised until it passes.
- This ensures the new code meets the feature requirements and does not break existing features.
- Refactor: after the code has passed the test, it should then be refactored for readability and maintainability, following best practices. Tests should also be run at every refactor to ensure that functionality is preserved.
- Repeat the process from step 1 for every piece of functionality.
What are the benefits of test-driven development?
In writing tests first before code, TDD takes us out of our comfort zone and forces us to think with clarity. It forces us to think critically about the problem we’re trying to solve with code and break it down into bite-size chunks. This singular act results in writing modular code, which increases code quality and lowers maintenance costs.
1. Test-driven development improves code quality
A study showed that 92% of participants surveyed cited an increase in code quality. In another study, a developer team that practiced TDD gradually for five years ended up with better software quality than the industry average.
Writing test first forces developers to eliminate all ambiguity and precisely define what they’re attempting to achieve before breaking it down into smaller pieces of useful functionality that stack upon each other to form a whole.
Rather than attempting to anticipate all requirements and committing to unnecessary abstractions, you design code for one behavior at a time, and you do it correctly. This process prompts better system design and leads to modular code.
Modular code leads to better code quality because it is small, focused, easy to understand, and easy to repurpose and re-use within different contexts. It also decreases the degree to which code complexity increases as software ages.
2. Test-driven development gives developers the confidence to make changes
You’ve either worked on or heard of systems where the internal quality is terrible and how frustrating and difficult it can be to make a seemingly simple change. That wouldn’t be the case if the system had tests. A research study showed that 95.8% of surveyed participants saw a reduction in the overall debugging effort. This is especially beneficial for niches like mobile fintech or health tech, where the cost of bugs is huge.
To have the confidence to safely add, remove, or refactor code without the fear of introducing bugs and regressions, we need tests. This is especially important later on in a project when the codebase is much larger than it was at the beginning, and there’s way more code than one human being can mentally account for.
You could argue that we can always write tests after we write code. But writing tests after the code is written is challenging to get right. Developers will almost always neglect writing tests once their code is working. Or worse, they’ll fall into the trap of conforming their test to the design of the code they’ve written rather than expected behavior.
With TDD, collaboration becomes much easier and more efficient. Team members can edit each other’s code with confidence as the tests notify them if the changes affect the code in unexpected ways.
What are the cons of test-driven development?
Everything with benefits also has trade-offs. Although TDD evidently leads to better software quality, there have been a couple of concerns against the approach from the developer community on why TDD isn’t always a good idea. The major reasons are that TDD slows down development and increases the code maintenance burden.
1. Test-driven development slows down the whole development process
Within companies, deadlines run the day. The major pushback against TDD is that it increases the amount of code a developer has to write to push features out, thereby slowing down the team.
But TDD doesn’t necessarily slow down a team. It is expected that when learning any new skill, you’ll need time to ramp up and familiarize yourself with the pattern and framework of actions. And, like any skill, with practice and training, it becomes easier and faster to work at a sustainable pace and consistently deliver value. That might be a trade-off worth considering for the long-term internal quality that it guarantees.
2. Test-driven development increases the maintenance burden
The second biggest disapproval of TDD is that it adds to the maintenance overhead of a project. Every time change is made to a functionality, tests would also have to be modified. However, this extra maintenance can be considered a negligible cost, seeing as the presence of tests makes it easier to add code without introducing bugs or regressions.
Tests only become a problem when they’re not done right or make the code more complex than necessary. Focus on the quality of your test code. If a test is difficult to read or write, it is telling you something. Listening to your tests is a crucial skill necessary for successfully practicing TDD. This will encourage you to write more focused, modular code, resulting in a better-designed system.
When should you avoid using test-driven development?
While TDD helps a team to prioritize codebase internal quality early on, it’s not the only way to do so. Whether you adopt TDD should depend on the reality of your project and the surrounding situation.
Here are some scenarios or situations where it may not make sense to use TDD:
1. When experimenting or creating a proof of concept
TDD lends itself really well to a clearly defined set of expected inputs, outputs, and requirements. If you’re just working on an experimental project — like proof of concept — then don’t bother with TDD. After you find out if what you have is worth moving forward with or not, that’s when you should think about testing.
2. When dealing with language/framework methods
Unit tests and TDD are fantastic tools for writing good code. They inspire trust that what you wrote will work correctly. However, there’s a point of diminishing returns where more tests aren’t worth the effort. Code that you don’t write yourself or code that your language or framework takes care of for you is code that probably doesn’t need to be unit tested. Think of a sum function provided by a math library. You can probably assume that the sum method works without writing tests for it.
3. When dealing with GUI development
GUIs, by their very nature, are difficult to test. First, because layout is subjective and is better “tested” by humans or using codeless automated testing tools with visual validation, and second, as the UI changes, you’ll constantly be rewriting your tests.
Granted, you can write unit tests for UIs, but it’s difficult and not scalable. Focus on testing the behavior, e.g., this click produces these events, or this data is available or displayed (but not how it’s displayed).
How can you get started with test-driven development for mobile development?
To get started with TDD for mobile development, you can make use of any unit testing framework or libraries supported by your language or framework of choice.
For Android test-driven development, you can make use of popular choices like JUnit and Robolectric. JUnit is a simple framework for writing repeatable tests. It is an instance of the xUnit architecture for unit testing frameworks. Robolectric is a popular Android unit test framework that allows faster test execution by running tests on the JVM (no device or emulator needed).
For iOS test-driven development, you can make use of XCTest. XCTest is a popular choice for iOS developers because it is directly integrated into Xcode, is fully compatible with both Objective-C and Swift, and is maintained by Apple. As a result, it is always up to date with the latest iOS and Xcode versions, and developers can effortlessly run automated unit tests without moving away from their favorite IDE.
For Flutter test-driven development, you can make use of test or flutter_test package. The test package provides the core framework for writing unit tests, and the flutter_test package provides additional utilities for testing widgets.
To TDD or not to TDD?
TDD is a whole culture and testing paradigm shift for a company/engineering team and requires that you get buy-in from both engineers and management. With everything discussed in this article, hopefully, you now have enough information to decide if TDD is worth trying, and also to convince others if you do.
You may work for an organization with lots of legacy code and be wondering how TDD can help. It’s true that TDD will not be able to impact the code already in use — but you can start adopting TDD gradually for new features and bug fixes. Over time, the quality of your codebase will improve as old code gets replaced. TDD isn’t a DO or DON’T do; a few tests and modularizations are better than none.
Run TDD and unit tests as part of the CI/CD pipeline
Bitrise has different steps that help you to run your unit tests on every pull request or even on every code change.
And once your build has run, you can click the Test Reports button and view all your unit test results collected in a single dashboard. Once you’ve analyzed your build’s test results, you can easily navigate back to your app through the top menu bar.
If you do decide to try out TDD for mobile development, you should hook it up with a CI/CD setup using a mobile CI/CD tool, like Bitrise, to automate the whole process and get feedback from tests faster.
- TDD in Android with Rivu Chakraborty
- Java Unit Testing with JUnit - Tutorial - How to Create And Use Unit Tests
- Flutter Testing Guide for Beginners - Part 1: Unit Tests & Setup
- Flutter Testing Guide for Beginners – Part 2: Widget & Integration Tests
- TDD in SwiftUI – Introduction
- Get More From Your Unit Tests - iOS Conf SG 2022
- iOS Dev 30: Getting Started with Unit Testing | Swift 5, XCode 12