Writing Testable Elm

Tessa Kelly

slides.com/tessak/writing-testable-elm/live

???

???

???

😬

???

???

Example code!

module Integration exposing (suite)

import Expect exposing (Expectation)
import Flags exposing (Flags, decoder)
import Init exposing (init)
import Json.Decode
import Model exposing (Model)
import Test exposing (..)
import Test.Html.Query
import Test.Html.Selector
import View exposing (view)


suite : Test
suite =
    test "page renders" <|
        \() ->
            case Json.Decode.decodeString decoder json of
                Ok flags ->
                    init flags
                        |> Tuple.first
                        |> view
                        |> Test.Html.Query.fromHtml
                        |> Test.Html.Query.has
                            [ Test.Html.Selector.text "Welcome, Tessa!"
                            , Test.Html.Selector.text "You've been active on this site for ________ seconds."
                            ]

                Err err ->
                    Expect.fail (Json.Decode.errorToString err)
import Test.Html.Query
import Test.Html.Selector
import View exposing (view)


suite : Test
suite =
    test "page renders" <|
        \() ->
            case Json.Decode.decodeString decoder json of
                Ok flags ->
                    init flags
                        |> Tuple.first
                        |> view
                        |> Test.Html.Query.fromHtml
                        |> Test.Html.Query.has
                            [ Test.Html.Selector.text "Welcome, Tessa!"
                            , Test.Html.Selector.text "You've been active on this site for ________ seconds."
                            ]

                Err err ->
                    Expect.fail (Json.Decode.errorToString err)

Testing for Collaboration

..

import Html exposing (div, Html)
import Html.Attributes exposing (class)

..

{-| Displays contents as elements on a slide.
-}
viewSlide : List (Html msg) -> Html msg
viewSlide contents =
	div [ class "slide" ] contents
    

Integrating


suite : Test
suite =
    test "page renders" <|
        \() ->
            case Json.Decode.decodeString decoder json of
                Ok flags ->
                    init flags
                        |> Tuple.first
                        |> view
                        |> Test.Html.Query.fromHtml
                        |> Test.Html.Query.has
                            [ Test.Html.Selector.text "Welcome, Tessa!"
                            , Test.Html.Selector.text "You've been active on this site for ________ seconds."
                            ]

                Err err ->
                    Expect.fail (Json.Decode.errorToString err)
module View exposing (view)

import Html exposing (..)
import Model exposing (Model)
import Time
import Update exposing (Msg(..))


view : Model -> Html msg
view model =
    let
        secondsActive =
            Model.secondsActive model
                |> Maybe.map String.fromInt
                |> Maybe.withDefault "________" 
    in
    div []
        [ h1 [] [ text ("Welcome, " ++ model.user.firstName ++ "!") ]
        , p [] [ text ("You've been active on this site for " ++ secondsActive ++ " seconds.") ]
        ]

type alias User =
    { firstName : String
    , lastName : Maybe String
    , userName : String
    , email : Maybe Email
    , birthTime : Time.Posix
    , activeSince : Maybe Time.Posix
    }


type alias Email =
    String


secondsActive : Model -> Maybe Int
secondsActive model =
    case ( model.user.activeSince, model.currentTime ) of
        ( Just time, Just otherTime ) ->
            (Time.posixToMillis time
                - Time.posixToMillis otherTime
            )
                // 1000
                |> Just

        _ ->
            Nothing
module Model exposing (Model, User, secondsActive)

import Time


type alias Model =
    { user : User
    , currentTime : Maybe Time.Posix
    }


type alias User =
    { firstName : String
    , lastName : Maybe String
    , userName : String
    , email : Maybe Email
    , birthTime : Time.Posix
    , activeSince : Maybe Time.Posix
    }


type alias Email =
    String


secondsActive : Model -> Maybe Int
secondsActive model =
    case ( model.user.activeSince, model.currentTime ) of
        ( Just time, Just otherTime ) ->
            (Time.posixToMillis time
                - Time.posixToMillis otherTime
            )
                // 1000
                |> Just

        _ ->
            Nothing
module ModelSpec exposing (suite)

import Expect exposing (Expectation)
import Model exposing (Model)
import Test exposing (..)
import Time


