Giter VIP home page Giter VIP logo

propcheck's Introduction

propCheck - Property based testing in kotlin

CircleCI License Download

Never write test data by hand again. Generate it!

Table of contents

Documentation

Usage


The latest version 0.10.x includes major changes and is experimental. I will write a migration guide at soon when I consider it stable, but up untill then I'd suggest sticking to 0.9.6.


Add the following to your build.gradle:

repositories {
    maven { url 'https://dl.bintray.com/jannis/propCheck-kt' }
}

dependencies {
    testImplementation 'propCheck:propCheck-kt:0.9.6'
}

Example usage:

propCheck {
    forAll { (a, b): Pair<Int, Int> ->
        a + b == b + a
    }
}
// prints =>
+++ OK, passed 100 tests.

In this example propCheck ran 100 tests with random pairs of integers and checks if addition over integers is commutative.

propCheck can also take an argument of type Args to change the way tests are run:

propCheck(Args(maxSuccess = 300)) {
    forAll { (a, b): Pair<Int, Int> ->
        a + b == b + a
    }
}
// prints =>
+++ OK, passed 300 tests.

A full overview of what can be customised can be seen here

Shrinking

A powerful concept in property based testing is shrinking. Given a failed test a shrinking function can output several "smaller" examples. For example: Lists tend to shrink to smaller lists, numbers shrink towards zero and so on. This often ends you with a minimal counterexample for the failed test and is very useful if the data would otherwise be messy (as will likely happen with random data).

For most cases enabling shrinking is as easy as changing forAll to forAllShrink:

propCheck { 
    forAllShrink { i: Int ->
        i < 20
    }
}
// prints =>
*** Failed! (after 35 tests and 3 shrinks):
Falsifiable
20

A note regarding test data

The quality of a property-based test is directly related to the quality of the data fed to it. There are some helpers to test and assure that the generated data holds some invariants.

To inspect results use either label, collect, classify or tabulate.

Here is an example on how to use classify:

propCheck {
    forAll(OrderedList.arbitrary(Int.order(), Int.arbitrary())) { (l): OrderedList<Int> ->
        classify(
            l.size > 1,
            "non-trivial",
            l.shuffled().sorted() == l
        )
    }
}
// prints something like this =>
+++ OK, passed 100 tests (92,00% non-trivial).

To fail a test with insufficient coverage use checkCoverage with functions like cover or coverTable.

propCheck {
    forAll(OrderedList.arbitrary(Int.order(), Int.arbitrary())) { (l): OrderedList<Int> ->
        checkCoverage(
            cover(
                95.0,
                l.size > 1,
                "non-trivial",
                l.shuffled().sorted() == l
            )
        )
    }
}
// prints something like this =>
*** Failed! (after 800 tests):
Insufficient coverage
89,99% non-trivial

Here a coverage of 95% non-trivial lists is required, but only 89.99% could be reached.

Running these tests with a test runner

propCheck is by itself stand-alone and does not provide test-runner capabilites like kotlintest. It however can and should be used together with a test-runner. By default propCheck will throw exceptions on failure and thus will cause the test case it is being run in to fail. (That can be diasbled by using methods like propCheckWithResult instead)

The following example runs a test in a kotlintest test:

class TestSpec : StringSpec({
    "test if positive is always > 0!" {
        propCheck {
            forAll { (i): Positive<Int> ->
                i > 0
            }
        }
    }
    "other tests" { ... }
})

There are plans of adding better support for test runners so that you can drop the propCheck {} wrapper.

Use of arrow in and with propCheck

First of all: If you are using arrow-kt: Great! There are plenty of instances already defined for arrows data-types.

If not then don't worry. Most of the api can be used without ever touching upon arrows datatypes and there are overloads specifically to avoid those cases if they do come up.

That is excluding types like Option or TupleN, which should be fine.

Kotlintest generators vs propCheck

Another library that offers property-based testing is kotlintest, however it has several drawbacks:

  • Shrinking is much worse.
    • Shrinking instances are rather primitive (especially lists). propCheck uses the same methods quickCheck uses, and quickcheck is built upon years and years of experience and research.
      • This causes kotlintests shrunk results to be worse for most non-trivial shrinking operations (and even in the trivial cases)
    • Generators are tied to shrinkers but most useful operations on generators silently drop the shrinker. This is quite problematic for several reasons, it makes using shrinking much harder as you have to avoid certain interface methods to keep it. Also it makes reasoning about code harder because it's silent! Gen.map(id) should not change the datatype, in kotlintest it will, because it forgets the shrinker
    • Operates over over lists rather than lazy sequences, this will become a problem with shrinking larger examples
  • It is just less powerful. propCheck offers generating functions (properly), coverage checks on your generated data, statemachine testing for complex stateful systems (with support for catching race-conditions), and more smaller things.
  • Not very flexible. propCheck offers many more combinators for both properties, generators and shrinkers
  • While kotlintest does allow reproducable tests (by seeding a specific generator), propCheck takes this quite a step further, not associating a generator with a random seed, but having the whole property test being deterministic after seeding it. Retaining good random values is achieved by using a splittable random generator.
  • Kotlintest also has no notion of growing it's input over time. propCheck starts random generation with smaller values and increases to fail faster. This also allows precise control over the size of generated structures (needed for anything recursive, like lists and trees etc)

