Janiczek/architecture-test/1.0.1

NOTE: the latest version is 2.1.2

ArchitectureTest

A library for fuzz testing TEA models by simulating user interactions (using fuzzed lists of Msgs).

This means:

  • start with a model (can be fuzzed, see TestedModel in Types module)
  • generate random Msgs (ie. "what the user would do")
  • apply them to the model
  • test a property of the model (ie. "Cancel Msg sets currentCoins to 0")

You get the nice property of fuzz tests that this kind of testing will show you the minimal Msg sequence to provoke a bug.

The app in doc examples below is:

{ model = ConstantModel model
, update = BeginnerUpdate update
, msgFuzzer =
      Fuzz.oneOf
          [ Fuzz.int 0 50 |> Fuzz.map AddCoins
          , Fuzz.constant Cancel
          , Fuzz.constant Buy
          , Fuzz.constant TakeProduct
          ]
}

For a complete code example, see the examples/ directory of the repo.

Tests

String
-> Config model msg
-> TestedApp model msg
-> Fuzzer msg
-> (model -> msg -> model -> Expectation)
-> Test
 Tests that a condition holds for a randomly generated Model
after that specific Msg is applied.

The process is as follows:

1.  get an initial `Model` (based on `TestedApp`)

2.  stuff it and a list of random `Msg`s into `update` to get a random `Model`

3.  create a `Msg` we will test

4.  stuff it and the random `Model` into `update` to get the final `Model`

5.  run your test function on the three values (random Model, tested Msg, final Model)

    cancelReturnsMoney : Test
    cancelReturnsMoney =
    msgTest "Cancelling returns all input money"
    config
    app
    (Fuzz.constant Cancel)
    <|
    _ _ finalModel ->
    finalModel.currentCoins
    |> Expect.equal 0

The test function's arguments are:

    random Model (before the tested Msg) -> tested Msg -> final Model

String
-> Config model msg
-> TestedApp model msg
-> Fuzzer msg
-> (model -> Bool)
-> (model -> msg -> model -> Expectation)
-> Test

Similar to msgTest, but only gets run when a precondition holds.

buyingAbovePriceVendsProduct : Test
buyingAbovePriceVendsProduct =
    msgTestWithPrecondition "Buying above price vends the product"
        config
        app
        (Fuzz.constant Buy)
        (\model -> model.currentCoins >= model.productPrice)
    <|
        \_ _ finalModel ->
            finalModel.isProductVended
                |> Expect.true "Product should be vended after trying to buy with enough money"

The precondition acts on the "model before specific Msg" (see msgTest docs).

String
-> Config model msg
-> TestedApp model msg
-> (model -> List msg -> model -> Expectation)
-> Test

Tests that a property holds no matter what Msgs we applied.

priceConstant : Test
priceConstant =
    invariantTest "Price is constant"
        config
        app
    <|
        \initModel _ finalModel ->
            finalModel.productPrice
                |> Expect.equal initModel.productPrice

The test function's arguments are:

init model -> random Msgs -> final model

Types

type alias Config model msg =
{ msgToString : msg -> String
, modelToString : model -> String
}

Functions used for showing Msg and Model in the test failures.

type alias TestedApp model msg =
{ model : TestedModel model
, update : TestedUpdate model msg
, msgFuzzer : Fuzzer msg
}

All of these "architecture tests" are going to have something in common: Model, update function and Msgs.

Note that for some tests you can eg. make the Msg fuzzer prefer certain Msgs more if you need to test them more extensively.

type TestedModel model
= ConstantModel model
| FuzzedModel (Fuzzer model)
| OneOfModels (List model)

The strategy for choosing an init model to which the Msgs will be applied.

type TestedUpdate model msg
= BeginnerUpdate (msg -> model -> model)
| NormalUpdate (msg -> model -> ( model, Cmd msg ))

Main applications can be of two types: beginner (no Cmds) and normal (Cmds present).

For custom update functions returning eg. triples etc., just use BeginnerUpdate with a function that returns just the model part of the result:

update : Msg -> Model -> {model : Model, cmd : Cmd Msg, outMsg : OutMsg}


BeginnerUpdate (\msg model -> update msg model |> .model)