suite : Test
suite =
    describe "secondsActive" 
        [ todo "activeSince is Nothing and currentTime is Nothing" 
        , todo "activeSince is a value and currentTime is Nothing" 
        , todo "activeSince is Nothing and currentTime is a value" 
        , todo "activeSince is a value and currentTime is a value"
        ]
module ModelSpec exposing (suite)

import Expect exposing (Expectation)
import Model exposing (Model)
import Test exposing (..)
import Time


suite : Test
suite =
    describe "secondsActive" 
        [ test "activeSince is Nothing and currentTime is Nothing" <|
            \() ->
                  { user =
                    { firstName = "Sally"
                    , lastName = Nothing
                    , userName = "user_sally"
                    , email = Nothing
                    , birthTime = Time.millisToPosix 1234
                    , activeSince = Nothing
                    }
                  , currentTime = Nothing
                  }
                    |> Model.secondsActive
                    |> Expect.equal Nothing
        , todo "activeSince is a value and currentTime is Nothing" 
        , todo "activeSince is Nothing and currentTime is a value" 
        , todo "activeSince is a value and currentTime is a value"
        ]
import Time


dummyModel : Model
dummyModel =
    { user =
        { firstName = "Sally"
        , lastName = Nothing
        , userName = "user_sally"
        , email = Nothing
        , birthTime = Time.millisToPosix 1234
        , activeSince = Nothing
        }
    , currentTime = Nothing
    }


suite : Test
suite =
    describe "secondsActive" 
        [ test "activeSince is Nothing and currentTime is Nothing" <|
            \() ->
                dummyModel
                    |> Model.secondsActive
                    |> Expect.equal Nothing
        , todo "activeSince is a value and currentTime is Nothing" 
        , todo "activeSince is Nothing and currentTime is a value"
        , todo "activeSince is a value and currentTime is a value"
        ]
        , activeSince = Nothing
        }
    , currentTime = Nothing
    }


suite : Test
suite =
    describe "secondsActive" 
        [ test "activeSince is Nothing and currentTime is Nothing" <|
            \() ->
                dummyModel
                    |> Model.secondsActive
                    |> Expect.equal Nothing
        , test "activeSince is a value and currentTime is Nothing" <|
            \() ->
                let
                    dummyUser =
                        dummyModel.user

                    userWithActiveSince =
                        { dummyUser | activeSince = Just (Time.millisToPosix 123) }
                in
                { dummyModel | user = userWithActiveSince }
                    |> Model.secondsActive
                    |> Expect.equal Nothing
        , todo "activeSince is Nothing and currentTime is a value"
        , todo "activeSince is a value and currentTime is a value"
        ]

suite : Test
suite =
    describe "secondsActive" 
        [ test "activeSince is Nothing and currentTime is Nothing" <|
            \() ->
                dummyModel
                    |> Model.secondsActive
                    |> Expect.equal Nothing
        , test "activeSince is a value and currentTime is Nothing" <|
            \() ->
                let
                    dummyUser =
                        dummyModel.user

                    userWithActiveSince =
                        { dummyUser | activeSince = Just (Time.millisToPosix 123) }
                in
                { dummyModel | user = userWithActiveSince }
                    |> Model.secondsActive
                    |> Expect.equal Nothing
        , todo "activeSince is Nothing and currentTime is a value" <|
            \() ->
                { dummyModel | currentTime = Just (Time.millisToPosix 123) }
                    |> Model.secondsActive
                    |> Expect.equal Nothing
        , todo "activeSince is a value and currentTime is a value"
        ]
                        dummyModel.user

                    userWithActiveSince =
                        { dummyUser | activeSince = Just (Time.millisToPosix 123) }
                in
                { dummyModel | user = userWithActiveSince }
                    |> Model.secondsActive
                    |> Expect.equal Nothing
        , todo "activeSince is Nothing and currentTime is a value" <|
            \() ->
                { dummyModel | currentTime = Just (Time.millisToPosix 123) }
                    |> Model.secondsActive
                    |> Expect.equal Nothing
        , test "activeSince is a value and currentTime is a value" <|
            \() ->
                let
                    dummyUser =
                        dummyModel.user

                    userWithActiveSince =
                        { dummyUser | activeSince = Just (Time.millisToPosix 123) }
                in
                { dummyModel
                    | user = userWithActiveSince
                    , currentTime = Just (Time.millisToPosix 123)
                }
                    |> Model.secondsActive
                    |> Expect.equal (Just 0)
        ]

