4.5

Easy to Test

In the Pure Functions section, we discovered that it’s much easier to check an Elm function for correctness than one written in a language that lacks purity. In this section, we will find out why that’s the case. We will also write quite a few tests to make sure that our implementation of various functions in the RippleCarryAdder module is correct. First, we need to learn how to write tests in Elm.

Getting Familiar with Elm Tests

Create a directory called tests in the project root directory (beginning-elm). It’s best practice to store tests in a separate directory. Inside the tests directory create a file named elm-package.json.

Add the following code to the tests/elm-package.json file.

{
    "version": "1.0.0",
    "summary": "A sample app for learning web application development with Elm.",
    "repository": "https://github.com/user/project.git",
    "license": "BSD3",
    "source-directories": [
        ".",
        "../elm-examples"
    ],
    "exposed-modules": [],
    "dependencies": {
        "elm-community/elm-test": "3.1.0 <= v < 4.0.0",
        "elm-lang/core": "5.0.0 <= v < 6.0.0",
        "elm-lang/html": "2.0.0 <= v < 3.0.0",
        "rtfeldman/html-test-runner": "2.0.0 <= v < 3.0.0"
    },
    "elm-version": "0.18.0 <= v < 0.19.0"
}

It’s also best practice to create a separate elm-package.json inside the test directory. This allows us to keep the dependencies for tests and application code separate. The elm-package.json file located at the root directory (beginning-elm) should only contain packages that are meant to be used in a production environment. The tests/elm-package.json contains packages such as elm-community/elm-test that are meant to be used only in a test environment.

The tests/elm-package.json file looks very similar to beginning-elm/elm-package.json. Here’s beginning-elm/elm-package.json again for comparison:

{
    "version": "1.0.0",
    "summary": "A sample app for learning web application development with Elm.",
    "repository": "https://github.com/user/project.git",
    "license": "BSD3",
    "source-directories": [
        ".",
        "elm-examples"
    ],
    "exposed-modules": [],
    "dependencies": {
        "elm-lang/core": "5.0.0 <= v < 6.0.0",
        "elm-lang/html": "2.0.0 <= v < 3.0.0",
        "elm-lang/http": "1.0.0 <= v < 2.0.0"
    },
    "elm-version": "0.18.0 <= v < 0.19.0"
}

The test version of elm-package.json differs from the production version in three ways:

  • It prepends ../ to elm-examples in the source-directories section because the tests directory is inside beginning-elm. That means to get to the elm-examples directory, which is also inside beginning-elm, it has to go up one directory. .. represents a parent directory. So ../elm-examples essentially means go up to the parent directory and look for the elm-examples directory.

  • It doesn’t include the elm-lang/http package as a dependency.

  • It adds two new packages:

elm-community/elm-test - This package contains modules for writing Elm tests and running them in the terminal.

rtfeldman/html-test-runner - This package contains modules for running the tests in a browser. In the beginning, we’ll run all our tests in a browser because the output is easier to look at. Later we will learn how to run them in the terminal as well.

The Elm version used in both beginning-elm/elm-package.json and tests/elm-package.json must be the same. Packages that are common in both files also need to have the same version. Otherwise, the behavior verified in tests might not match the one exhibited in production.

Next we will install the packages listed in tests/elm-package.json. Go to the tests directory in the terminal and run the following command.

It’s important that you run the following command from the tests directory and not beginning-elm. If you run it from the beginning-elm directory, elm-package will try to install the packages listed in beginning-elm/elm-package.json.

elm-package install

elm-package will ask for your permission. Answer y. In the Installing a Package section, we installed a specific package like this:

elm-package install elm-lang/http

If we don’t specify a package name, elm-package looks for an elm-package.json file in the directory its run from and installs all packages listed in the dependencies section of that file.

Although the production and test versions of elm-package.json have a few common packages in them, elm-package installs a completely new version of those packages when we run elm-package install from the tests directory.

Now that all the packages needed for writing and running tests are installed, we’re ready to write some tests. Create a new file called RippleCarryAdderTests.elm in the tests directory.

Add the following code to RippleCarryAdderTests.elm.

module RippleCarryAdderTests exposing (main)

import Test exposing (describe, test)
import Expect
import Test.Runner.Html exposing (run)
import RippleCarryAdder exposing (..)


main =
    run <|
        describe "Addition"
            [ test "1 + 1 = 2" <|
                \() ->
                    (1 + 1) |> Expect.equal 2
            ]

From the tests directory in the terminal, run elm-reactor.

It’s important that you run the elm-reactor command from tests and not beginning-elm. If elm-reactor is already running, terminate it by using the key combination Ctrl + c.

elm-reactor

elm-reactor 0.18.0
Listening on http://localhost:8000

