Giter Club home page Giter Club logo

ooze's Introduction

Go Reference Go Report Card CI Workflow Mutation Testing Workflow

Mutation Testing?

Mutation testing is a technique used to assess the quality and coverage of test suites. It involves introducing controlled changes to the code base, simulating common programming mistakes. These changes are, then, put to test against the test suites. A failing test suite is a good sign. It indicates that the tests are identifying mutations in the code—it "killed the mutant". If all tests pass, we have a surviving mutant. This highlights an area with weak coverage. It is an opportunity for improvement.

There are different types of changes that mutation tests can perform. A common collection usually include:

  • Changing an operator;
  • Replacing a constant;
  • Removing a statement;
  • Increasing/decreasing numbers;
  • Flipping booleans;

Mutations can also be domain/application-specific. Although, these are up to the maintainers of such application to develop.

It is worth mentioning that mutation tests can be quite expensive to run. Especially on larger code bases. And the reason is that for every mutation, on every source file, the entire suite of tests has to run. One can look at the bright side of this and think as an incentive to keep the test suites fast.

Mutation testing is a great ally in developing a robust code base and a reliable set of test suites.

Quick Start

Prerequisites

In order to ensure that you get accurate results, make sure that test suite that Ooze will run is passing. Otherwise, Ooze will report as if all mutants have been killed.

When Ooze reports that it found a living mutant, it will print a diff of the changes the virus made to the source file. The mutant source is printed using Go's go/format package. This means that, if your source code isn't gofmt'd, the diff may contain some formatting changes that are not relevant to the mutation. This isn't a prerequisite per se, but for a better experience, it is recommended that you run gofmt on your source files.

Installation

  1. Install ooze:

    go get github.com/gtramontina/ooze

    This pulls the latest version of Ooze and updates your go.mod and go.sum to reference this new dependency.

  2. Create a mutation_test.go file in the root of your repository and add the following:

    //go:build mutation
    
    package main_test
    
    import (
    	"testing"
    
    	"github.com/gtramontina/ooze"
    )
    
    func TestMutation(t *testing.T) {
    	ooze.Release(t)
    }

    The build tag is so you can better control when to run these tests (see the next step). This is a test as you'd write any other Go test. What differs is what the test actually does. And this is where it delegates to Ooze, by Releaseing it.

  3. Run with:

    go test -v -tags=mutation

    This will execute all tests in the current package including the sources tagged with mutation. This assumes that the above is the only test file in the root of your project. If you have other tests, you may want to put the mutation tests in a separate package, under ./mutation for example, and configure Ooze to use .. as the repository root (see WithRepositoryRoot below).

    If -v is enabled, Ooze will also be verbose. To enable Ooze's verbose mode only without the test framework verbosity, use -ooze.v.

    Note printing to stdout while Go tests are running has its intricacies. Running the tests at a particular package (without specifying which test file or subpackages, like ./...), allows for Ooze to print progress and reports as they happen. Otherwise, the output is buffered and printed at the end of the test run and, in some cases, only if a test fails. This is a limitation of Go's testing framework.

Results

Once all tests on all mutants have run, Ooze will print a report with the results. It will also exit with a non-zero exit code if the mutation score is below the minimum threshold (see WithMinimumThreshold below). This is an example of the report:

report sample

More examples of the results can be found in the mutation.yml workflow.

Settings

Ooze's Release method takes variadic Options, like so:

ooze.Release(
	t,
	ooze.WithRepositoryRoot("."),
	ooze.WithTestCommand("make test"),
	ooze.WithMinimumThreshold(0.75),
	ooze.Parallel(),
	ooze.IgnoreSourceFiles("^release\\.go$"),
)

The table below presents all available options.

Option Default Description
WithRepositoryRoot . A string that configures which directory is the repository root. This is usually required when your mutation test file lives some other place that is not root itself.
WithTestCommand go test -count=1 ./... The test command to run, as string. You may configure it as you wish, as a makefile phony target, for example. Or simply run the standard go test command with extra flags, such as timeout and tags.
WithMinimumThreshold 1.0 A float between 0.0 and 1.0. This represents the minimum mutation test score to consider the execution successful.
Parallel false Indicates whether to run the tests on the mutants in parallel. Given Ooze is executed via Go's testing framework, the level of parallelism can be configured when running the mutation tests from the command line. For example with go test -v -tags=mutation -parallel 3.
IgnoreSourceFiles nil Regular expression representing source files to be filtered out and not suffer any mutations.
WithViruses all available (see below) A list of viruses to infect the source files with. You can also implement your own viruses (generic or even application-specific).
ForceColors false Forces colors in logs. This is useful when running the mutation tests in a CI environment, for example.