Results

➜  2019_elmconf git:(master) ✗ elm-test

elm-test 0.19.0-beta9
---------------------

Running 3 tests. To reproduce these results, run: elm-test --fuzz 100 --seed 366961207238742 /Users/tessakelly/Documents/2019_elmconf/tests/Integration.elm /Users/tessakelly/Documents/2019_elmconf/tests/ModelSpec.elm


TEST RUN PASSED

Duration: 165 ms
Passed:   3
Failed:   0
                        dummyModel.user

                    userWithActiveSince =
                        { dummyUser | activeSince = Just (Time.millisToPosix 123) }
                in
                { dummyModel | user = userWithActiveSince }
                    |> Model.secondsActive
                    |> Expect.equal Nothing
        , todo "activeSince is Nothing and currentTime is a value" <|
            \() ->
                { dummyModel | currentTime = Just (Time.millisToPosix 123) }
                    |> Model.secondsActive
                    |> Expect.equal Nothing
        , test "activeSince is a value and currentTime is a value" <|
            \() ->
                let
                    dummyUser =
                        dummyModel.user

                    userWithActiveSince =
                        { dummyUser | activeSince = Just (Time.millisToPosix 123) }
                in
                { dummyModel
                    | user = userWithActiveSince
                    , currentTime = Just (Time.millisToPosix 123)
                }
                    |> Model.secondsActive
                    |> Expect.equal (Just 0)
        ]
module Model exposing (Model, User, secondsActive)

import Time


type alias Model =
    { user : User
    , currentTime : Maybe Time.Posix
    }


type alias User =
    { firstName : String
    , lastName : Maybe String
    , userName : String
    , email : Maybe Email
    , birthTime : Time.Posix
    , activeSince : Maybe Time.Posix
    }


type alias Email =
    String



secondsActive : Model -> Maybe Int
secondsActive model =
    case ( model.user.activeSince, model.currentTime ) of
        ( Just time, Just otherTime ) ->
            (Time.posixToMillis time
                - Time.posixToMillis otherTime
            )
                // 1000
                |> Just

        _ ->
            Nothing





























module ModelSpec exposing (suite)

import Expect exposing (Expectation)
import Model exposing (Model)
import Test exposing (..)
import Time


dummyModel : Model
dummyModel =
    { user =
        { firstName = "Sally"
        , lastName = Nothing
        , userName = "user_sally"
        , email = Nothing
        , birthTime = Time.millisToPosix 1234
        , activeSince = Nothing
        }
    , currentTime = Nothing
    }


suite : Test
suite =
    describe "secondsActive"
        [ test "activeSince is Nothing and currentTime is Nothing" <|
            \() ->
                dummyModel
                    |> Model.secondsActive
                    |> Expect.equal Nothing
        , test "activeSince is a value and currentTime is nothing" <|
            \() ->
                let
                    dummyUser =
                        dummyModel.user

                    userWithActiveSince =
                        { dummyUser | activeSince = Just (Time.millisToPosix 123) }
                in
                { dummyModel | user = userWithActiveSince }
                    |> Model.secondsActive
                    |> Expect.equal Nothing
        , todo "activeSince is Nothing and currentTime is a value" <|
            \() ->
                { dummyModel | currentTime = Just (Time.millisToPosix 123) }
                    |> Model.secondsActive
                    |> Expect.equal Nothing
        , test "activeSince is a value and currentTime is a value" <|
            \() ->
                let
                    dummyUser =
                        dummyModel.user

                    userWithActiveSince =
                        { dummyUser | activeSince = Just (Time.millisToPosix 123) }
                in
                { dummyModel
                    | user = userWithActiveSince
                    , currentTime = Just (Time.millisToPosix 123)
                }
                    |> Model.secondsActive
                    |> Expect.equal (Just 0)
        ]

???

???

???

🤔

???

???

Trying to test everything all at once

Testing logic

Testing behavior

Exposed Values

module Flags exposing (Flags, User, decoder)
..

module Init exposing (init)
..

module Main exposing (main)
..

module Model exposing (Model, User, secondsActive)
..

module Port exposing (time)
..

