GithubHelp home page GithubHelp logo

janiczek / elm-minithesis Goto Github PK

View Code? Open in Web Editor NEW
16.0 5.0 1.0 690 KB

An Elm port of Minithesis

License: BSD 3-Clause "New" or "Revised" License

Elm 100.00%
hypothesis minithesis property-based-testing elm

elm-minithesis's Introduction

IMPORTANT:

See also ✨ elm-microthesis ✨ (and its various branches/tags) which is more Elm-idiomatic implementation of the same ideas. The repo you are currently viewing is a more direct port of the Python Minithesis idea. Read below.

elm-minithesis

elm-minithesis is a property-based testing library based on Minithesis, which is the minimal implementation of the core idea of Hypothesis.

Read more in the About section.

import Minithesis.Fuzz as Fuzz exposing (Fuzzer)
import Minithesis exposing (Test, TestResult)

ints : Fuzzer (List Int)
ints =
    Fuzz.list (Fuzz.int 0 10000)


findsSmallList : Test (List Int)
findsSmallList =
    Minithesis.test "list always sums under 1000 lol" ints <|
        \fuzzedList ->
            List.sum fuzzedList <= 1000


{-| Will fail and shrink to the minimal example:

`( "list always sums under 1000 lol", FailsWith [ 1001 ] )`

-}
result : Int -> TestResult (List Int)
result seed =
    Minithesis.run seed findsSmallList


{-| Running these tests inside elm-test can be done via functions inside
the Test.Minithesis module
-}
test : Test.Test
test =
    Test.Minithesis.mFuzz findsSmallList

Tips and tricks

Examples

Try Fuzz.example and Fuzz.exampleWithSeed in the REPL for quick sanity checks of your fuzzers!

import Minithesis.Fuzz as F

F.string |> F.exampleWithSeed 0
--> gives 10 examples
["x","I","","6a=U",";W?","uDc",":ei_^~","=Y","-NAT\\QJ","{92H2DI}-(KOc"]

F.string |> F.exampleWithSeed 1
--> different seed -> different examples
["KI","<j XT","","'xpvdQ1ONkM/","tdVd_v","I3=:e0i3","","P)y8$e@^y}1s",",]uz\\","8"]

Inspect shrink history

Use the showShrinkHistory field of Minithesis.runWith to get additional information about how shrinking of your data went. All FailsWith results become FailsWithShrinks containing additional info. This gives a bit of visibility into what happens in the black box that Minithesis shrinking is.

import Minithesis as M
import Minithesis.Fuzz as F

M.runWith 
  { maxExamples = 100
  , showShrinkHistory = True 
  } 
  1
  (M.test "list always sums under 1000 lol"
    (F.list (F.int 0 10000))
    (\list -> List.sum list <= 1000)
  )
--> 
( "list always sums under 1000 lol"
, FailsWithShrinks 
    { finalRun = [1,1001,0]
    , finalValue = [1001]
    , history = 
        [ value = [166,5536,4725,8499,7844,1727], { run = [1,166,1,5536,1,4725,1,8499,1,7844,1,1727,0], shrinkerUsed = "Initial"                                                           }
        , value = [166,5536],                     { run = [1,166,1,5536,0],                             shrinkerUsed = "DeleteChunkAndMaybeDecrementPrevious { size = 8, startIndex = 5 }" }
        , value = [5536],                         { run = [1,5536,0],                                   shrinkerUsed = "DeleteChunkAndMaybeDecrementPrevious { size = 2, startIndex = 1 }" }
        , value = [1001],                         { run = [1,1001,0],                                   shrinkerUsed = "MinimizeChoiceWithBinarySearch { index = 1 }"                      }
        ] 
    }
)

Paired with some knowledge about which shrinking strategies there are and what they do, you can sometimes tweak your fuzzers to optimize how they interact with the shrinking process, allowing them to be shrunk better.

(Related: Hypothesis docs: "Strategies that shrink")

About

elm-minithesis is a property-based testing library based on Minithesis, which is the minimal implementation of the core idea of Hypothesis.

Hypothesis itself is a Python testing library for property-based testing. What sets it apart is its underlying implementation: instead of working on the generated values themselves (defining shrinkers on these values, eg. saying that a Bool will shrink from True to [False] and from False to []), it remembers the underlying random "dice rolls" that were used to generate the values, and it shrinks those.

-- "type-based shrinking"
-- (QuickCheck and most other property-based testing libraries, including elm-test)
shrink : a -> LazyList a -- has to be defined for each fuzzed type,
                         -- doesn't shrink by default

-- "integrated shrinking"
-- (Hypothesis, jqwik, elm-minithesis :) )
shrink : List Int -> LazyList (List Int) -- shrinks all fuzzers automatically,
                                         -- can't be configured

A very cool consequence of the above is is that it mostly sidesteps the issue most other property-based testing libraries have: andThen (monadic bind). More specifically, QuickCheck-like libraries either don't expose andThen at all (as is the case with Elm), or struggle with making the shrunk values satisfy the same invariants the andThen-generated values do. Hypothesis instead shrinks the underlying "dice roll" history and generates a new value from that, so its values satisfy the invariants out of the box even if using andThen!