However propCheck also has some drawbacks:

  • Exception handling has to go through IO/suspend or needs to be handled manually. Not a huge problem, but for exception heavy code can be a burden
  • Kotlintests api is easier to use in trivial cases (but that is also what limits it)

Outside of property based testing kotlintest still makes a fine testing library, and I recommend using it to execute the tests and for anything that is not a property test!

Feedback

propCheck is still in its early days so if you notice bugs or think something can be improved please create issues or shoot me a pull request. All feedback is highly appreciated.

Credits

propCheck is a port of the awesome library quickcheck. If you ever come around to use haskell make sure to give it a go! Writing propCheck was also made much easier by using arrow-kt to be able to write code in a similar style to haskell and thus close to the original. As with quickCheck make sure to check out arrow for a more functional programming style in kotlin.

propcheck's People

Contributors

1jajen1 avatar alphaho 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

Watchers

 avatar  avatar  avatar  avatar

Forkers

alphaho

propcheck's Issues

Handle thrown exceptions

Right now exception handling in tests is left to the user writing the test and any exception thrown will not be caught. That is fine for most cases (when programming in a functional way and prefering total functions) but it may be a restriction that does not need to be there. Any ideas on how to approach this (if at all) are welcome :)

Setup benchmark env

Not necessarily against other libraries. The main focus here is to check regressions when dealing with performance related issues. Just setting up something that runs on the ci and reports regressions is enough. Actual benchmarks will follow eventually.

Document Function generation

Function generation has some quirks that need to be generated.
List of topics to cover:

  • Implementing Func and Coarbitrary
  • Using it in forAlls and the rules that function generation follows (determinism etc)
  • Gotchas in regards to shrinking (very weak support for shrinking atm, will be a bit better when #13 is done)

Rose monad is not stacksafe

Currently the rose monad is not stacksafe and thus breaks MonadLaw tests. This is rarely a problem in practice, but definitly a bug that should be fixed.

Check usage of kotlin random

I am sort of cheating by using kotlin.Random and generating tuples of A (value generated) and a Long (new seed). That should be fine for running tests, but there might be better ways. A review would be great :)

Improve shrinking long sequences

Shrinking always terminates if implemented correctly, but in the meantime due to missing lazyness very large lists will be generated and sometimes even retained in memory. The worst offender here is shrinkList, it would be much better if it operated on the Sequence data-type and only forced loading the entire thing if it's needed (when the list cannot be cut down to smaller sizes anymore, or that cutting had no effect). This is not an easy task because the implicit strictness of everything in kotlin makes for some fun situations (for example m(): Sequence is not lazy in generation, but sequenceOf(Unit).flatMap(m) is) It is quite tricky to find all of those situations, but it needs to be done to get shrinking back to a performant level on large sequences.

Ideas for api changes

Hedgehog has a much nicer api than quickcheck (although I don't think it's too bad), which also comes with tradeoffs:

One such tradeoff is shrinking is limited in monadic context to be an in order shrink, which if the failure is on the second param can lead to unshrunk first parameters being displayed. This is especially bad for shrinking functions because unshrunk functions aren't displayed.
Another tradeoff is, in kotlin an exact port would have higher-kinded return types etc, which is not really feasable, so some sort of compromise needs to be found (that is the reason I ported quickcheck and not hedgehog in the first place)

Things I would like:
First class effectful testing and generating. While easy in haskell this is an api burden in kotlin, needs some tests too see how that will look.
Merging shrinkers with generators in a lawful way. This will get back a lot of convenience (removing the need for Arbitrary altogether). If it can be done without sacrificing too much in terms of shrinking then it'll be amazing!
The AMAZING printout hedgehog gives! This should be possible!

Convenience for kotlin:
Exception handling needs first class support
Suspend function support

This is just a list of ideas so I don't forget, feel free to comment with more, but open seperate issues if you have something more concrete to do.

Regardless of what gets implemented: The current internal implementation is quite rigid, to make it easier to test and write these changes a serious rewrite with more polymorphic functions is needed. That could be possible without any api change at all. Since the library is somewhat feature complete now, that will be the next focus.