module Update exposing (Msg(..), update)
..

module View exposing (view)
..
module Main exposing (Model, decoder, init, main, update, view)

import Browser
import Html exposing (..)
import Json.Decode exposing (Decoder)
import Json.Decode.Pipeline as Pipeline
import Task
import Time


main : Program Json.Decode.Value Model Msg
main =
    Browser.element
        { init = init
        , view = view
        , update = update
        , subscriptions = always time
        }


time : Sub Msg
time =
    Time.every 1000 NewTime


type alias Flags =
    { user : UserFlags
    }


decoder : Decoder Flags
decoder =
    Json.Decode.map Flags decodeUser


type alias UserFlags =
    { firstName : Maybe String
    , lastName : Maybe String
    , userName : String
    , email : Maybe String
    , birthTime : Int
    }



type alias Flags =
    { user : UserFlags
    }


decoder : Decoder Flags
decoder =
    Json.Decode.map Flags decodeUser


type alias UserFlags =
    { firstName : Maybe String
    , lastName : Maybe String
    , userName : String
    , email : Maybe String
    , birthTime : Int
    }


decodeUser : Decoder UserFlags
decodeUser =
    Json.Decode.succeed UserFlags
        |> Pipeline.required "firstName" (Json.Decode.nullable Json.Decode.string)
        |> Pipeline.required "lastName" (Json.Decode.nullable Json.Decode.string)
        |> Pipeline.required "username" Json.Decode.string
        |> Pipeline.required "email" (Json.Decode.nullable Json.Decode.string)
        |> Pipeline.required "birthtime" Json.Decode.int

module Integration exposing (suite)

import Expect exposing (Expectation)
import Json.Decode
import Main exposing (Model, decoder, init, update, view)
import Test exposing (..)
import Test.Html.Query
import Test.Html.Selector


suite : Test
suite =
    test "page renders" <|
        \() ->
            case Json.Decode.decodeString decoder json of
                Ok flags ->
                    init flags
                        |> Tuple.first
                        |> view
                        |> Test.Html.Query.fromHtml
                        |> Test.Html.Query.has
                            [ Test.Html.Selector.text "Welcome, Tessa!"
                            , Test.Html.Selector.text "You've been active on this site for ________ seconds."
                            ]

                Err err ->
                    Expect.fail (Json.Decode.errorToString err)
module User exposing (Flags, User, decoder, init)

import Json.Decode exposing (Decoder)
import Json.Decode.Pipeline as Pipeline
import Time


type alias Flags =
    { firstName : Maybe String
    , lastName : Maybe String
    , userName : String
    , email : Maybe String
    , birthTime : Int
    }


decoder : Decoder Flags
decoder =
    Json.Decode.succeed Flags
        |> Pipeline.required "firstName" (Json.Decode.nullable Json.Decode.string)
        |> Pipeline.required "lastName" (Json.Decode.nullable Json.Decode.string)
        |> Pipeline.required "username" Json.Decode.string
        |> Pipeline.required "email" (Json.Decode.nullable Json.Decode.string)
        |> Pipeline.required "birthtime" Json.Decode.int


init : Flags -> User
init user =
    { firstName = Maybe.withDefault "friend" user.firstName
    , lastName = user.lastName
    , userName = user.userName
    , email = user.email
    , birthTime = Time.millisToPosix (user.birthTime * 1000)
    , activeSince = Nothing
    }


type alias User =
    { firstName : String
    , lastName : Maybe String
    , userName : String
    , email : Maybe Email
    , birthTime : Time.Posix
    , activeSince : Maybe Time.Posix
    }


type alias Email =
    String
module Integration exposing (suite)

import Expect exposing (Expectation)
import Json.Decode
import Main exposing (Model, decoder, init, update, view)
import Test exposing (..)
import Test.Html.Query
import Test.Html.Selector


suite : Test
suite =
    test "page renders" <|
        \() ->
            case Json.Decode.decodeString decoder json of
                Ok flags ->
                    init flags
                        |> Tuple.first
                        |> view
                        |> Test.Html.Query.fromHtml
                        |> Test.Html.Query.has
                            [ Test.Html.Selector.text "Welcome, Tessa!"
                            , Test.Html.Selector.text "You've been active on this site for ________ seconds."
                            , Test.Html.Selector.text "You've been out and about for ________ seconds."
                            ]

                Err err ->
                    Expect.fail (Json.Decode.errorToString err)


