brian-watkins / elm-spec / Spec.Witness

A Witness can be used to record information about the arguments passed to a Cmd-generating function.

If you use dependency inversion to decouple part of your program from the Cmd-generating functions it depends upon, you can use a Witness to prove that this part of your program works with the Cmd-generating function in the right way.

Let's say I have a Cmd-generating function that takes a score (an Int) and saves it to the server via some HTTP request. Let's further suppose I want to describe the behavior of my user interface, without worrying about the details of how a score is saved. In this case, I'll 'inject' the Cmd-generating function instead of calling it directly from my update function. This allows me to substitute a fake function during my spec.

Here's the update function:

update : (Int -> Cmd Msg) -> Msg -> Model -> (Model, Cmd Msg)
update scoreSaver msg model =
  case msg of
    SaveScore score ->
      ( model, scoreSaver score )
    ...

To use a Witness, you must reference the elmSpecOut port defined when configuring Spec.program or Spec.browserProgram. I suggest creating a file called Spec.Witness.Extra like so:

module Spec.Witness.Extra exposing (record)

import Runner -- your own setup module
import Spec.Witness

record =
  Runner.elmSpecOut
    |> Spec.Witness.connect
    |> Spec.Witness.record

Now, I can write a spec that uses a witness to record the score passed to the injected scoreSaver function like so:

Spec.describe "saving the score"
[ Spec.scenario "successful save" (
    Spec.given (
      Spec.Setup.init (App.init testFlags)
        |> Spec.Setup.withView App.view
        |> Spec.Setup.withUpdate (
          App.update (\score ->
            Json.Encode.int score
              |> Spec.Witness.Extra.record "saved-score"
          )
        )
    )
    |> Spec.when "the score is saved"
      [ Spec.Markup.target << by [ id "game-over-button" ]
      , Spec.Markup.Event.click
      ]
    |> it "saves the proper score" (
      Witness.observe "saved-score" (Json.Decode.int)
        |> Spec.expect (Spec.Claim.isListWhere
          [ Spec.Claim.isEqual Debug.toString 28
          ]
        )
    )
  )
]


type Witness msg

Use a Witness to record information from inside a Cmd-generating function.

connect : (Spec.Message.Message -> Platform.Cmd.Cmd msg) -> Witness msg

Create a Witness by connecting it with the elmSpecOut port.

When you configure Spec.program or Spec.browserProgram you must provide a reference to a port called elmSpecOut. Pass that same port to this function to create a Witness.

For example, you might create a file called Spec.Witness.Extra that sets up a record function for you to use in your specs:

module Spec.Witness.Extra exposing (record)

import Runner -- your own setup module
import Spec.Witness

record =
  Runner.elmSpecOut
    |> Spec.Witness.connect
    |> Spec.Witness.record

See Spec.Config and the README for more information on the elmSpecOut port.

record : Witness msg -> String -> Json.Encode.Value -> Platform.Cmd.Cmd msg

Create a Cmd that records some information.

Provide the name of this witness and a JSON value with any information to be recorded. Use Spec.Witness.observe to make a claim about the recorded value.

observe : String -> Json.Decode.Decoder a -> Spec.Observer.Observer model (List a)

Observe the values recorded by a witness.

Provide the name of the witness and a JSON decoder that can decode whatever value you need to observe.