Go to the page located at http://localhost:8000/RippleCarryAdderTests.elm. You’ll see the results produced by elm-reactor after our tests are run.

There is a lot going on in the RippleCarryAdderTests.elm file. Let’s break it down. We defined a module called RippleCarryAdderTests and imported a few modules:

  • import Test exposing (describe, test) imports the Test module which contains functions for creating and managing tests.

  • import Expect imports the Expect module which contains functions for describing what we expect to happen in a test.

  • import Test.Runner.Html exposing (run) imports the Test.Runner.Html module which contains functions for running tests in a browser.

  • import RippleCarryAdder exposing (..) imports the RippleCarryAdder module we created in the previous section. Later in this section, we will be testing the functions listed in this module.

Then we defined the main function that contains a test we want to run. The following diagram explains the test syntax.

Unit Type

The elm-test package requires us to wrap our test in an anonymous function.

That’s why we can’t just write our test like this:

test "1 + 1 = 2" <|
    (1 + 1) |> Expect.equal 2

The parameter passed to an anonymous function that wraps our test never gets used inside the test. So elm-test doesn’t even bother to pass a parameter that can be used in a meaningful way. It just passes an empty value represented by (), which means an empty tuple in Elm. It’s also known as the unit type. It’s often used to represent an empty value. When we enter () in the repl, its type is also displayed as ().

> ()
() : ()

We will cover types in detail in the Type System section, but for now think of it as something that represents a collection of values that have similar properties. For example, the type Int represents numbers with no fractional part: -1, 0, 1, 2, etc. And the type Bool represents logical values True and False.

Elm uses the () symbol to represent both value and type. There can only ever be one value of the unit type: (). That’s because the number of elements in a tuple determines its type. For example, the following two tuples have completely different types even though they both contain the same type of values.

> ( 1, 2 )
(1,2) : ( number, number1 )

> ( 1, 2, 3 )
(1,2,3) : ( number, number1, number2 )

For two tuples to be of the same type, they must contain the same number and type of values. In contrast, two lists containing the same type of values but different lengths have the exact same type.

> [ 1, 2 ]
[1,2] : List number

> [ 1, 2, 3 ]
[1,2,3] : List number

() vs _

It’s also common in Elm to use an underscore (_) when we want to ignore a parameter. Here’s a simple anonymous function that always returns 0:

> List.map (\_ -> 0) [ 1, 2, 3 ]
[0,0,0] : List number

> List.map (\_ -> 0) [ 'a', 'b', 'c' ]
[0,0,0] : List number

If you don’t remember how the List.map function works, go ahead and refresh your memory by reviewing the Mapping a List section. The (\_ -> 0) anonymous function doesn’t use its parameter to compute a return value. So instead of giving a name to the parameter, it just uses _.

On the surface, _ and () seem to be used for the same purpose: ignoring a parameter. But if we look deeper, there’s a subtle difference. Although (\_ -> 0) ignores its parameter, it allows us to pass any type of value to it. In the above example, we first passed it numbers and then characters. Will it still allow us to pass any type of value if we switched to () from _? Let’s find out.

> List.map (\() -> 0) [ 1, 2, 3 ]

------------------ TYPE MISMATCH ------------------------
The 2nd argument to function `map` is causing a mismatch.

3|   List.map (\() -> 0) [ 1, 2, 3 ]
                         ^^^^^^^^^^^
Function `map` is expecting the 2nd argument to be:

    List ()

But it is:

    List number

No it doesn’t. (\() -> 0) only accepts a value of type (), nothing else.

> List.map (\() -> 0) [ (), (), () ]
[0,0,0] : List number

_ means any value, but () means only unit type value. By replacing _ with (), we severely limited our input options. If you just want to ignore a parameter without limiting the types of input values that can be passed to your function, then use _. On the other hand, if no meaningful parameter will ever get passed to your function, as it was the case in the anonymous function that wrapped our test, then just use ().

Using the Forward Function Application Operator (|>)

The Expect.equal function expects to receive two arguments, the expected value first and the actual value second. In our test above, we used the |> operator to separate those two arguments. If you’re fuzzy on how |> works, go back and review the Forward Function Application section.

test "1 + 1 = 2" <|
    \() ->
        (1 + 1) |> Expect.equal 2

Syntactically speaking, we aren’t required to use |>. We could write the above test without |> and it’ll work just fine.

test "1 + 1 = 2" <|
    \() ->
        Expect.equal 2 (1 + 1)

However, if we use |>, it’ll be easier to tell which argument is which when a test fails. Change the expected value in the main function in RippleCarryAdderTests.elm to 1 like this:

test "1 + 1 = 2" <|
    \() ->
        (1 + 1) |> Expect.equal 1

If you refresh the page at http://localhost:8000/RippleCarryAdderTests.elm, you’ll see a failing test.