-- MODEL


type alias Model =
    { user : User
    , currentTime : Maybe Time.Posix
    }


secondsActive : Model -> Maybe Int
secondsActive model =
    case ( model.user.activeSince, model.currentTime ) of
        ( Just time_, Just otherTime ) ->
            (Time.posixToMillis time_
                - Time.posixToMillis otherTime
            )
                // 1000
                |> Just

        _ ->
            Nothing

Implementation Counter

doAThing : a -> a
doAThing a =
    ?
doAThing : a -> a
doAThing a =
    a

Implementation Counter

doAThing : a -> a
doAThing a =
    a


doAStringThing : String -> String
doAStringThing string =
    ?

Implementation Counter

doAThing : a -> a
doAThing a =
    a


doAStringThing : String -> String
doAStringThing string =
    string ++ "💃" ++ string ++ "💃" ++ string

Implementation Counter

doAThing : a -> a
doAThing a =
    a


doAStringThing : String -> String
doAStringThing string =
    string ++ "💃" ++ string ++ "💃" ++ string

Test me!



-- MODEL


type alias Model =
    { user : User
    , currentTime : Maybe Time.Posix
    }


secondsActive : Model -> Maybe Int
secondsActive model =
    case ( model.user.activeSince, model.currentTime ) of
        ( Just time_, Just otherTime ) ->
            (Time.posixToMillis time_
                - Time.posixToMillis otherTime
            )
                // 1000
                |> Just

        _ ->
            Nothing


-- MODEL


type alias Model =
    { user : User
    , currentTime : Maybe Time.Posix
    }


secondsActive : Model -> Maybe Int
secondsActive model =
    case ( model.user.activeSince, model.currentTime, model.user.firstName ) of
        ( Just time, Just otherTime, "Sally" ) ->
            (Time.posixToMillis time
                - Time.posixToMillis otherTime
            )
                // 1000
                |> Just

        _ ->
            Nothing

Model Complexity

-- Main.elm

type alias Model =
    { user : User
    , currentTime : Maybe Time.Posix
    }


-- User.elm


type alias User =
    { firstName : String
    , lastName : Maybe String
    , userName : String
    , email : Maybe Email
    , birthTime : Time.Posix
    , activeSince : Maybe Time.Posix
    }


type alias Email =
    String

If you want to test code easily, make the code easy to test!

Model Complexity, 2

secondsActive : Model -> Maybe Int
secondsActive model =
    case ( model.user.activeSince, model.currentTime ) of
        ( Just time_, Just otherTime ) ->
            (Time.posixToMillis time_
                - Time.posixToMillis otherTime
            )
                // 1000
                |> Just

        _ ->
            Nothing
secondsActive :
    { user :
        { firstName : String
        , lastName : Maybe String
        , userName : String
        , email : Maybe Email
        , birthTime : Time.Posix
        , activeSince : Maybe Time.Posix
        }
    , currentTime : Maybe Time.Posix
    }
    -> Maybe Int
secondsActive model =
    case ( model.user.activeSince, model.currentTime ) of
        ( Just time_, Just otherTime ) ->
            (Time.posixToMillis time_
                - Time.posixToMillis otherTime
            )
                // 1000
                |> Just

        _ ->
            Nothing

Removing Structure

secondsActive :
    Time.Posix
    ->
        { firstName : String
        , lastName : Maybe String
        , userName : String
        , email : Maybe Email
        , birthTime : Time.Posix
        , activeSince : Maybe Time.Posix
        }
    -> Maybe Int
secondsActive currentTime user =
    case user.activeSince of
        Just time_ ->
            (Time.posixToMillis time_
                - Time.posixToMillis currentTime
            )
                // 1000
                |> Just

        Nothing ->
            Nothing
secondsActive :
    Time.Posix
    -> User    
    -> Maybe Int
secondsActive currentTime user =
    case user.activeSince of
        Just time_ ->
            (Time.posixToMillis time_
                - Time.posixToMillis currentTime
            )
                // 1000
                |> Just

        Nothing ->
            Nothing

Removing Structure, 2

