Package: pytest-mutagen
MutagenMutagen is a plugin to pytest that makes it easy to do mutation testing. Mutation testing is a
method of testing your tests. Mutagen helps you to define "mutant" versions of your code---code
which is intentionally buggy---then you run your test suite on these mutants and verify that your
tests actually catch the bugs. Mutation testing helps you to gauge test coverage and verify that
your tests are good enough to exercise interesting behaviors in your code.For Property-Based TestingIf you are a user of aproperty-based testingframework such as Hypothesis, mutation testing can
also be used to test your input generators. It is relatively easy to write a generator that cannot
generate a certain kind of input. Mutation testing can be used to find those gaps.Installationpython3 -m pip install pytest-mutagenUsagePython importimport pytest_mutagen as mgDeclare a mutantMutant functionTo mutate a whole function you have to write the new version of the function, decorated with@mg.mutant_of(function_qual_name, mutant_name, file (optional), description (optional)). If the mutations affect an object (function or class) you have to be sure that this object exists in the__globals__symbols table of the mutant functions. For this purpose you can simply writefrom [your_module] import [target_object]in the mutation file.
Example:definc(x):returnx+1@mg.mutant_of("inc","INC_OBO",description="Increment is off by one.")definc_mut(x):returnx+2Mutant expressionIf you don't want to change the whole function but only one line, you must decorate the function with@mg.has_mutant(mutant_name, file (optional), description (optional)). Then you have two ways to do it:By replacing the expression by themg.mut(mutant_name, normal_expression, mutant_expression)function, using lambda expressions.Example:mg.mut("FLIP_LT", lambda: a < b, lambda: b < a)Using themg.not_mutant(mutant_name)function combined with anifstatement.Example:k = inc(k) if mg.not_mutant("INC_OBO2") else inc(k) + 1Mutating a class methodIn fact the@mutant_ofdecorator doesn't require the function name but its fully qualified name. It does not change anything for top-level functions but in the case of a class method you need to write the dotted path leading to the object from the module top-level.
Example:classFoo:defbar(self):pass@staticmethoddefstatic_bar():pass@mg.mutant_of("Foo.bar","")defbar_mut(self):pass@mg.mutant_of("Foo.static_bar","")defstatic_bar_mut():passGlobal functioningMutagen collects all declared mutants, stored per file names. Then it looks through all tests collected by pytest and apply the mutants to the matching files. This is handled by the optional file parameter in@has_mutantand@mutant_ofwhich can be a file name or a list of file names where you want your mutant to be applied. You can set it to APPLY_TO_ALL (constant string declared in mutagen) if you want it to be applied to all collected files. By default, file is:APPLY_TO_ALL for@has_mutantthe current file name for@mutant_of(the one where it is written)Therefore you can either:write your mutations and specify for each one where you want it to be applied (use the functionmg.link_to_file(filename)at the beginning of your file to link the current file to the specified filename)or create a mutations.py file where you import all test files you want (from testfile.py import *), write yourmutant_ofwith no file specified and run pytest on mutation.py.Run the testspython3 -m pytest --mutateQuick runThe--quick-mutoption will stop each mutant after its first failed test. If not specified each mutant will run the whole test suiteCache useMutagen stores in the pytest cache the functions that failed during the last run, for each mutant. For the next runs it will try these functions first, in order to find failures more quickly. If you don't need this feature you can simply use the--cache-clearoption that will clear the cache before running the tests.Run only the mutationsIf you don't want to run the original test suite but only the mutations you can use the pytest option--collect-onlySelective run of mutantsThe--selectoption expects a comma-separated list of mutants (no spaces) and will run these ones exclusively.
Example:python3-mpytest--mutate--selectINC_OBO,FLIP_LTMutagen statsThe--mutagen-statsoption adds a section to the terminal summary, which displays the number of tests that caught each mutant.Add trivial mutationsTo find holes in a test suite with mutagen, we often try trivial mutations on some functions (like
replacing them with pass) to see whether a lot of tests catch them or not.
For this purpose thetrivial_mutations(functions, obj=None, file=APPLY_TO_ALL)function with a
list of functions as input adds all mutants corresponding to replacing them by an empty function.
There are two ways to use it:frommoduleimportsort,invert,ExampleClass# With a list of top-level functionsmg.trivial_mutations([sort,invert])# With a list of method names and the corresponding objectmg.trivial_mutations(["sort","clear"],ExampleClass)This is equivalent to doing this:frommoduleimportsort,invert,ExampleClassmg.link_to_file(mg.APPLY_TO_ALL)@mg.mutant_of("sort","SORT_NOTHING")defsort_mut(*args,**kwargs):pass@mg.mutant_of("invert","INVERT_NOTHING")definvert_mut(*args,**kwargs):pass@mg.mutant_of("ExampleClass.sort","EXAMPLECLASS.SORT_NOTHING")defsort_mut(*args,**kwargs):pass@mg.mutant_of("ExampleClass.clear","EXAMPLECLASS.CLEAR_NOTHING")defclear_mut(*args,**kwargs):passtrivial_mutationshas an optionalfileparameter to specify the test file where the mutations
should be applied, which is by default set to APPLY_TO_ALL.The functiontrivial_mutations_all(object, file=APPLY_TO_ALL)applies this process to each
method of the class (or list of classes) given as a parameter.
Example:frommoduleimportExampleClassmg.trivial_mutations_all(ExampleClass)ExamplesYou can find some examples in the examples folderThe file short_example.py is a very simple example of the use of mutagen to test a merge sort functionThe file BST_mutations.py implements the Binary Search Tree data structure, and the test suite and mutations fromHow to specify it!(John Hughes, 2019)The subfolder separate_files is an example of the separation between the source file, the test file and the mutation fileThe run-tests.py scripts show how to run these testsAutomatic toolWriting mutations by hand can be very long and we are aware that it can discourage a lot of
programmers from using pytest-mutagen, that is initially a manual mutation-testing tool. To fix
this problem while keeping the ability to manually edit the mutants we've added the possibility
to interactively generate a bunch of mutants following a set of rules.Usagepython3-mpytest_mutagen[-h][-oOUTPUT_PATH][-mMODULE_PATH]input_pathThis command will browse the providedinput_path(that can be a file or a directory) and
interactively propose several mutants. You can accept them by pressing ENTER and refuse them by
typing 'n' then ENTER. The purpose of this is to avoid false positives and equivalent mutants, that
are among the main problems of mutation testing. Finally all accepted mutants are written in the
mutagen syntax (ready to be use withpytest --mutate) inmutation.pyor the file/directory
specified with the-ocommand-line option.
For more details on its use you can usepython3 -m pytest_mutagen --help.RulesIntegers are incrementedOperators are switched to a different (but close) operatorIn assignments, the right value is replaced withNoneThe return statement is removedThe condition ofifstatements are replaced withnot (condition)