As you can see in the figure above, the actual result is listed before the expected result. The output closely resembles the test case we wrote:

(1 + 1) |> Expect.equal 1

Because of this resemblance, it’s easier to notice that our expected result is incorrect. If we had written our test like this:

Expect.equal 1 (1 + 1)

Our test code would not be in the same order as our test output, so it would be harder to visually compare them and find the source of the error. Change the expected result back to 2 to make sure that the test is passing.

Let’s write another test to see how the use of |> makes our tests much more readable. Replace the main function in RippleCarryAdderTests.elm with the following code.

main =
    run <|
        describe "Addition"
            [ test "1 + 1 = 2" <|
                \() ->
                    (1 + 1) |> Expect.equal 2
            , test "only 2 guardians have names with less than 6 characters" <|
                \() ->
                    let
                        guardians =
                            [ "Star-lord", "Groot", "Gamora", "Drax", "Rocket" ]
                    in
                        guardians
                            |> List.map String.length
                            |> List.filter (\x -> x < 6)
                            |> List.length
                            |> Expect.equal 2
            ]

If you refresh the page at http://localhost:8000/RippleCarryAdderTests.elm, you should see that all tests have passed.

The anonymous function passed to the test function as an argument is like any other function except it doesn’t have a name. We can use anything inside it that’s allowed in a normal function including a let expression.

The new test verifies that there are only two guardians whose names have less than six characters in them. We applied three different transformations to the original list to get to the final number. As you can see, the |> operator significantly improves the readability of our test. When a test contains a complex computation like this, the anonymous function tends to take the following shape:

\() ->
    someComputation
        |> producing
        |> actualValue
        |> Expect.equal expectedValue

If we didn’t use the |> operator, our test would be very hard to read because English reads left to right, but the function logic starts on the far right in this instance. Plus you would have to be careful to evaluate all the parentheses in the right order.

Expect.equal 2
    (List.length (List.filter (\x -> x < 6) (List.map String.length guardians)))

Even with the <| operator, the test would still be hard to read because English reads top to bottom, but the function logic starts on the bottom in this instance.

Expect.equal 2 <|
    List.length <|
        List.filter (\x -> x < 6) <|
            List.map String.length <|
                guardians

Using the Backward Function Application Operator (<|)

In main, we used the <| operator to pass our tests to the run function. We could have used the |> operator instead, but that would have made it difficult to notice the run function.

main =
    describe "Addition"
        [ test "1 + 1 = 2" <|
            \() ->
                (1 + 1) |> Expect.equal 2
        , test "only 2 guardians have names with less than 6 characters" <|
            \() ->
                let
                    guardians =
                        [ "Star-lord", "Groot", "Gamora", "Drax", "Rocket" ]
                in
                    guardians
                        |> List.map String.length
                        |> List.filter (\x -> x < 6)
                        |> List.length
                        |> Expect.equal 2
        ]
        |> run

Notice how the run function is dangling at the bottom. We also need to use <| after each test description, but we don’t need it on the describe line. That’s because describe doesn’t need to wrap its second argument (a list of tests) in parentheses. Here’s a simplified version of describe with no tests:

main =
    describe "Addition"
        []

The test function, on the other hand, needs parentheses around its second argument, which is an anonymous function. Without the parentheses, the Elm compiler can’t parse the code correctly. Here’s how the tests look with parentheses:

main =
    run <|
        describe "Addition"
            [ test "1 + 1 = 2"
                (\() ->
                    (1 + 1) |> Expect.equal 2
                )
            , test "only 2 guardians have names with less than 6 characters"
                (\() ->
                    let
                        guardians =
                            [ "Star-lord", "Groot", "Gamora", "Drax", "Rocket" ]
                    in
                        guardians
                            |> List.map String.length
                            |> List.filter (\x -> x < 6)
                            |> List.length
                            |> Expect.equal 2
                )
            ]

It’s a bit hard to read. We can make it easier to read by replacing parentheses with the <| operator, which passes the result from the expression on its right as the last argument to the function on its left.

Failed Tests

Earlier we saw an example of a failing test. Let’s write one more failing test to train our brain to read the output correctly. Replace the main function in RippleCarryAdderTests.elm with the following code.

main =
    run <|
        describe "Less than comparison"
            [ test "an empty list's length is less than 1" <|
                \() ->
                    List.length []
                        |> Expect.lessThan -1
            ]

If you refresh the page at http://localhost:8000/RippleCarryAdderTests.elm, you should see a test that has failed.

Once again, because we’ve used the forward function operator, the order of our output closely resembles the order of our test case.

List.length []
    |> Expect.lessThan -1