secondsActive : Time.Posix -> Time.Posix -> Int
secondsActive currentTime activeSince =
    (Time.posixToMillis activeSince - Time.posixToMillis currentTime ) // 1000


























Removing Structure, 2

secondsActive : Time.Posix -> Time.Posix -> Int
secondsActive currentTime activeSince =
    (Time.posixToMillis activeSince - Time.posixToMillis currentTime ) // 1000


secondsActive2 : Time.Posix -> { user | activeSince = Time.Posix } -> Maybe Int
secondsActive2 currentTime { activeSince } =
    case activeSince of
        Just time_ ->
            ( Time.posixToMillis time_ - Time.posixToMillis otherTime ) // 1000
                |> Just

        Nothing ->
            Nothing












Removing Structure, 2

secondsActive : Time.Posix -> Time.Posix -> Int
secondsActive currentTime activeSince =
    (Time.posixToMillis activeSince - Time.posixToMillis currentTime ) // 1000


secondsActive2 : Time.Posix -> { user | activeSince = Time.Posix } -> Maybe Int
secondsActive2 currentTime { activeSince } =
    case activeSince of
        Just time_ ->
            ( Time.posixToMillis time_ - Time.posixToMillis otherTime ) // 1000
                |> Just

        Nothing ->
            Nothing


secondsActive3 : { currentTime : Time.Posix, activeSince : Time.Posix } -> Int
secondsActive3 {currentTime, activeSince} =
    (Time.posixToMillis activeSince - Time.posixToMillis currentTime ) // 1000










Removing Structure, 2

    case activeSince of
        Just time_ ->
            ( Time.posixToMillis time_ - Time.posixToMillis otherTime ) // 1000
                |> Just

        Nothing ->
            Nothing


secondsActive3 : { currentTime : Time.Posix, activeSince : Time.Posix } -> Int
secondsActive3 {currentTime, activeSince} =
    (Time.posixToMillis activeSince - Time.posixToMillis currentTime ) // 1000


type CurrentTime 
    = CurrentTime Time.Posix

type ActiveSince 
    = ActiveSince Time.Posix

secondsActive4 : CurrentTime -> ActiveSince -> Int
secondsActive4 (CurrentTime currentTime) (ActiveSince activeSince) =
    (Time.posixToMillis activeSince - Time.posixToMillis currentTime ) // 1000








Simply Testing

test "secondsActive" <|
  \() ->
     { activeSince = Just (millisToPosix 1000) }
         |> secondsActive (millisToPosix 4000)
         |> Expect.equal 3
secondsActive :
    Time.Posix
    -> { u | activeSince : Maybe Time.Posix }
    -> Maybe Int
secondsActive time_ user =
    case user.activeSince of
        Just activeSince ->
            (Time.posixToMillis activeSince
                - Time.posixToMillis time_
            )
                // 1000
                |> Just

        Nothing ->
            Nothing

Results

➜  v6 elm-test

elm-test 0.19.0-beta9
---------------------

Running 2 tests. To reproduce these results, run: elm-test --fuzz 100 --seed 378839653482905 /Users/tessakelly/Documents/2019_elmconf/tests/Integration.elm /Users/tessakelly/Documents/2019_elmconf/tests/UserSpec.elm

↓ UserSpec
✗ secondsActive

    -3
    ╷
    │ Expect.equal
    ╵
    3



TEST RUN FAILED

Duration: 169 ms
Passed:   1
Failed:   1

🐛 Fix

➜  v6 elm-test

elm-test 0.19.0-beta9
---------------------

Running 2 tests. To reproduce these results, run: elm-test --fuzz 100 --seed 367277347268046 /Users/tessakelly/Documents/2019_elmconf/tests/Integration.elm /Users/tessakelly/Documents/2019_elmconf/tests/UserSpec.elm


TEST RUN PASSED

Duration: 171 ms
Passed:   2
Failed:   0
secondsActive :
    Time.Posix
    -> { u | activeSince : Maybe Time.Posix }
    -> Maybe Int
secondsActive time_ user =
    case user.activeSince of
        Just activeSince ->
            (Time.posixToMillis time_
                - Time.posixToMillis activeSince
            )
                // 1000
                |> Just

        Nothing ->
            Nothing

Removing Structure, 3

