A mutation testing library in Elixir. Inspired by pitest
(http://pitest.org) and mutant
.
Mutation testing [...] is used to design new software tests and evaluate the quality of existing software tests. Mutation testing involves modifying a program in small ways. Each mutated version is called a mutant and tests detect and reject mutants by causing the behavior of the original version to differ from the mutant. This is called killing the mutant. Test suites are measured by the percentage of mutants that they kill. New tests can be designed to kill additional mutants.
The exavier
library mutates code in parallel per module, but mutates each module sequentially per mutator. Initial code line coverage analysis is done sequentially for all modules as a pre-processing step. It is better explained as follows:
- Run code line coverage analysis for each module, sequentially
- Mutate the code according to each available mutator
- For each module, in parallel:
- For each mutator, sequentially:
- Mutate code with given mutator
- Run tests once again (now against mutated code)
- Record results (% mutants survived vs. killed)
- For each mutator, sequentially:
- For each module, in parallel:
Mutators specify ways in which we can mutate the code. Currently we have 13 mutators available in exavier
:
- AOR1
- AOR2
- AOR3
- AOR4
- ROR1
- ROR2
- ROR3
- ROR4
- ROR5
- IfTrue
- NegateConditionals
- ConditionalsBoundary
- InvertNegatives
AOR
stands for "Arithmetic Operator Replacement". There are several possibilities for replacing an arithmetic operator. We follow the ones defined by pitest
. Similarly, ROR
stands for "Relational Operator Replacement". IfTrue
is inspired by pitest
's "Remove Conditionals".
NegateConditionals
is also inspired by pitest
.
You can create new mutators. You just have to make sure they abide to the interface provided by behaviour Exavier.Mutators.Mutator
:
defmodule Exavier.Mutators.Mutator do
@type operator() :: atom()
@type metadata() :: keyword()
@type args() :: term()
@type ast_node() :: {operator(), metadata(), args()}
@type lines_to_mutate() :: [integer()]
@callback operators() :: [operator()]
@callback mutate(ast_node(), lines_to_mutate()) :: ast_node() | :skip
end
An Exavier.Mutators.Mutator
has two mandatory functions:
-
operators/0
- input:
- (none)
- output:
- an array of atoms (operators to which the mutation can be applied, e.g.,
[:==, :>=]
)
- an array of atoms (operators to which the mutation can be applied, e.g.,
- input:
-
mutate/2
- input:
- AST node (e.g.,
{operator, meta, args}
) - lines that should be mutated as part of that mutation (array of integers, e.g.,
[3, 6]
)
- AST node (e.g.,
- output:
- mutated AST node (e.g.,
{operator, meta, args}
)
- mutated AST node (e.g.,
- input:
Add custom mutators to .exavier.exs
under the :custom_mutators
key:
%{
...
custom_mutators: [
MyApp.MySpecialMutator
]
...
}
The package can be installed by adding exavier
to your list of dependencies in mix.exs
:
def deps do
[
{:exavier, "~> 0.3.0"}
]
end
Run mix exavier.test
and you should see output similar to this:
......................
(...)
16) test when infinity (Elixir.HelloWorldTest)
- if(y == :special) do
- :yes
- else
- :no
- end
+ if(true) do
+ :yes
+ else
+ :no
+ end
/Users/dnlserrano/Repos/exavier/test/hello_world_test.exs:10
22 tests, 6 failed (mutants killed), 16 passed (mutants survived)
27.27% mutation coverage
exavier
provides the following configuration options via dotfile .exavier.exs
:
:threshold
: Mutation testing coverage threshold (can be integer or floating point number)
# .exavier.exs
%{
...
threshold: 67
...
}
:test_files_to_modules
: Overrides the default mapping of finding a module based on its test file name. E.g., test filetest/my_file_abc_test.exs
might be testing moduleMyFileABC
instead ofMyFileAbc
(exavier
's default mapping)
# .exavier.exs
%{
...
test_files_to_modules: %{
"test/my_file_abc_test.exs" => MyFileABC
}
...
}
:custom_mutators
: Adds mutator modules to be run during tests. See the Mutators section for directions on how to create your own mutators.
%{
...
custom_mutators: [
MyApp.MySpecialMutator
]
...
}
This is for now just a proof-of-concept. A lot of it has been no more than a joyful exercise in exploring what tools Erlang and Elixir provide to make such a library possible. Among some things I'd love to tackle in the near future are:
- Add way more tests (OMG the irony, forgive me, this is still a bit of a PoC as you can tell by the length of this "To be done" section)
- Add mutators
- AOR1
- AOR2
- AOR3
- AOR4
- ROR1
- ROR2
- ROR3
- ROR4
- ROR5
- Remove Conditionals
- Can still be done for
case
,unless
- Can still be done for
- Conditionals Boundary
- Negate Conditionals
- Invert Negatives
- Ability to tune which mutators are used
- Ability to add custom mutators defined by the user (i.e., not in
exavier
) - Analyse if we really should or shouldn't care about pre-processing step of code line coverage
- Have other ways of terminating mutation test suite (e.g., fast-fail if threshold of X mutants have survived)
- Parallelise mutating module per mutator
- Discussion of the library at ElixirForum.com
- I wrote about
exavier
in my personal blog - Always happy to chat in the
elixir-lang
Slack channel over at#exavier
Inspired by Dr. Charles Xavier (Professor X) from the X-Men mutants comic books I read as a kid.
Thanks to Tita Moreira ❤️ for putting up with my nerdiness.
Thanks to Richard A. DeMillo, Richard J. Lipton and Fred G. Sayward for their seminal work on Mutation Testing back in 1978, with the paper "Hints on Test Data Selection: Help for the Practicing Programmer".
Thanks to Henry Coles for pitest
and Markus Schirp for mutant
, which served as an inspiration for this project.
Copyright © 2019-present Daniel Serrano <danieljdserrano at protonmail>
This work is free. You can redistribute it and/or modify it under the
terms of the MIT License. See the LICENSE file for more details.
Made in Portugal 🇵🇹 by dnlserrano