(Source: Hypothesis blog: "Integrated vs type based shrinking")

Community

There is a small piece of internet dedicated to elm-minithesis: the #elm-minithesis channel on Incremental Elm Discord. Come join and hear about updates first!

The original discussion around elm-minithesis happened on the Elm Discourse.

elm-minithesis's People

Contributors

dependabot[bot] avatar janiczek avatar

Stargazers

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

Watchers

 avatar  avatar  avatar  avatar  avatar

Forkers

lemol

elm-minithesis's Issues

Status?

Hey, this is just to have your word on the status of elm-minithesis/microthesis. Do you think it's on temporary / indefinite halt? Everything is fine to me so no pressure, it's just to know.

opinion: duplication/confusion in list fuzzers

Why is there both listOfLength and listWith? Seems fine to have one or the other, but not both. Like I mentioned in #10, I also think customAverageLength could be dropped.

I also don't understand what uniqueByList does from the docs. Why isn't calling list with a filtered fuzzer sufficient?

opinion: char/string fuzzers could be improved

Right now there is:

char : Fuzzer Char
charRange : Int -> Int -> Fuzzer Char
anyChar : Fuzzer Char

string : Fuzzer String
stringOfLength : Int -> Fuzzer String
stringWith : { minLength : Maybe Int, maxLength : Maybe Int, customAverageLength : Maybe Float, charFuzzer : Fuzzer Char } -> Fuzzer String

But I've never actually found the string fuzzers very useful! They're very so far out of my domain that it's not worth the combinatorial overhead to generate them. So I wonder about this API instead, which would make char match how int works now:

char : Char -> Char -> Fuzzer Char -- not Ints! It's easy to accept literal values and it makes tests easier to read
humanReadableChar : Fuzzer Char -- existing plus accented characters and maybe a few unicode/CJK characters

as for string itself… maybe we could drop it! I have always gotten better test results by generating some data structure I care about and then serializing it, or picking from a small set of known data. If we want realistic edge case strings for fuzzing, we could consider generating Unicode data. (e.g. Unicode publishes test tables)

But I know that dropping string is probably pretty controversial! So if you don't like that, how about:

string : Fuzzer Char -> Fuzzer String
stringOfLength : Fuzzer Int -> Fuzzer Char -> Fuzzer String

then

string humanReadableChar

stringOfLength (int 1 20) (char 'a' 'Z')

wanting strings to average around a given length feels sufficiently unusual that I would think something like this could solve it better:

fromGenerator : Random.Generator a -> Fuzzer a

-- usage

stringOfLength (fromGenerator (Random.Float.normal 100 10)) humanReadableChar

opinion: number fuzzers could be simplified

Right now there are:

int : Int -> Int -> Fuzzer Int
anyNumericInt : Fuzzer Int
anyInt : Fuzzer Int
positiveInt : Fuzzer Int
negativeInt : Fuzzer Int
nonpositiveInt : Fuzzer Int
nonnegativeIn : Fuzzer Int

float : Float -> Float -> Fuzzer Float
anyNumericFloat : Fuzzer Float
anyFloat : Fuzzer Float
floatWith : { min : Maybe Float, max : Maybe Float, allowNaN : Bool, allowInfinities : Bool } -> Fuzzer Float
probability : Fuzzer Float

Some of this make me go "hmmm"! For example, why are there two different styles between ints and floats? Without looking it up, why is anyNumericInt necessary (aren't all ints numeric?)

But there's also some of this that I'm like "yes! exactly what we need!" In particular, the change from intRange to int is 💯—so much wasted fuzz runs on out-of-domain values!

So here's my suggestion:

int : Int -> Int -> Fuzzer Int
intWithNaN : Int -> Int -> Fuzzer Int

float : Float -> Float -> Fuzzer Float
floatWithNaN : Float -> Float -> Fuzzer Float

probability : Fuzzer Float

because:

  • anyNumeric*, any* can be expressed with the *With variants
  • positiveInt, negativeInt, etc can be trivially defined (if someone really wants to go up to 2,147,483,647 I'm not gonna stop 'em but that's something they should decide is a valid value)
  • infinities are easily defined as well (even though it's weird to do 1/0 or -1/0)

If you think there need to be easier ways to say "max value" or "infinity" then you could also ship with

intInfinity : Int
biggestInt : Int
smallestInt : Int

floatInfinity : Int
biggestFloat : Float
smallestFloat : Float

Usage would then look like:

float -floatInfinity 0

intWithNan 0 intInfinity

pretty clear what's going on IMO.

question: how does oneOf shrink?

just what it says on the tin. How does oneOf shrink with this shrinking strategy? In elm-exploration/test fuzzing, it seems like each item is shrunk independently. Is that different here (e.g. shrinking towards the first item in the list?)

tiny opinion: tuple names

totally just a personal thing, but "tuple" and "tuple3" made me raise an eyebrow—it implies "tuple2", which wouldn't make any sense.

Suggestions:

  • the 2-item constructor in core is called pair, so maybe it should match that!
  • how about "triple" or "triplet" for the 3-item version?

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.