In my last article, I helped you set up mutation testing using the Infection library on an example project that implemented a simple card game. Of course, the real world is not so simple. When you try to implement mutation testing in your own project, you're likely to encounter stumbling blocks and questions such as:
These issues can seem overwhelming at first, but fear not! In this post I'll walk you through how to get mutation tests running (and running well) on a real project.
I've updated my example Mutation Testing application to demonstrate two of the most common problems in mutation testing: errors and timeouts. Try it yourself:
pecl install pcov # If you don't already have pcov installedgit clone https://github.com/danepowell/mutation-example.git --branch errorscd mutation-examplecomposer install./vendor/bin/infection --show-mutations
The results should look something like this:
It may not be immediately obvious what each part of this report indicates, and indeed, whether it's good or bad. Let's break it down:
As mentioned, timeouts and errors don't indicate a problem with the quality of your source code; they are simply a byproduct of mutation testing. However, they are still a cause for concern because over time they can degrade the performance of mutation tests by increasing the amount of required time and resources (especially memory.)
For instance, consider the following mutation which generates an error:
In this case, the Increment mutator causes an infinite loop, leading to memory exhaustion. Besides wasting time and resources on a test that you know will fail, this may have knock-on effects and cause stability issues on the machine running tests, so it's best to address these errors by disabling mutators on the affected line using comments such as /** @infection-ignore-all */.
The same mutation could result in a timeout instead of an error if the process runs out of time before it runs out of other resources:
In either case, the easiest fix is to disable mutators for that line of code.
You might be inclined to increase the Infection timeout in order to 'fix' timeouts. Unless you know that your code legitimately takes longer than the timeout to function, this is likely to just exacerbate the problem and either convert timeouts to errors (i.e., memory exhaustion) or make tests take longer.
When you add mutation testing to an existing project, the problem you're most likely to encounter is that mutation tests fail when running the initial test suite, resulting in a rather alarming error:
Before actually running any mutation tests, Infection ensures your tests are passing by running the initial test suite. This is an important step, because otherwise failing tests would appear as caught mutants, erroneously boosting your mutation score indicator (MSI).
You might wonder why your tests pass in PHPUnit and fail in Infection. The most likely answer is that Infection runs tests in parallel and random order, whereas PHPUnit by default runs tests serially in a static order.
To ensure PHPUnit behaves like Infection, use a tool like ParaTest to run tests in parallel and configure PHPUnit to run tests in random order by updating your phpunit.xml:
The Infection documentation provides guidance on the most common root causes of test failures and some workarounds. Having fully independent test cases that can run in parallel will make your tests much faster and more robust. For instance, Acquia CLI is a great example of how to implement mutation testing on a complex application and is able to run over 350 functional tests in less than 3 seconds.
If you follow the best practices defined here and run PHPUnit tests yourself prior to running Infection, you can save even more time by using the
--skip-initial-test
flag.
If you have tests that cannot be parallelized, or that otherwise are incompatible with mutation testing, it's easy to exclude them by adding the testFrameworkOptions
directive to infection.json5
. For instance, a real-world configuration file that excludes test cases annotated as 'serial' might look like this:
{ "$schema": "vendor/infection/infection/resources/schema.json", "source": { "directories": [ "src" ] }, "logs": { "stryker": { "report": "main" }, "github": true, "html": "var/infection.html" }, "mutators": { "@default": true }, "timeout": 300, "testFrameworkOptions": "--exclude-group=serial"}
Any value you provide for testFrameworkOptions
will be passed directly to PHPUnit, so you could also use --filter
or similar arguments.
Hopefully this demystifies mutation testing and you're ready to go improve your tests. If you get stuck, be sure to check the comprehensive Infection documentation, and if you're still stuck open an issue on GitHub.
If this content did not answer your questions, try searching or contacting our support team for further assistance.
Fri May 03 2024 17:15:49 GMT+0000 (Coordinated Universal Time)