How to write parameterized tests in Elm

Hi, how would you write a following test suite in Elm? I can’t find a way to write a bunch of test cases without wrapping each of them in test function.

test('isOdd returns true for odd numbers', function (assert) {
  assert.isTrue(isOdd(-1));
  assert.isTrue(isOdd(1));
  assert.isTrue(isOdd(3));
});

in Elm, I managed to write this, which is not really nice to read

describe "isOdd"
    (let
        testCase fn input expected =
            test (String.concat [ String.fromInt input , " = " , if expected then "True" else "False" ]) <|
                \_ ->
                    Expect.equal (fn input) expected
     in
     [ testCase Utils.isOdd -1 True
     , testCase Utils.isOdd 1 True
     , testCase Utils.isOdd 3 True
     ]
    )

Is there a better way to write this type of tests?

Can try Expect.all

suite : Test
suite =
    describe "isOdd"
        [ test "odd numbers" <|
            \_ ->
                Expect.all
                    [ Expect.equal (isOdd -1)
                    , Expect.equal (isOdd 1)
                    , Expect.equal (isOdd 3)
                    ]
                    True
        , test "even numbers" <|
            \_ ->
                Expect.all
                    [ Expect.equal (isOdd -2)
                    , Expect.equal (isOdd 2)
                    , Expect.equal (isOdd 6)
                    ]
                    False
        ]
3 Likes

thanks for the solution.

Is there also a way to write it so that the expected could differ for different inputs?

test("sayHi", function(assert) {
  assert.equal(sayHi("Thomas"), "Hello Thomas!");
  assert.equal(sayHi("George"), "Hello George");
  assert.equal(sayHi(""), "Hello");
  ...
})

There’s a common testing principle that many people follow, which is that a test should only have a single reason to fail. That way, you don’t end up with tests failing and keeping you from getting information about other assertions in the test case. And you have a clear focus for each test. elm-test takes that stance. That’s why Expect.all applies to a single test subject. And perhaps it helps explain the design if I point out that the subject argument is really intended to be a single test subject that you make multiple assertions about, so passing in True or False as the last argument to Expect.all is probably going to end up feeling awkward I would say.

Also, I would recommend trying out Expect.true and Expect.false instead of Expect.equal. You can create a nice helper function for isOdd or whatever helpers would make sense.

isOdd n =
  (n |> modBy 2 == 0)
  |> Expect.false "Expected an odd number."

Hope that helps!

I agree that it’s good practice to have a single reason to fail. What I wrote was for simplification.

Usually the testing fw has some way to write down test cases and it will then generate those tests for you. In C# you would usually annotate the method and the build system would generate tests with one assertion each.

[TestCase(-1, True)]
[TestCase(0, False)]
[TestCase(1, True)]
public void IsOdd_Tests(int input, bool expected) {
  Assert.Equal(Utils.isOdd(input), expected);
}

I’m looking for something similar in Elm so I don’t have to write a bunch of boilerplate code.

If you want different Expectations, then you’ll need to have different tests.

suite : Test
suite =
    describe "isOdd" <|
        List.map
            (\{ given, expected } ->
                test (String.fromInt given) <|
                    \_ ->
                        Expect.equal expected (isOdd given)
            )
            [ { given = 1, expected = True }
            , { given = 2, expected = False }
            , { given = 3, expected = True }
            , { given = 6, expected = False }
            ]

This pattern is similar to “table driven tests” that Go community prefers, but because of the way List.map argument order works, your test code will come before your test data “table”; no biggie imo

Also, feel free to use Debug.toString given instead of String.fromInt given if your given is a more complicated type. Since this is just test, using Debug to spell out the test name is probably forgivable.

3 Likes

We got rid of those on master because we found that the explanatory strings people were writing weren’t helpful, like false was not true. You’re using it as intended, to add extra context, but that didn’t happen in practice.

@ondrej If you have a lot of similar tests like this, have you thought about a fuzz test?

describe "isOdd"
 [ fuzz Fuzz.int "detects odd numbers"  <| \k -> 2*k + 1 |> isOdd |> Expect.equal True
 , fuzz Fuzz.int "detects even numbers" <| \k -> 2*k |> isOdd |> Expect.equal False
 ]
3 Likes

Fuzz! :+1: I really gotta remember to use more Fuzz

I like that. Using Debug.toString gives me an ability to write pretty general testCase wrapper. Thanks for the tip.