When a test fails, it could mean two things: either we made a mistake writing the test or the logic inside the function (or expression) under test is incorrect. If the logic is incorrect, we obviously have to fix it. If the test itself is incorrect, we should remove the test. In the example above, the expression List.length [] will never produce a value less than -1. Therefore, our test is incorrect.

A suite of well written tests play a critical role in refactoring the existing code. It gives us the confidence to rearrange our code so that we can make it easier to read and maintain. If we make a mistake, a test will catch it. For that reason, it’s important to keep our test suite clean by removing any incorrectly written failing tests.

Refactoring
“Refactoring is the process of changing a software system in such a way that it does not alter the external behavior of the code yet improves the internal structure.” - Martin Fowler

More Expectations

The Expect module provides quite a few other functions that allow us to express different expectations and not just equality. Replace the main function in RippleCarryAdderTests.elm with the following code.

main =
    run <|
        describe "Comparison"
            [ test "2 is not equal to 3" <|
                \() ->
                    2 |> Expect.notEqual 3
            , test "4 is less than 5" <|
                \() ->
                    4 |> Expect.lessThan 5
            , test "6 is less than or equal to 7" <|
                \() ->
                    6 |> Expect.atMost 7
            , test "9 is greater than 8" <|
                \() ->
                    9 |> Expect.greaterThan 8
            , test "11 is greater than or equal to 10" <|
                \() ->
                    11 |> Expect.atLeast 10
            , test "a list with zero elements is empty" <|
                \() ->
                    (List.isEmpty [])
                        |> Expect.true "expected the list to be empty"
            , test "a list with some elements is not empty" <|
                \() ->
                    (List.isEmpty [ "Jyn", "Cassian", "K-2SO" ])
                        |> Expect.false "expected the list not to be empty"
            ]

If you refresh the page at http://localhost:8000/RippleCarryAdderTests.elm, you should see that all tests have passed.

The tests above are self-explanatory except the last two. Expect.true and Expect.false require us to pass a string as the first argument. That’s because when a test fails, that string is used to explain why the test failed. Make the test that uses Expect.true fail by passing a non-empty list like this:

main =
    run <|
        describe "Comparison"
            .
            .
            , test "a list with zero elements is empty" <|
                \() ->
                    (List.isEmpty [ "The Ancient One" ])
                        |> Expect.true "expected the list to be empty"
            ]
            .
            .

If you refresh the page at http://localhost:8000/RippleCarryAdderTests.elm, you should see the explanation for why it failed.

The Expect module provides even more ways to express expectations. You can learn all about them here. Now that we’re familiar with how to write tests in Elm, let’s turn our attention to testing various functions in the RippleCarryAdder.elm file.

Testing the inverter Function

Let’s start with the inverter function which is the easiest one to test. To refresh our memory, here’s how the function looks:

inverter a =
    case a of
        0 ->
            1

        1 ->
            0

        _ ->
            -1

Add the following tests to the bottom of RippleCarryAdderTests.elm.

inverterTests =
    describe "Inverter"
        [ test "output is 0 when the input is 1" <|
            \() ->
                inverter 0
                    |> Expect.equal 1
        , test "output is 1 when the input is 0" <|
            \() ->
                inverter 1
                    |> Expect.equal 0
        ]

Although the inverterTests function looks different from other functions we’ve written before, there’s nothing special about it. It behaves exactly like how a normal function behaves. It also follows all the formatting rules that apply to a normal function.

Each test case in the inverterTests function represents a row from an inverter’s truth table we saw in the previous section.

Now replace the main function in RippleCarryAdderTests.elm with this:

main =
    run <|
        describe "4-bit Ripple Carry Adder Components"
            [ inverterTests ]

If you refresh the page at http://localhost:8000/RippleCarryAdderTests.elm, you should see that all tests have passed.

Up until now, we’ve been listing all our tests inside the main function, but we created a separate function for the inverter tests. Extracting related tests into a separate function makes it easier to organize our test suite.

Testing the andGate Function

Testing the andGate function is very similar to testing inverter. We just need to account for all four possible outcomes. Here’s how the andGate function looks:

andGate a b =
    Bitwise.and a b

Add the following tests to the bottom of RippleCarryAdderTests.elm.

andGateTests =
    describe "AND gate"
        [ test "output is 0 when both inputs are 0" <|
            \() ->
                andGate 0 0
                    |> Expect.equal 0
        , test "output is 0 when the first input is 0" <|
            \() ->
                andGate 0 1
                    |> Expect.equal 0
        , test "output is 0 when the second input is 0" <|
            \() ->
                andGate 1 0
                    |> Expect.equal 0
        , test "output is 1 when both inputs are 1" <|
            \() ->
                andGate 1 1
                    |> Expect.equal 1
        ]

Similarly to the inverterTests function, each test in the andGateTests function represents a row from an AND gate’s truth table.

