Developers probably spend a lot of time writing, running, and maintaining tests, and having them all pass is a great feeling! But what if there is something missing? In this case, developers and testers can be blissfully unaware that they're shipping time bombs to the customer. How would they know that? Who is testing the tests?
As developers we’ve been all at some point in our career in a situation where we would have forgotten to write a test for a certain function no matter what the reason was and this can lead to some issues:
Unit testing is standard practice now, but we can only rely on our suite, if we make sure it covers enough of the application’s functionality. A great way to do this is with a mutation testing, but in order to understand its true value, we need to think about why we actually need to write tests in the first place.
There are lots of reasons to write tests, to name a few:
TDD (Test-Driven Development) is not enough for delivering lean code that works exactly towards expectations. Mutation testing is a powerful step forward.
Mutation testing is a fault-based testing technique that investigates how systems would behave if something changes. This is done to determine the effectiveness of the test set by isolating the deviations. It sounds a little complicated, isn’t it? Let’s break it down and try to understand it piece by piece.
So, mutation tests are a way for us to validate the tests we wrote in the way we expect them to, and this solves the problem of who tests the tests.
A mutation score reflects how effective a test suite is at detecting logic changes and can tell us how performant our tests are by noticing whether or not a side effect occurre
Briefly, the mutation testing process works as follows:
- Surviving mutant if all tests pass in.
- Killed mutant if at least one test failed in.
A really cool thing nowadays about mutation testing is that you do not write them! That’s handled by a tool for you. It does this by introducing small changes into your app’s code.
The key thing is that these mutants always mimic realistic programming errors :).
The existence of surviving mutants exposes which pieces of code need changes.
The mutation score is defined as follows:
If the mutation score is 100%, test cases are mutation adequate. This is usually not achievable, like a 100% code coverage.
To answer the question “How do I know it works, especially since over time the complexity of the app is going to grow, and it is not going to be only one developer who touches the code?” we can simply refer to the mutation score.
Note: All the following examples are in Swift.
We have to mention that mutation testing relies on two things:
My advice here is: Be very intentional with what you are choosing to be coupled. Unit tests should couple to the method implementation, UI tests should couple to the navigation hierarch
Hint: some tools allow you to explicitly specify files to run mutation.
As human being you can’t always avoid making a mistake, but when it comes to software development, there are ways to catch that mistake before it goes to production. One way is mutation testing. Even though it is time-consuming and requires automation, mutation testing is still the undisputed best technique to test any program and highlights code that needs additional tests or better assertions.
I hope that I managed to give you a brief introduction on what mutation testing is about and how it works, and it’s over to you now to look into how to leverage it and put it into practice. Otherwise, if you are interested to dig deeper into the topic, we'll put mutation testing to good use and tackle some everyday scenarios in a future article.