Hello Elm,
I wrote something for you. Or actually, for myself, over the last 6+ months. I thought I’d share it, because I think you’ll find it useful.
http://package.elm-lang.org/packages/drathier/elm-test-tables/1.0.0
Fuzzer comparable
I’ve felt for quite a while now that I’d like to make my tests read like what they’re trying to test, rather than how they’re testing it. For example, using fuzz (list int)
when you really want a fuzz (list comparable)
or a fuzz (list a)
. Elm-test doesn’t expose a comparble
fuzzer, and I’m fairly convinced that it shouldn’t, but I still want it for my tests.
fuzz (list int) "reversing a list twice results in the original list" <|
\lst -> lst |> List.reverse |> List.reverse |> Expect.equal lst
changes into:
fuzz (list a) "reversing a list twice results in the original list" <|
\lst -> lst |> List.reverse |> List.reverse |> Expect.equal lst
Flaky Fuzzers and Edge-cases
Another problem I’ve faced too many times is that fuzzers aren’t perfect; they just don’t always find all edge-cases, and as a result, there might be regressions, because we rely on fuzz tests to find all edge-cases for us. To combat this, I wrote a function with the same api, that takes a list of edge cases, runs those first, and then the normal fuzzer.
fuzz int "input is not equal to 11147" <|
\a -> a |> Expect.notEqual 11147
turns into
fuzzTable int "input is not equal to 11147" [ 11147 ] <|
\a -> a |> Expect.notEqual 11147
JSON Fuzzers
We all use json, and it’s fairly easy to get the encoders/decoders right, but every once in a while something sneaks in. Thus, I started writing tests for every single json encoder/decoder I had, and this is a wonderful example of where fuzz tests can shine, if only there was a small helper function:
roundtrip "int encode/decode" Fuzz.int Json.Encode.int Json.Decode.int
With this function, we’ll generate random values, encode them, decode them, and check that there was no data loss along the way.
Proper tests for T.map and T.andThen
Many data structures supply map
and andThen
(a.k.a. concatMap
), and we’ve all learned what they look like and how they should work. They’re actually Functors and Monads, and Functors and Monads have laws, that we expect map
and andThen
to obey. They’re just a bit tricky to express as tests in Elm, so I wrote some helper functions for them. The v1
, v2
, v3
are to make sure your map
/andThen
functions are not limited to a single type, and I had to split them into three functions to make it type check.
describe "List.map"
[ mapv1 List.map Fuzz.list
, mapv2 List.map Fuzz.list
, mapv3 List.map Fuzz.list
]
describe "List.andThen a.k.a. List.concatMap"
[ andThenv1 List.singleton List.concatMap Fuzz.list
, andThenv2 List.singleton List.concatMap Fuzz.list
, andThenv3 List.singleton List.concatMap Fuzz.list
]
Expect + Category theory
Wait, come back! It’s actually useful!
fuzz3 int int int "+" <|
\a b c -> { f = (+), a = a, b = b, c = c }
|> Expect.all [ identityElement 0, associative, commutative ]
This tests that the function +
:
- Has an identity element 0, which means that
a + 0 = 0 + a = a
- Is associative, which means that
(a+b)+c = a+(b+c)
- Is commutative, which means that
a+b = b+a
Unique Fuzzers
Sometimes you just don’t want your inputs to equal each other. Good thing we have these fuzzers then.
fuzz (list Fuzz.Unique.int) "list of unique int" <|
\v -> v |> Set.fromList |> Set.size |> Expect.equal (List.length v)
or even
fuzz (list Fuzz.Opaque.Unique.comparable) "list of unique int" <|
\v -> v |> Set.fromList |> Set.size |> Expect.equal (List.length v)
But wait, there’s more
I’ve just outlined some of the things in this package. Go and have a look yourself, and let me know what you think!
http://package.elm-lang.org/packages/drathier/elm-test-tables/1.0.0
Hope you find it useful!
Best,
Filip