Viruses

Virus Name Description
arithmetic Arithmetic Replaces + with -, * with /, % with * and vice versa.
arithmeticassignment Arithmetic Assignment Replaces +=, -=, *=, /=, %=, &=, |=, ^=, <<=, >>= and &^= with =.
arithmeticassignmentinvert Arithmetic Assignment Invert Replaces += with -=, *= with /=, %= with *= and vice versa.
bitwise Bitwise Replaces & with |, | with &, ^ with &, &^ with &, << with >> and >> with <<.
cancelnil Cancel Nil Changes calls to context.CancelCauseFunc to pass nil.
comparison Comparison Replaces < with <=, > with >= and vice versa.
comparisoninvert Comparison Invert Replaces > with <=, < with >=, == with != and vice versa.
comparisonreplace Comparison Replace Replaces the left and right sides of an && comparison with true and the left and right sides of an || with false. E.g. 1 == 1 && 2 == 2 gets two mutations: true && 2 == 2 and 1 == 1 && true.
floatdecrement Float Decrement Decrements floating points by 1.0.
floatincrement Float Increment Increments floating points by 1.0.
integerdecrement Integer Decrement Decrements integers by 1.
integerincrement Integer Increment Increments integers by 1.
loopbreak Loop Break Replaces loop break with continue and vice versa.
loopcondition Loop Condition Replaces loop condition with an always false value.
rangebreak Range Break Adds an early break to ranges.

Custom viruses

Ooze's viruses follow the viruses.Virus interface. All it takes to write a new virus is to have a struct that implements this interface. To get this new virus running, let Ooze know about it by running Release with the WithViruses(…) option. In order to test it, you may want to use the oozetesting package to help out. Take a look at the existing viruses to have an idea.

If your new virus is domain-agnostic, and you find it useful, consider contributing it to this project. You can also write domain-specific viruses. One that looks for a particular struct type and change it in a particular way, for example.

Tips

  1. Ooze runs your test suite for every mutant it creates. Having a fast suite is a good idea. The way Ooze detects that a mutant was killed is by having a failing test. The quicker your suite catches the faster the mutation testing will finish. Go testing framework allows for us to flag it to fail fast with -failfast. Although this is better than nothing, this doesn't work across packages (see this issue for more details). This is where gotestsum comes in. It allows us to fail even faster by configuring it with --max-fails=1.
  2. Mutation testing usually takes a significant amount of time to run. Especially if you have a large codebase. It may be a good approach to run it on a separate path on your CI pipeline; preferably after you get confirmation that your test suite is passing. This way you can get the results of the mutation testing without slowing down your main pipeline.
  3. Ooze runs itself. I recommend exploring this codebase to get a better idea of how to use it.

Prior Art

Ooze is heavily inspired by go-mutesting, by @zimmski, and by extra mutations added to a fork by @avito-tech.

You can find more resources and tools on this subject by browsing through the mutation testing topic on GitHub. The awesome-mutation-testing repository also contains many good resources.

License

Ooze is open-source software released under the MIT License.


ooze icon

ooze's People

Contributors

bios-marcel avatar blizzy78 avatar dependabot[bot] avatar gtramontina avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar

ooze's Issues

Running mutation tests against Ooze itself fails

After introducing #5, running mutation tests against Ooze itself (and I think it is safe to assume that other projects too) fails with the following (or similar) error:

https://github.com/gtramontina/ooze/actions/runs/4843531326/jobs/8631215763#step:4:26