secondsActive : Time.Posix -> { user | activeSince : Maybe Time.Posix } -> Int
secondsActive currentTime user =
    case user.activeSince of
        Just activeSince ->
            (Time.posixToMillis activeSince  - Time.posixToMillis currentTime)
                // 1000

        Nothing ->
            0

Property tests!

module UserSpec exposing (secondsActiveSpec)

import Expect exposing (Expectation)
import Test exposing (..)
import Time
import User exposing (secondsActive)


secondsActiveSpec : Test
secondsActiveSpec =
    test "secondsActive" <|
        \() ->
            { activeSince = Just (Time.millisToPosix 1000) }
                |> secondsActive (Time.millisToPosix 4000)
                |> Expect.equal 3















Fuzz Tests

import Expect exposing (Expectation)
import Fuzz exposing (Fuzzer)
import Random
import Test exposing (..)
import Time
import User exposing (secondsActive)


secondsActiveFuzzSpec : Test
secondsActiveFuzzSpec =
    fuzz timeFuzzers "seconds active is always greater than or equal to zero" <|
        \( activeSince, currentTime ) ->
            secondsActive currentTime activeSince
                |> Expect.atLeast 0


timeFuzzers : Fuzzer ( { activeSince : Maybe Time.Posix }, Time.Posix )
timeFuzzers =
    Fuzz.map2
        (\t t2 -> ( { activeSince = Just (Time.millisToPosix t) }, Time.millisToPosix (t + t2) ))
        (Fuzz.intRange 0 Random.maxInt)
        (Fuzz.intRange 0 Random.maxInt)

Integration tests!

Testing User-facing Behavior

import Model exposing (Model)
import Test exposing (..)
import Test.Html.Query
import Test.Html.Selector
import View exposing (view)


suite : Test
suite =
    test "page renders" <|
        \() ->
            case Json.Decode.decodeString decoder json of
                Ok flags ->
                    init flags
                        |> Tuple.first
                        |> view
                        |> Test.Html.Query.fromHtml
                        |> Test.Html.Query.has
                            [ Test.Html.Selector.text "Welcome, Tessa!"
                            , Test.Html.Selector.text "You've been active on this site for 0 seconds."
                            ]

                Err err ->
                    Expect.fail (Json.Decode.errorToString err)

ProgramTest Setup

module Integration exposing (suite)

import Expect exposing (Expectation)
import Main exposing (Model, Msg(..), decoder, init, update, view)
import ProgramTest exposing (ProgramTest)
import Test exposing (..)
import Test.Html.Query as Query
import Test.Html.Selector as Selector
import Time


programTest : String -> ProgramTest Model Msg (Cmd Msg)
programTest flags =
    ProgramTest.createElement
        { init = init
        , update = update
        , view = view
        }
        |> ProgramTest.withJsonStringFlags decoder
        |> ProgramTest.start flags





import Test.Html.Selector as Selector
import Time


programTest : String -> ProgramTest Model Msg (Cmd Msg)
programTest flags =
    ProgramTest.createElement
        { init = init
        , update = update
        , view = view
        }
        |> ProgramTest.withJsonStringFlags decoder
        |> ProgramTest.start flags


suite : Test
suite =
    test "page renders" <|
        \() ->
            json
                |> programTest
                |> ProgramTest.ensureView
                    (Query.has
                        [ Selector.text "Welcome, Tessa!"
                        , Selector.text "You've been active on this site for 0 seconds."
                        ]
                    )
                |> ProgramTest.done

    ProgramTest.createElement
        { init = init
        , update = update
        , view = view
        }
        |> ProgramTest.withJsonStringFlags decoder
        |> ProgramTest.start flags