Add andGateTests to the list of tests we want to run in the main function in RippleCarryAdderTests.elm.

main =
    run <|
        describe "4-bit Ripple Carry Adder Components"
            [ inverterTests
            , andGateTests
            ]

If you refresh the page at http://localhost:8000/RippleCarryAdderTests.elm, you should see that all tests have passed.

Exercise 4.5.1

Using the andGateTests function as a reference, write tests for the orGate function whose definition looks like this:

orGate a b =
    Bitwise.or a b

Here’s the truth table for an OR gate:

After you’ve written your tests and added them to the list of tests in the main function, don’t forget to refresh the page at http://localhost:8000/RippleCarryAdderTests.elm and make sure they all pass.

Testing the halfAdder Function

A half adder is slightly more complex because it produces two outputs: the sum and carry-out.

halfAdder a b =
    let
        d =
            orGate a b

        e =
            andGate a b
                |> inverter

        sumDigit =
            andGate d e

        carryOut =
            andGate a b
    in
        { carry = carryOut
        , sum = sumDigit
        }

Despite multiple outputs, the tests for the halfAdder function look very similar to the andGate tests. Add the following tests to the bottom of RippleCarryAdderTests.elm.

halfAdderTests =
    describe "Half adder"
        [ test "sum and carry-out are 0 when both inputs are 0" <|
            \() ->
                halfAdder 0 0
                    |> Expect.equal { carry = 0, sum = 0 }
        , test "sum is 1 and carry-out is 0 when the 1st input is 0 and the 2nd input is 1" <|
            \() ->
                halfAdder 0 1
                    |> Expect.equal { carry = 0, sum = 1 }
        , test "sum is 1 and carry-out is 0 when the 1st input is 1 and the 2nd input is 0" <|
            \() ->
                halfAdder 1 0
                    |> Expect.equal { carry = 0, sum = 1 }
        , test "sum is 0 and carry-out is 1 when both inputs are 1" <|
            \() ->
                halfAdder 1 1
                    |> Expect.equal { carry = 1, sum = 0 }
        ]

Each test case in the halfAdderTests function represents a row from the half adder’s truth table.

Add halfAdderTests to the list of tests we want to run in the main function in RippleCarryAdderTests.elm.

main =
    run <|
        describe "4-bit Ripple Carry Adder Components"
            [ inverterTests
            , andGateTests
            , orGateTests
            , halfAdderTests
            ]

If you refresh the page at http://localhost:8000/RippleCarryAdderTests.elm, you should see that all tests have passed.

If you didn’t complete the Exercise 4.5.1, you will get an error. That’s where the orGateTests function gets defined.

Testing the fullAdder Function

Here’s how the fullAdder function looks:

fullAdder a b carryIn =
    let
        firstResult =
            halfAdder b carryIn

        secondResult =
            halfAdder a firstResult.sum

        finalCarry =
            orGate firstResult.carry secondResult.carry
    in
        { carry = finalCarry
        , sum = secondResult.sum
        }

It takes one more input (carryIn) compared to the halfAdder function. Therefore, we have to account for all eight combinations of inputs as shown in the truth table below.

Add the following tests to the bottom of RippleCarryAdderTests.elm.

fullAdderTests =
    describe "Full adder"
        [ test "sum and carry-out are 0 when both inputs and carry-in are 0" <|
            \() ->
                fullAdder 0 0 0
                    |> Expect.equal { carry = 0, sum = 0 }
        , test "sum is 1 and carry-out is 0 when both inputs are 0, but carry-in is 1" <|
            \() ->
                fullAdder 0 0 1
                    |> Expect.equal { carry = 0, sum = 1 }
        , test "sum is 1 and carry-out is 0 when the 1st input is 0, the 2nd input is 1, and carry-in is 0" <|
            \() ->
                fullAdder 0 1 0
                    |> Expect.equal { carry = 0, sum = 1 }
        , test "sum is 0 and carry-out is 1 when the 1st input is 0, the 2nd input is 1, and the carry-in is 1" <|
            \() ->
                fullAdder 0 1 1
                    |> Expect.equal { carry = 1, sum = 0 }
        , test "sum is 1 and carry-out is 0 when the 1st input is 1, the 2nd input is 0, and the carry-in is 0" <|
            \() ->
                fullAdder 1 0 0
                    |> Expect.equal { carry = 0, sum = 1 }
        , test "sum is 0 and carry-out is 1 when the 1st input is 1, the 2nd input is 0, and the carry-in is 1" <|
            \() ->
                fullAdder 1 0 1
                    |> Expect.equal { carry = 1, sum = 0 }
        , test "sum is 0 and carry-out is 1 when the 1st input is 1, the 2nd input is 1, and the carry-in is 0" <|
            \() ->
                fullAdder 1 1 0
                    |> Expect.equal { carry = 1, sum = 0 }
        , test "sum is 1 and carry-out is 1 when the 1st input is 1, the 2nd input is 1, and the carry-in is 1" <|
            \() ->
                fullAdder 1 1 1
                    |> Expect.equal { carry = 1, sum = 1 }
        ]