Error: 	panic: failed type checking for file 'internal/cmdtestrunner/cmdtestrunner.go': internal/cmdtestrunner/cmdtestrunner.go:7:2: could not import github.com/gtramontina/ooze/internal/ooze (can't find import: "github.com/gtramontina/ooze/internal/ooze")

goroutine 6 [running]:
testing.tRunner.func1.2({0x6ef2c0, 0xc000202d20})
	/opt/hostedtoolcache/go/1.18.10/x64/src/testing/testing.go:1389 +0x[24](https://github.com/gtramontina/ooze/actions/runs/4843531326/jobs/8631215763#step:4:25)e
testing.tRunner.func1()
	/opt/hostedtoolcache/go/1.18.10/x64/src/testing/testing.go:1392 +0x39f
panic({0x6ef2c0, 0xc000202d20})
	/opt/hostedtoolcache/go/1.18.10/x64/src/runtime/panic.go:838 +0x207
github.com/gtramontina/ooze/internal/gosourcefile.(*GoSourceFile).Incubate(0xc000124bd0, {0x7ab3e0?, 0xc000010078})
	/home/runner/work/ooze/ooze/internal/gosourcefile/gosourcefile.go:46 +0x453
github.com/gtramontina/ooze/internal/ooze.(*Ooze).Release(0xc0001dfe50, {0x95f6e0, 0xe, 0xc0000b7e30?})
	/home/runner/work/ooze/ooze/internal/ooze/ooze.go:83 +0x2[25](https://github.com/gtramontina/ooze/actions/runs/4843531326/jobs/8631215763#step:4:26)
github.com/gtramontina/ooze.Release(0xc000097860, {0xc0000b7f[30](https://github.com/gtramontina/ooze/actions/runs/4843531326/jobs/8631215763#step:4:31), 0x6, 0x3a?})
	/home/runner/work/ooze/ooze/release.go:138 +0xa3a
github.com/gtramontina/ooze_test.TestMutation(0x0?)
	/home/runner/work/ooze/ooze/ooze_mutation_test.go:12 +0xeb
testing.tRunner(0xc000097860, 0x75a558)
	/opt/hostedtoolcache/go/1.18.10/x64/src/testing/testing.go:1439 +0x102
created by testing.(*T).Run
	/opt/hostedtoolcache/go/1.18.10/x64/src/testing/testing.go:1486 +0x[35](https://github.com/gtramontina/ooze/actions/runs/4843531326/jobs/8631215763#step:4:36)f

The diffs don't provide much value

The diffs currently don't provide a lot of value, since a single line changed is displayed as a many-line diff. I am not sure if this is an issue on my machine or simply how the feature was implemented.

WindowsTerminal_lQU2TzFYid

Panic on windows

Calls to os.Symlink can error on windows, due to lack of "certain permissions". Can we just replace this with hardlinking? If so, only for Windows, or for all systems?

Invalid application of virus `arithmetic.New()`

Output shows, that the virus arithmetic.New() has been applied to a string concatenation, which causes a build failure.

internal/service/routing.go:168:9: invalid operation: operator - not defined on "/" (constant of type string)

This was the code:

func HealthPath() string {
	return "/" + path.Join(BasePathInternalCases(), "health")
}

I am assuming this is not intended behaviour.

Running working tests with mutations causes errors and no report being generated

I am not sure if this is a problem on our end but when running the tests with mutations after some time (usually around 10 mins) we get these types of errors:

goroutine 651 [chan send, 4 minutes]:
github.com/gtramontina/ooze/internal/future.(*DeferredFuture[...]).Resolve.func1.1()
        /Users/vardex/go/pkg/mod/github.com/gtramontina/[email protected]/internal/future/deferred.go:28 +0x2c
created by github.com/gtramontina/ooze/internal/future.(*DeferredFuture[...]).Resolve.func1
        /Users/vardex/go/pkg/mod/github.com/gtramontina/[email protected]/internal/future/deferred.go:27 +0xb8

...

goroutine 1157 [chan send]:
github.com/gtramontina/ooze/internal/future.(*DeferredFuture[...]).Resolve.func1.1()
        /Users/vardex/go/pkg/mod/github.com/gtramontina/[email protected]/internal/future/deferred.go:28 +0x2c
created by github.com/gtramontina/ooze/internal/future.(*DeferredFuture[...]).Resolve.func1
        /Users/vardex/go/pkg/mod/github.com/gtramontina/[email protected]/internal/future/deferred.go:27 +0xb8

The mutations test eventually fails

exit status 2
FAIL <repository/package>     600.244s

Our tests (without mutations) run perfectly fine

Go version

go version go1.20.3 darwin/arm64

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.