suite : Test
suite =
    test "page renders" <|
        \() ->
            json
                |> programTest
                |> ProgramTest.ensureView
                    (Query.has
                        [ Selector.text "Welcome, Tessa!"
                        , Selector.text "You've been active on this site for 0 seconds."
                        ]
                    )
                |> ProgramTest.update (NewTime (Time.millisToPosix 1500000000000))
                |> ProgramTest.ensureView
                    (Query.has
                        [ Selector.text "Welcome, Tessa!"
                        , Selector.text "You've been active on this site for 0 seconds."
                        ]
                    )
                |> ProgramTest.done
                |> programTest
                |> ProgramTest.ensureView
                    (Query.has
                        [ Selector.text "Welcome, Tessa!"
                        , Selector.text "You've been active on this site for 0 seconds."
                        ]
                    )
                |> ProgramTest.update (NewTime (Time.millisToPosix 1500000000000))
                |> ProgramTest.ensureView
                    (Query.has
                        [ Selector.text "Welcome, Tessa!"
                        , Selector.text "You've been active on this site for 0 seconds."
                        ]
                    )
                |> ProgramTest.update (NewTime (Time.millisToPosix 1500000001000))
                |> ProgramTest.ensureView
                    (Query.has
                        [ Selector.text "Welcome, Tessa!"
                        , Selector.text "You've been active on this site for 1 seconds."
                        ]
                    )
                |> ProgramTest.update (NewTime (Time.millisToPosix 1500000002000))
                |> ProgramTest.ensureView
                    (Query.has
                        [ Selector.text "Welcome, Tessa!"
                        , Selector.text "You've been active on this site for 2 seconds."
                        ]
                    )

                |> ProgramTest.done
                |> programTest
                |> ProgramTest.ensureView
                    (Query.has
                        [ Selector.text "Welcome, Tessa!"
                        , Selector.text "You've been active on this site for 0 seconds."
                        ]
                    )
                |> ProgramTest.update (NewTime (Time.millisToPosix 1500000000000))
                |> ProgramTest.ensureView
                    (Query.has
                        [ Selector.text "Welcome, Tessa!"
                        , Selector.text "You've been active on this site for 0 seconds."
                        ]
                    )
                |> ProgramTest.update (NewTime (Time.millisToPosix 1500000001000))
                |> ProgramTest.ensureView
                    (Query.has
                        [ Selector.text "Welcome, Tessa!"
                        , Selector.text "You've been active on this site for 1 seconds."
                        ]
                    )
                |> ProgramTest.update (NewTime (Time.millisToPosix 1500000002000))
                |> ProgramTest.ensureView
                    (Query.has
                        [ Selector.text "Welcome, Tessa!"
                        , Selector.text "You've been active on this site for 2 seconds."
                        ]
                    )

                |> ProgramTest.done

Fix me!

module Integration exposing (suite)

import Expect exposing (Expectation)
import Main exposing (Model, Msg(..), decoder, init, update, view)
import ProgramTest exposing (ProgramTest)
import Test exposing (..)
import Test.Html.Query as Query
import Test.Html.Selector as Selector
import Time


programTest : String -> ProgramTest Model Msg (Cmd Msg)
programTest flags =
    ProgramTest.createElement
        { init = init
        , update = update
        , view = view
        }
        |> ProgramTest.withJsonStringFlags decoder
        |> ProgramTest.start flags


suite : Test
suite =
    test "page renders" <|
        \() ->
            json
                |> programTest
                |> ProgramTest.ensureView
                    (Query.has
                        [ Selector.text "Welcome, Tessa!"

Where did this come from?!

Testing application edges

module Integration exposing (suite)

import Edge.GetUser
import Edge.GetUserWithFeatureFlag
...


suite : Test
suite =
    describe "page renders"
        [ test "for a user" <|
            \() ->
                Edge.GetUser.json
                    |> programTest
                    ...
        , test "for a user with a feature flag turned on" <|
            \() ->
                Edge.GetUserWithFeatureFlag.json
                    |> programTest
                    ...
        ]

!!!

!!!

!!!

🗝️💡

!!!

!!!

!!!

!!!

!!!

!!!

!!!

!!!

!!!

Test the breakable bits

Test for humanity

Lean on the compiler

Thank you!

t_kelly9

tesk9

Tessa Kelly

Writing Testable Elm

By Tessa K

Writing Testable Elm

Writing Elm applications is a joy, but do you feel the same way when you go to write or modify your test code? In this talk, we'll explore techniques for writing easily testable Elm code. We'll cover testing decoders and initialization logic, testing complex user flows, testing modules (inlcuding those taking advantage of the benefits of opaque types), and adding property testing. Coming away from this talk, you should be able to identify the traits that make some Elm code challenging to test, and you should feel empowered to refactor with testability in mind. Whether you're excited or nervous about testing, this talk will have something for you. Get ready to verify some behavior!

  • 2,403