Like each test we’ve written so far, the tests inside the fullAdderTests function represent rows from the full adder’s truth table. Add fullAdderTests to the main function.

main =
    run <|
        describe "4-bit Ripple Carry Adder Components"
            [ inverterTests
            , andGateTests
            , orGateTests
            , halfAdderTests
            , fullAdderTests
            ]

If you refresh the page at http://localhost:8000/RippleCarryAdderTests.elm, you should see that all tests have passed.

The test descriptions in fullAdderTests are too long. It makes reading the tests hard. We can improve readability by grouping similar tests inside a describe block. Replace the fullAdderTests function with the following code.

fullAdderTests =
    describe "Full adder"
        [ describe "when both inputs are 0"
            [ test "and carry-in is 0 too, then both sum and carry-out are 0" <|
                \() ->
                    fullAdder 0 0 0
                        |> Expect.equal { carry = 0, sum = 0 }
            , test "but carry-in is 1, then sum is 1 and carry-out is 0" <|
                \() ->
                    fullAdder 0 0 1
                        |> Expect.equal { carry = 0, sum = 1 }
            ]
        , describe "when the 1st input is 0, and the 2nd input is 1"
            [ test "and carry-in is 0, then sum is 1 and carry-out is 0" <|
                \() ->
                    fullAdder 0 1 0
                        |> Expect.equal { carry = 0, sum = 1 }
            , test "and carry-in is 1, then sum is 0 and carry-out is 1" <|
                \() ->
                    fullAdder 0 1 1
                        |> Expect.equal { carry = 1, sum = 0 }
            ]
        , describe "when the 1st input is 1, and the 2nd input is 0"
            [ test "and carry-in is 0, then sum is 1 and carry-out is 0" <|
                \() ->
                    fullAdder 1 0 0
                        |> Expect.equal { carry = 0, sum = 1 }
            , test "and carry-in is 1, then sum is 0 and carry-out is 1" <|
                \() ->
                    fullAdder 1 0 1
                        |> Expect.equal { carry = 1, sum = 0 }
            ]
        , describe "when the 1st input is 1, and the 2nd input is 1"
            [ test "and carry-in is 0, then sum is 0 and carry-out is 1" <|
                \() ->
                    fullAdder 1 1 0
                        |> Expect.equal { carry = 1, sum = 0 }
            , test "and carry-in is 1, then sum is 1 and carry-out is 1" <|
                \() ->
                    fullAdder 1 1 1
                        |> Expect.equal { carry = 1, sum = 1 }
            ]
        ]

They are much easier to read now. It’s recommended that you group the similar tests inside a describe. We should always look for ways to improve the readability of our tests. In addition to verifying the behavior of our code, tests also act as a documentation describing how that code should behave. Therefore, improving the readability of our tests is just as important as improving the readability of our code.

We can nest as many describe blocks as we want. Here’s the fullAdderTests function re-written with multiple describe blocks:

fullAdderTests =
    describe "Full adder"
        [ describe "when both inputs are 0"
            [ describe "and carry-in is 0"
                [ test "both sum and carry-out are 0" <|
                    \() ->
                        fullAdder 0 0 0
                            |> Expect.equal { carry = 0, sum = 0 }
                ]
            , describe "but carry-out is 1"
                [ test "sum is 1 and carry-out is 0" <|
                    \() ->
                        fullAdder 0 0 1
                            |> Expect.equal { carry = 0, sum = 1 }
                ]
            ]
        , describe "when the 1st input is 0"
            [ describe "and the 2nd input is 1"
                [ describe "and carry-in is 0"
                    [ test "sum is 1 and carry-out is 0" <|
                        \() ->
                            fullAdder 0 1 0
                                |> Expect.equal { carry = 0, sum = 1 }
                    ]
                , describe "and carry-in is 1"
                    [ test "sum is 0 and carry-out is 1" <|
                        \() ->
                            fullAdder 0 1 1
                                |> Expect.equal { carry = 1, sum = 0 }
                    ]
                ]
            ]
        , describe "when the 1st input is 1"
            [ describe "and the 2nd input is 0"
                [ describe "and carry-in is 0"
                    [ test "sum is 1 and carry-out is 0" <|
                        \() ->
                            fullAdder 1 0 0
                                |> Expect.equal { carry = 0, sum = 1 }
                    ]
                , describe "and carry-in is 1"
                    [ test "sum is 0 and carry-out is 1" <|
                        \() ->
                            fullAdder 1 0 1
                                |> Expect.equal { carry = 1, sum = 0 }
                    ]
                ]
            ]
        , describe "when the 1st input is 1"
            [ describe "and the 2nd input is 1"
                [ describe "and carry-in is 0"
                    [ test "sum is 0 and carry-out is 1" <|
                        \() ->
                            fullAdder 1 1 0
                                |> Expect.equal { carry = 1, sum = 0 }
                    ]
                , describe "and carry-in is 1"
                    [ test "sum is 1 and carry-out is 1" <|
                        \() ->
                            fullAdder 1 1 1
                                |> Expect.equal { carry = 1, sum = 1 }
                    ]
                ]
            ]
        ]

