In this thread I will discuss everything about unit tests. Examples will be in Swift and XCTest framework, but the principles apply generally to all languages and test frameworks.
Thread 🧵👇
Unit test is a method that runs a piece of code to verify that a known, fixed input produces a known, fixed output. If the assumptions turn out to be wrong, the unit test considered failed.
We all make mistakes. Unit tests verify that our code works and it keeps working. They allow us to make sweeping changes all over the code base.
When you test something, you refer to it as the system under test (SUT).
System under test is a sum of actions that has a single noticeable end result in the system. Typically, SUT is a single method or function.
Single means that a test must verify one concert. A good way to think about it: if the first check fails, does it matter what happens to the next one? If yes, you probably test more than a “unit”.
It's not enough to define what a unit test is. More importantly, we must highlight what makes a good unit test.
There is not point in writing a bad unit test, unless you want to learn how to write a good one.
- Readable
- Maintainable
- Trustworthy
The rest follows from these three.
Every time something breaks in our code, we examine failing tests. This happens very often. We must understand what's wrong just by looking at test code. We are likely to delete or rewrite the test if we can't figure things out.
Readability can be broken into:
- Naming unit tests
- Naming variables
- Maintaining uniform test structure
- Writing descriptive assert messages
I recommend to fix these things in a style guide. Like this one google.github.io/swift/
There is a well-established practice to structure a unit test around three steps:
- Given: prepare test data and system under test.
- When: poke system under test.
- Then: verify that output is as expected.
Maintainability is another core quality of a good unit test.
Otherwise tests can ruin project deliveries and are candidates to be disabled when schedule becomes aggressive.
Ideally, we should change our tests only when our system's requirements change. Refactoring should not not result in a cascade of changes in the tests.
- Verify one thing per test.
- Test only publics and internals. Do not weaken encapsulation just to verify private behavior.
- Do not put code into production which is there only to support testing.
- Reuse code. If tests contain lots of duplication, it’s time to introduce new abstractions. Create helper methods, introduce new test classes. Extract commonly used test data.
- Make tests atomic. Tests should not depend on the order in which they run.
- Every test must start fresh. Cleanup before and after each test if they modify globals.
- Isolate system under test from its dependencies. Use mocks to mimic their behavior for testing.
- Do not call a test method from another test method. This couples the tests and makes them fragile.
It's common to initialize SUT and dependencies in setup(). Don't do that!
- setup() couples tests and makes them fragile.
- It grows over time, making tests unmanageable.
- Tests get more dependencies than they actually need.
Don't test too much:
- Avoid testing internal state even if it is public. It can change later.
- Don't assert three values when one is enough.
- Never use real database, network, device properties and settings (time, language, locale) in tests. They are the primary source of fuzziness.
Whenever possible, avoid testing asynchronous code:
- Mock all dependencies, so that they become synchronous.
- Break an async test into two. One test can verify SUT before the async execution and one after.
If it's impossible to avoid testing async code, use special patterns to control test flow:
- Signaling. Notify the waiting test that an async operation has completed.
- Busy waiting. Re-assert a value in a loop until the condition holds.
Trustworthiness is the last core quality of a good unit test.
A test is trustworthy when it makes clear what’s going on and that you can do something about it.
When a test passes, you trust such test and that the code works under this scenario. And when the test fails, you don’t tell yourself that this does not mean that the code is not working.
Tests must always have 100% pass rate.
Having at least one failing test results in a broken window effect.
Having tests that sometimes fail will soon infect the whole suite. It is usually a sign of more serious problems.
Avoid test logic. No ‘for’ or ‘while’ loops,. No ‘if’, ‘else’ or ‘switch’ statements. Not even a ‘do-catch’. They are another source of flaky and buggy tests.
Unit tests must be fast. How fast? I believe that less than 0.1 second per test. We have around 5000 tests in our project. It would take > 8 minutes to run them. This is too long of a feedback when doing TDD.
A piece of code is testable if we can quickly and easily write unit tests against it.
Testable code must have two properties:
- SUT can be isolated from its dependencies;
- SUT must have points where we can “sense” the change.
We strive to:
- have public dependencies,
- depend on abstractions, instead of implementations.
Such dependencies are easy to mock during testing.
Books 📖:
- xUnit Test Patterns amazon.com/xUnit-Test-Pat…. Classical book on unit testing, highly recommended.
- The Art of Unit Testing amazon.com/Art-Unit-Testi…. Teaches writing tests that are readable, maintainable and trustworthy.
- Working Effectively with Legacy Code amazon.com/Working-Effect…. How to deal with a large legacy of untested code. Explains high-level patterns that can dramatically improve your maintenance tasks.
- The Clean Code Talks . Must see introduction into unit testing.
- Effective Unit Testing .
- Testing legacy code .
Martin Fowler on unit testing:
- Basics martinfowler.com/bliki/UnitTest…
- Test pyramid martinfowler.com/articles/pract…
Microsoft on unit testing:
- Basics docs.microsoft.com/en-us/visualst…
- Best practices docs.microsoft.com/en-us/dotnet/c…
- Getting started vadimbulavin.com/real-world-uni…
- Async testing vadimbulavin.com/unit-testing-a…
- Busy assertion pattern vadimbulavin.com/swift-asynchro…
- Best practices vadimbulavin.com/unit-testing-b…
- Unit testing the UI vadimbulavin.com/unit-testing-v…