Who is Testing the Tests?

Rate this content
Bookmark

Have you ever wondered: "who's testing the tests"? Of course, tests are only valuable if they catch bugs, but how would one validate that? Well, let me tell you about mutation testing!


Mutation testing is the act of testing your test verifying that they catch bugs. Of course, you can do this manually inserting bugs and running the tests, but a mutation testing framework can do this for you!


Join me and learn the basics of mutation testing and how to use StrykerJS, the mutation testing framework for JavaScript or TypeScript.

At the end of this talk, you'll be the one that is testing your tests, and it won't even cost you much time!

This talk has been presented at TestJS Summit 2021, check out the latest edition of this JavaScript Conference.

FAQ

Mutation testing is a method used to improve the quality of tests by deliberately introducing bugs (mutations) into the source code to check if the existing tests can detect and fail because of these changes. This process helps verify the effectiveness of the test suite in catching errors.

Code coverage only measures whether parts of the code are executed during testing, not whether the tests are effectively checking the correctness of the code. It doesn't ensure that all functional aspects and potential errors are tested, leading to a false sense of security.

In mutation testing, small changes called mutations are made to the source code to mimic potential bugs. Tests are then run to see if they fail due to these changes. If a test fails, the mutation is 'killed'; if not, the mutation 'survives'. This process assesses the ability of the test suite to detect and handle errors.

An example of a mutation could be changing a 'greater than or equal to' (>=) operator to a 'less than' (<) operator in a condition check within the code. This change tests whether the existing tests are robust enough to catch such a modification.

Mutation testing helps identify weaknesses in a test suite by showing whether the tests can detect minor, deliberate errors introduced into the code. This method encourages the development of more comprehensive tests, leading to higher quality software that is more resilient to bugs.

Common mutations include arithmetic changes (like changing '+' to '-'), logical shifts (such as altering boolean operators), and structural mutations (like modifying loop boundaries or conditional statements). Each mutation tests different aspects of the test's ability to catch errors.

Stryker.js is a mutation testing framework specifically designed for JavaScript and TypeScript. It automates the process of applying mutations to the code and running tests to see if they fail, which helps developers measure and improve the effectiveness of their test suites.

For large enterprise applications, mutation testing provides a detailed analysis of test suite effectiveness, helping to identify specific areas where tests may be lacking. This can be crucial for maintaining high code quality and reliability in complex software environments.

Simon de Lang
Simon de Lang
8 min
19 Nov, 2021

Comments

Sign in or register to post your comment.

Video Summary and Transcription

Mutation testing is a method to improve test quality by inserting bugs into code to test if tests can detect them. Mutation-testing frameworks like Striker.js allow for various mutations to be performed. Mutation testing provides a mutation score that is a better tool than code coverage for measuring test quality. It can help identify missing tests or bugs in existing tests. Stryker is recommended for JavaScript and TypeScript mutation testing.

1. Introduction to Mutation Testing

Short description:

Hi, I'm Simon, a software engineer at InfoSupport. Let's talk about how mutation testing can improve test quality. Code coverage is a bad metric for measuring test quality. Mutation testing inserts bugs into code to test if tests can detect them. It generates mutants and runs tests to detect failing ones. Hundreds of mutations are performed to generate a comprehensive report. An example of mutating code is changing a greater than equals sign to a less than sign.

Hi, I'm Simon. I'm a software engineer working at InfoSupport, and today I want to talk to you about how mutation testing can help to improve the quality of your tests. But before we dive into mutation testing, I first want to talk a bit about code coverage.

Code coverage is often used by developers as a means to measure the quality of their tests. In my eyes, it's a bad metric to measure this. The only thing code coverage actually tests is whether or not codes get executed. The reason I write unit tests is to verify that my code works right now and to make sure it will keep working in the future. And if something happens and my code behaves differently, I will get a failing test. And again, code coverage does not measure this, it only measures if your code gets executed.

So, how do we actually measure the quality of your tests? Well, that brings me to the topic of this talk, mutation testing. Mutation testing is a way to insert bugs into your source code to actually test if your tests can pick them up. How does this work? Well, a mutation testing framework will start with your source code, which is all just happy and fine and nothing is wrong with it. Then it will make one small mutation for your source code, this will generate a mutant. For example, A plus B could be mutated into A minus B. This will result in a different outcome for your source code. And because your source code has changed, it should cause a failing test. So, the mutation testing framework will run your tests and it can have either outcomes. Either the mutant has been detected because of a failing test, in that case the mutant has been killed, or none of your tests failed and the mutant was able to survive. In mutation testing, we want to have killed mutants. So, we want tests to actually fail because we have inserted a bug. In mutation testing framework, we'll do this hundreds or thousands of times for your source code, and in the end, we'll combine it all together and generate a nice report.

So, how does this mutating work in actual code? Well, here I have a small JavaScript function on the top to check if a customer is allowed to buy alcohol. This customer is allowed to buy alcohol in this country if the age is at least 18. And below I have a test for Professor X, who is age 96 and is thus allowed to buy alcohol. In mutation testing look at the source code above and make a small change. In this case, the greater than equals sign will be changed into a less than sign, thus flipping the check around. If we make this change in our source code ourselves, the test will fail, thus the mutant has been killed. This proves that we have a test for this specific case and this bug cannot be inserted into our code without us knowing it. We can also change it in another way. For example, we can change it into a greater than.

2. Mutation Testing and Frameworks

Short description:

We started with greater than equals, now it's greater than. Changing the entire return statement to always return 2 also passes. Mutation-testing frameworks allow for various mutations like changing signs, emptying strings and arrays, and flipping operations. Striker.js is recommended for JavaScript and TypeScript, but there are options for other languages as well. A small demo of striker.js is available on our website. Running mutation tests can be slower for large applications due to the number of mutations being tested. The demo application generated 126 mutants from 12 source files.

We started with greater than equals, now it's greater than. Since we only have one test case for someone aged 96, all our tests pass and the mutant has survived. We can also change the entire return statement and just simply always return 2. In our case, it still passes and we have a surviving mutant.

This is very simple code. Most of our code does not look like this and most of our tests are also more complicated. While someone looking at this code might be able to say, hey, obviously you're missing some test cases, in the production code that we're writing, it's often a lot more difficult.

So, what kind of mutations can you expect for mutation-testing frameworks? Well, the definite list depends on the framework and the language that you're using. But in most cases, mutations like this are possible. For example, changing the plus sign into a minus sign, emptying strings, emptying arrays, flipping operations around, you can do a lot of things as a very small change in your code. And only one of these changes will be active at a time to ensure we know which mutation is causing a test to fail.

Well, mutation-testing is available for a lot of different frameworks. And since we're at a JavaScript conference, I would recommend striker.js as the framework to use for JavaScript and TypeScript. But it's available for pretty much every language that you know. If it's not on this list, simply Google it for your language and you will probably find something.

So, for striker.js, I have a small demo. The demo application that we have is also available on our website. The link will be at the end of the slides with information how you can set this up yourself. I can start striker from the command line. And it will start running our tests. Since the demo application is a small application, it's quite fast. If you have a large enterprise application, you will most likely notice that mutation testing is quite a bit slower than running the unit test. Because there are thousands of mutations being made and each and every one of them has to be tested. So, my laptop is starting up right now. It's testing. And in about a second or two, it will actually be done. For our application, it has 12 source files to mutate and it was able to generate 126 mutants. So, it's done. We get a nice table here as an output. But we also have a HTML report.