We shouldn’t go overboard with nesting describe blocks, though. Writing deeply-nested describe blocks could get tedious. That said, nesting does make the output read a little bit better when a test fails. If you try to make a test fail in the above example that has fewer describe blocks, the output will look like this:

Compare that to the output below that shows a failing test from the example that has deeply-nested describe blocks.

There are no set guidelines in Elm for how deep the nesting should go, so use whatever level of nesting you find most readable.

Exercise 4.5.2

See if you can improve readability of the tests in the halfAdderTests function above by adding more describe blocks.

Testing the rippleCarryAdder Function

A 4-bit ripple carry adder has nine inputs and five outputs as shown in the truth table below.

That means there are 512 (2 ^ 9) different permutations of the inputs we can test for. That’s a lot of tests. So the question becomes, do we have to write tests to account for each member in the input set of a function? The answer depends on how much confidence do we have in our function. If we know what our function does is quite straightforward then we might not need as much testing, but if it’s fairly complex with many edge cases then we might need to write more tests to cover those edge cases. In any case, it’s rare to have a function that warrants 100% test coverage.

“I get paid for code that works, not for tests, so my philosophy is to test as little as possible to reach a given level of confidence.” - Kent Beck

One good thing about testing in Elm is that we can combine different types of tests to achieve the right level of confidence in our code. So far we’ve only written unit tests. Elm also offers another type of testing called fuzz testing. It’s a form of testing where the same test is run over and over with randomly generated inputs. In contrast, a unit test runs the code under test only once with one input scenario, and then verifies that the output is correct.

The rippleCarryAdder function is a good candidate for fuzz testing, but we’ll have to wait until the Fuzz Testing section to find out how to write those tests. For now, let’s write a few unit tests which will supplement the fuzz tests we will write later. Add the following tests to the bottom of RippleCarryAdderTests.elm.

rippleCarryAdderTests =
    describe "4-bit ripple carry adder"
        [ describe "given two binary numbers and a carry-in digit"
            [ test "returns the sum of those numbers and a carry-out digit" <|
                \() ->
                    rippleCarryAdder 1001 1101 1
                        |> Expect.equal 10111
            ]
        , describe "when the 1st input is 1111, and the 2nd input is 1111"
            [ test "and carry-in is 0, the output is 11110" <|
                \() ->
                    rippleCarryAdder 1111 1111 0
                        |> Expect.equal 11110
            , test "and carry-in is 1, the output is 11111" <|
                \() ->
                    rippleCarryAdder 1111 1111 1
                        |> Expect.equal 11111
            ]
        , describe "when the 1st input is 0000, and the 2nd input is 0000"
            [ test "and carry-in is 0, the output is 0000" <|
                \() ->
                    rippleCarryAdder 0 0 0
                        |> Expect.equal 0
            , test "and carry-in is 1, the output is 0001" <|
                \() ->
                    rippleCarryAdder 0 0 1
                        |> Expect.equal 1
            ]
        ]

Now add rippleCarryAdderTests to the main function so that we can run the tests above.

main =
    run <|
        describe "4-bit Ripple Carry Adder Components"
            [ inverterTests
            , andGateTests
            , orGateTests
            , halfAdderTests
            , fullAdderTests
            , rippleCarryAdderTests
            ]

If you refresh the page at http://localhost:8000/RippleCarryAdderTests.elm, you should see that all tests have passed.

We wrote one test to communicate what the function actually does, and two more to test boundary cases. Unit tests are generally useful for testing a specific scenario that either represents an edge case or input boundary. The first test verifies that the rippleCarryAdder function generates an expected output when we add two binary numbers chosen by us at random. The last four tests verify that a correct output is generated when we add inputs with all 1s and 0s.

In the last two tests, we had to use single digits to represent the binary numbers with leading zeros because elm-format gets rid of zeros in the front when a file is saved. The tests still work because rippleCarryAdder pads zeros in the front if the input number doesn’t have four digits in it.