testCases testFn cases =
    let
        testCase c =
            test (Debug.toString c) <|
                \() ->
                    Expect.equal (testFn c.given) c.expected
    in
    List.map testCase cases

and then use as

describe "isOdd" <|
     testCases Utils.isOdd
        [ { given = -2, expected = False }
        , { given = -1, expected = True }
        ...
        ]
2 Likes

I was thinking about it, but I don’t have much experience with that. It seems pretty neat, I will need to explore it a bit.

The beauty of functional programming is that you can parameterize just about anything, so long as your syntax is valid!

For example:

length : List String -> List Int
length vals =
    case vals of 
        [] -> []
        v :: vs -> String.length v :: length vs

Lets parameterize that over the String.length function:

length : (String -> Int) -> List String -> List Int
length fn vals =
    case vals of 
        [] -> []
        v :: vs -> fn v :: length vs

But then we realize there is nothing making specific use of String or Int in this, so we arrive at:

map : (a -> b) -> List a -> List b
map fn vals =
    case vals of 
        [] -> []
        v :: vs -> fn v :: length vs

Take any function, extract a syntactically valid expression from it and make it a parameter, get creative with that idea, and in a nutshell you have what is so wonderful about functional programming.

So here is what I did today to parameterize some tests that I am writing:

I created a record that captures the API of Dict, and put it in a module that has a whole bunch of tests around the expected behaviour of Dict.

[https://github.com/elm-scotland/elm-tries/blob/master/tests/DictIface.elm]

type alias IDict comparable v dict b dictb result =
    { empty : dict
    , singleton : comparable -> v -> dict
    , insert : comparable -> v -> dict -> dict
    , update : comparable -> (Maybe v -> Maybe v) -> dict -> dict
    , remove : comparable -> dict -> dict
    , isEmpty : dict -> Bool
    , member : comparable -> dict -> Bool
    , get : comparable -> dict -> Maybe v
    , size : dict -> Int
    , keys : dict -> List comparable
    , values : dict -> List v
    , toList : dict -> List ( comparable, v )
    , fromList : List ( comparable, v ) -> dict
    , map : (comparable -> v -> b) -> dict -> dictb
    , foldl : (comparable -> v -> b -> b) -> b -> dict -> b
    , foldr : (comparable -> v -> b -> b) -> b -> dict -> b
    , filter : (comparable -> v -> Bool) -> dict -> dict
    , partition : (comparable -> v -> Bool) -> dict -> ( dict, dict )
    , union : dict -> dict -> dict
    , intersect : dict -> dict -> dict
    , diff : dict -> dictb -> dict
    , merge :
        (comparable -> v -> result -> result)
        -> (comparable -> v -> b -> result -> result)
        -> (comparable -> b -> result -> result)
        -> dict
        -> dictb
        -> result
        -> result
    }

Then multiple implementations of that ‘interface’ were written, one that wraps Dict and one that wraps the data structure I am working on, which is Trie. The Dict tests are just to confirm the expected behaviour against the Dict, and the Trie tests are against the Trie API which can be thought of as an extension of Dict. The same tests are run over both data structures using the same code, by some creative test parameterization.

[https://github.com/elm-scotland/elm-tries/blob/master/tests/DictTest.elm]
[https://github.com/elm-scotland/elm-tries/blob/master/tests/TrieTest.elm]

This is similar to what choonkeat and ondrej suggested, but I like to do it this way (without the List.map) to make the code a bit less abstract:

    describe "isOdd returns true for odd numbers" <|
        let
            checkIsOdd i =
                test (Debug.toString i) <|
                    \() ->
                        isOdd i
                            |> Expect.equal True
        in
        [ checkIsOdd -1
        , checkIsOdd 1
        , checkIsOdd 3
        ]

Another alternative if I really want it to feel like a single test is:

    test "isOdd returns true for odd numbers" <|
        \() ->
            [ -1, 1, 3 ]
                |> List.map isOdd
                |> Expect.equal [ True, True, True ]

edit: one other option that came to mind later:

    test "isOdd returns true for odd numbers" <|
        \() ->
            [ -1, 1, 3 ]
                |> List.map isOdd
                |> List.map (Expect.equal True)
                |> Expect.all
4 Likes

This topic was automatically closed 10 days after the last reply. New replies are no longer allowed.