Improve documentation

  • More and better examples.
  • In general more descriptive text
  • Ank checked examples to get results

Document stateful testing

Statemachine testing is available in both sequential and parallel mode, but is not yet documented.
See the tests for it to understand how it works.

Topics to cover:

  • Implementing a state-machine test
  • When to use state-machine tests
  • Parallel tests and their uses (Catching race conditions,...)
  • Some gotchas regarding shrinking parallel tests (non-deterministic failure means shrinking is not deterministic either)

Shrinking functions does not terminate for all cases

fun main() {
    propCheck(Args(maxShrinks = 10000)) {
        forAll { (f): Fun<Int, Int> ->
            f(0) == f(1)
        }
    }
}

Sometimes shrinks down to [0 -> 1, _ -> 0] (or similar) very fast, but sometimes just seems to loop somewhere. This is kind of hard to debug, but needs a fix!

Integrated shrinking

Benefits:

  • A user would not have to write a shrinking function for most if not all data-types
  • Shrinking automatically follows invariants by construction. This reduces duplication in logic that checks/maintains those when manually shrinking
  • Allows to get rid of Arbitrary which is in itself not a big problem, but jut a hassle to implement, carry around and lookup.
  • Since shrinking is coupled to generators and changes to gen also change shrinking, which is a good thing if for example range is changed you'd not want shrunk results to be outside of that range

This comes with a few drawbacks:

  • When two generators are combined monadically they are shrunk one after the other and if the first one fails to shrink it will not be shrunk again. This can be overcome with using Applicative combinators wherever possible. Yet another way to overcome this is to simply add back a shrinker. While certainly not great, it gives the same level of control as type based shrinking (Or at least I think it does)
  • Generating functions relies on Coarbitrary and Function and some unsafe methods. I do not want to loose generating functions so there must be a way. I'd be fine keeping the typeclasses for the two, also the unsafe methods, but this needs to be done in a way that still allows integrated shrinking to work

My take on this atm is: Convenience over the best possible shrinking (which can still be recovered by extra work if needed). I'll start playing around with integrated shrinking once the internal rewrite is done.

This is a very good resource on the differences between the two and their drawbacks: http://www.well-typed.com/blog/2019/05/integrated-shrinking/#fnref10

Gradle dependency not available?

First the smaller issue - a typo in the readme:

testImplementation: 'propCheck:propCheck-kt:0.9.3'

the : should be removed.

Adding the maven repository and the gradle dependency (without :) I get the following error:

Execution failed for task ':compileTestKotlin'.
> Could not resolve all files for configuration ':testCompileClasspath'.
   > Could not find propCheck:propCheck-kt:0.9.3.
     Searched in the following locations:
       - https://jcenter.bintray.com/propCheck/propCheck-kt/0.9.3/propCheck-kt-0.9.3.pom
       - https://jcenter.bintray.com/propCheck/propCheck-kt/0.9.3/propCheck-kt-0.9.3.jar
       - https://repo.maven.apache.org/maven2/propCheck/propCheck-kt/0.9.3/propCheck-kt-0.9.3.pom
       - https://repo.maven.apache.org/maven2/propCheck/propCheck-kt/0.9.3/propCheck-kt-0.9.3.jar

Diff eqv result

Currently the counterexample added by eqv just prints both values one after the other. A proper diff can make it a lot clearer what is happening.

I have a working version that uses kotlin-pretty for nice output, a parser for generic toString output (works best with data classes) and a decent diffing algorithm. The result is quite nice and I plan on releasing it together with the internals rewrite soon.

This can be taken a lot further with showing errors and values at correct positions in code etc.

Test runner and mpp

Several things:

  • A custom test runner with no dependencies other than kotlin mpp libraries (seperate module)
  • Modules for interop with existing testing libraries
  • As a step towards mpp: Drop all jvm dependencies

Custom test runner:

Just a basic dsl to write property based tests that then will be detected and executed, reporting failure. This should be mpp if possible.

Interop with testing libraries:

A custom test runner is all great and fine, but interop with other libraries is a good thing as well. As this means simply offering wrappers and dsl's to build tests with propCheck this should be a easy task. Just a module with the correct dependencies and wrappers around their methods/classes.

Mpp:

There are only two jvm dependencies atm:

  • arrow
  • commons-math

arrow is going to get mpp support soon, no idea about IO tho, so it's best to wait on that for now.
commons-math is used in one place only and that is for statistics analysis. Either replace it with a pure kotlin lib or implement that method here.

This is kept as one issue atm as a reminder. I'll split this soon when I have though about this some more.
The interop libraries would be amazing to have for 1.0, a test runner would not be bad as well but can be put off. Mpp is not a priority until arrow has it's IO or something equivalent mpp compatible.

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.