As we saw in the above examples, writing tests for pure functions is quite straightforward. All we have to do is properly identify values from the input set, feed those values into the function repeatedly, and verify that the values generated by the function belong to the output set. We don’t need to worry about verifying whether the function modified its state or caused any other side effects.

Running Tests from Terminal

Running our tests in a browser is convenient, but there are times when we want to run them from the terminal as well. Before we can do that, we need to move some code around, and install a few more packages.

Step 1: Create a new file called RunTestsInBrowser.elm in the beginning-elm/tests directory and add the following code to it.

module RunTestsInBrowser exposing (main)

import Test.Runner.Html exposing (run)
import RippleCarryAdderTests exposing (allTests)


main =
    run allTests

Going forward, instead of running our tests from the RippleCarryAdderTests.elm file, we’ll be running them from RunTestsInBrowser.elm. All RunTestsInBrowser.elm does is reach out to the RippleCarryAdderTests module to get a list of tests and run them in a browser. The allTests function will be discussed in step 3 below.

Step 2: Create another file called RunTestsInTerminal.elm in the beginning-elm/tests directory and add the following code to it.

port module RunTestsInTerminal exposing (main)

import Test.Runner.Node exposing (run)
import Json.Encode exposing (Value)
import RippleCarryAdderTests exposing (allTests)


main =
    run emit allTests


port emit : ( String, Value ) -> Cmd msg

The run function from Test.Runner.Node module emits tests out to the terminal through a port. Without understanding how ports work, it’s hard to figure out how exactly the code in RunTestsInTerminal.elm works. We will discuss ports in detail in the Ports section. After that you can come back and study this code again. We’ll install the package that contains the Test.Runner.Node module in step 6 below.

Step 3: Rename the main function in RippleCarryAdderTests.elm to allTests, and remove the line that applied the run function.

allTests =
    describe "4-bit Ripple Carry Adder Components"
        [ inverterTests
        , andGateTests
        , orGateTests
        , halfAdderTests
        , fullAdderTests
        , rippleCarryAdderTests
        ]

Step 4: Remove the following line from RippleCarryAdderTests.elm.

import Test.Runner.Html exposing (run)

Since we aren’t using the run function anymore, there’s no need to import the Test.Runner.Html module.

Step 5: Expose the allTests function in RippleCarryAdderTests.elm by replacing main with allTests like this:

module RippleCarryAdderTests exposing (allTests)

Both RunTestsInBrowser and RunTestsInTerminal modules will access the tests in RippleCarryAdderTests.elm through the allTests function. That’s why it needs to be exposed.

Step 6: Go to the beginning-elm/tests directory in the terminal and run the following command to install an Elm package that enables us to run our tests in the terminal.

It’s important that you run the following command from the tests directory. If you run it from the beginning-elm directory, the package will be added as a dependency to the production version of the elm-package.json file which is not what we want.

elm-package install rtfeldman/node-test-runner

elm-package will ask for your permission. Answer y and approve the upgrade plan it proposes.

Step 7: We need to install one more tool before we can run our tests. Go to the beginning-elm directory in the terminal and run the following command.

npm install -g elm-test

In the Installing Node.js section, we learned that one of the reasons we had to install Node.js was so that we could use NPM (Node.js Package Manager) to install tools and libraries needed to build front-end web applications. A lot of tools that are meant to streamline the development and testing process in Elm are also distributed as NPM packages. elm-test is one such package. It allows us to run Elm tests from the terminal or a continuous integration server.

The -g option above installs the package globally so that we can use the elm-test command from anywhere in the terminal.

Step 8: Now we’re ready to run our tests from the terminal. Go to the beginning-elm directory and run the following command.

elm-test tests/RunTestsInTerminal.elm

You should see the following output showing that all tests have passed.

Success! Compiled 11 modules.
Successfully generated /var/folders/w9/lhzr92wx6hnd44s39dqf5hsh0000gn/T/elm_test_11703-71443-4nkwak.jresotuik9.js

elm-test
--------

Running 28 tests. To reproduce these results, run: elm-test --seed 521208900

TEST RUN PASSED

Duration: 47 ms
Passed:   28
Failed:   0

Now we know how to run tests from the terminal. Let’s make sure that we can still run our tests in a browser. Go to the beginning-elm/tests directory and run elm-reactor. After that if you visit http://localhost:8000/RunTestsInBrowser.elm, you should see that all tests have run successfully in the browser as well.

Exercise 4.5.3

Modify a test in RippleCarryAdderTests.elm so that it’s guaranteed to fail. After that, run our test suite from the terminal to find out how the output looks when a test fails.

Exercise 4.5.4

Our current setup doesn’t allow us to run individual tests (i.e., inverterTests, orGateTests, etc.). See if you can figure out how to do that.

Back to top

New chapters are coming soon!

Sign up for the Elm Programming newsletter to get notified!

* indicates required
Close