# `Finitomata.ExUnit`
[🔗](https://github.com/am-kantox/finitomata/blob/v0.35.0/lib/finitomata/test/ex_unit.ex#L1)

Helpers and assertions to make `Finitomata` implementation easily testable.

## Testing with `Finitomata.ExUnit`

There are several steps needed to enable extended testing with `Finitomata.ExUnit`.

In the first place, `mox` dependency should be included in your `mix.exs` project file

```elixir
{:mox, "~> 1.0", only: [:test]}
```

Then, the `Finitomata` declaration should include a listener. If you already have the
  listener, it should be changed to `Mox` in `:test` environment, and the respecive `Mox`
  should be defined somewhere in `test/support` or like

```elixir
@listener (if Mix.env() == :test, do: MyFSM.Mox, else: MyFSM.Listener)
use Finitomata, fsm: @fsm, listener: @listener
# or
# use Finitomata, fsm: @fsm, listener: {:mox, MyFSM.Listener}
```

If you don’t have an actual listener, the special `:mox` value for `listener` would do
  everything, including an actual `Mox` declaration in `test` environment.

```elixir
use Finitomata, fsm: @fsm, listener: :mox
```

The last thing you need, `import Mox` into your test file which also does
  `import Finitomata.ExUnit`. That’s it, now your code is ready to use `Finitomata.ExUnit`
  fancy testing.

## Example

Consider the following simple FSM

```elixir
defmodule Turnstile do
  @fsm ~S[
    ready --> |on!| closed
    opened --> |walk_in| closed
    closed --> |coin_in| opened
    closed --> |switch_off| switched_off
  ]
  use Finitomata, fsm: @fsm, auto_terminate: true

  @impl Finitomata
  def on_transition(:opened, :walk_in, _payload, state) do
    {:ok, :closed, update_in(state, [:data, :passengers], & &1 + 1)}
  end
  def on_transition(:closed, :coin_in, _payload, state) do
    {:ok, :opened, state}
  end
  def on_transition(:closed, :off, _payload, state) do
    {:ok, :switched_off, state}
  end
end
```

Of course, in the real life, one would not only collect the total number of passengers passed
  in the state, but also validate the coin value to let in or fail a transition, but
  for the demonstration purposes this one is already good enough.

We now want to test it works as expected. Without `Finitomata.ExUnit`, one would
  write the test like below

```elixir
# somewhere else → Mox.defmock(Turnstile.Mox, for: Finitomata.Listener)
test "standard approach" do
  start_supervised(Finitomata.Supervisor)

  fini_name = "Turnstile_1"
  fsm_name = {:via, Registry, {Finitomata.Registry, fini_name}}

  Finitomata.start_fsm(Turnstile, fini_name, %{data: %{passengers: 0}})

  Finitomata.transition(fini_name, :coin_in)
  assert %{data: %{passengers: 0}}} = Finitomata.state(Turnstile, "Turnstile_1", :payload)

  Finitomata.transition(fini_name, :walk_in)
  assert %{data: %{passengers: 1}}} = Finitomata.state(Turnstile, "Turnstile_1", :payload)

  Finitomata.transition(fini_name, :switch_off)

  Process.sleep(200)
  refute Finitomata.alive?(Turnstile, "Turnstile_1")
end
```

At the first glance, there is nothing wrong with this approach, but it requires
  an enormous boilerplate, it cannot check it’s gone without using `Process.sleep/1`,
  but most importantly, it does not allow testing intermediate states.

If the FSM has instant transitions (named with a trailing bang, like `foo!`) which
  are invoked automatically by `Finitomata` itself, there is no way to test intermediate
  states with the approach above.

OK, let’s use `Mox` then (assuming `Turnstile.Mox` has been declared and added
  as a listener in test environment to `use Finitomata`)

```elixir
# somewhere else → Mox.defmock(Turnstile.Mox, for: Finitomata.Listener)
test "standard approach" do
  start_supervised(Finitomata.Supervisor)

  fini_name = "Turnstile_1"
  fsm_name = {:via, Registry, {Finitomata.Registry, fini_name}}
  parent = self()

  Turnstile.Mox
  |> allow(parent, fn -> GenServer.whereis(fsm_name) end)
  |> expect(:after_transition, 4, fn id, state, payload ->
    parent |> send({:on_transition, id, state, payload}) |> then(fn _ -> :ok end)
  end)

  Finitomata.start_fsm(Turnstile, fini_name, %{data: %{passengers: 0}})

  Finitomata.transition(fini_name, :coin_in)
  assert_receive {:on_transition, ^fsm_name, :opened, %{data: %{passengers: 0}}}
  # assert %{data: %{passengers: 0}}} = Finitomata.state(Turnstile, "Turnstile_1", :payload)

  Finitomata.transition(fini_name, :walk_in)
  assert_receive {:on_transition, ^fsm_name, :closed, %{data: %{passengers: 1}}}
  # assert %{data: %{passengers: 1}}} = Finitomata.state(Turnstile, "Turnstile_1", :payload)

  Finitomata.transition(fini_name, :switch_off)
  assert_receive {:on_transition, ^fsm_name, :switched_off, %{data: %{passengers: 1}}}

  Process.sleep(200)
  refute Finitomata.alive?(Turnstile, "Turnstile_1")
end
```

That looks better, but there is still too much of boilerplate. Let’s see how it’d look like
  with `Finitomata.ExUnit`.

```elixir
describe "Turnstile" do
  setup_finitomata do
    parent = self()
    initial_passengers = 42

    [
      fsm: [implementation: Turnstile, payload: %{data: %{passengers: initial_passengers}})],
      context: [passengers: initial_passengers]
    ]
  end

  test_path "respectful passenger", %{passengers: initial_passengers} do
    :coin_in ->
      assert_state :opened do
        assert_payload do
          data.passengers ~> ^initial_passengers
        end
      end

    :walk_in ->
      assert_state :closed do
        assert_payload do
          data.passengers ~> one_more when one_more == 1 + initial_passengers
        end
      end

    :switch_off ->
      assert_state :switched_off
      assert_state :*
  end
```

With this approach, one could test the payload in the intermediate states, and validate
  messages received from the FSM with `assert_receive/3`.

No other code besides `assert_state/2`, `assert_payload/1`, and `ExUnit.Assertions.assert_receive/3` is
  permitted to fully isolate the FSM execution from side effects.

## Custom environments

In the bigger application, it might be not convenient to declare mocks for
  each and every case when `Finitomata`/`Infinitomata` might have been called under the hood.

For such cases, one might pass `mox_envs: :finitomata` to an FSM declaration,
  or set such a config options as `config :finitomata, :mox_envs, :finitomata`. That would result
  in mocks implemented for `listener: :mox` in this environment(s) _only_.

Then the tests should have been split into two groups assuming the finitomata tests
  were generated with the mix task (see below)

```
mix test --exclude finitomata
MIX_ENV=finitomata mix test --exclude test --include finitomata
```

Don’t forget to add `:finitomata` env to the list of envs where `mox` is installed

## Test Scaffold Generation

> ### `mix` tasks to simplify testing {: .info}
>
> One might generate the tests scaffold for all possible paths in the FSM with a `mix` task
>
>  ```bash
>  mix finitomata.generate.test --module MyApp.FSM
>  ```
>
> besides the mandatory `--module ModuleWithUseFinitomata` argument, it also accepts
> `--dir` and `--file` arguments (defaulted to `test/finitomata` and
> `Macro.underscore(module) <> "_test.exs`) respectively.)

# `assert_payload`

Asserts the payload within `test_path/3` and `assert_transition/3`.

```elixir
assert_payload do
  counter ~> 42
  user.id ~> ^user_id # assuming `user_id` variable is in context
end
```

# `assert_state`

Asserts the state within `test_path/3` context.

Typically, one would assert the state and the payload within it as shown below
```elixir
assert_state :idle do
  assert_payload do
    data.counter ~> value when is_integer(value)
    data.listener ~> ^pid # assuming `pid` variable is in context
  end
end
```

# `assert_transition`
*macro* 

Convenience macro to assert a transition initiated by `event_payload`
  argument on the _FSM_ defined by the test context previously setup
  with a call to `setup_finitomata/1`.

Last regular argument in a call to `assert_transition/3` would be an
  `event_payload` in a form of `{event, payload}`, or just `event`
  for no payload.

`to_state` argument would be matched to the resulting state of the transition,
  and `block` accepts validation of the `payload` after transition in a form of

```elixir
test "some", ctx do
  assert_transition ctx, {:increase, 1} do
    :counted ->
      assert_payload do
        user_data.counter ~> 2
        internals.pid ~> ^parent
      end
      # or: assert_payload %{user_data: %{counter: 2}, internals: %{pid: ^parent}}

      assert_receive {:increased, 2}
  end
end
```

Any matchers should be available on the right side of `~>` operator in the same way as the first
  argument of [`match?/2`](https://hexdocs.pm/elixir/Kernel.html#match?/2).

Each argument might be matched several times.

```elixir
  ...
  assert_payload do
    user_data.counter ~> {:foo, _}
    internals.pid ~> pid when is_pid(pid)
  end
```

# `assert_transition`
*macro* 

> This macro is deprecated. Use `assert_transition/3` instead.

Convenience macro to assert a transition initiated by `event_payload`
  argument on the _FSM_ defined by first three arguments.

**NB** it’s not recommended to use low-level helpers, normally one should
  define an _FSM_ in `setup_finitomata/1` block and use `assert_transition/3`
  or even better `test_path/3`.

```elixir
parent = self()

assert_transition id, impl, name, {:increase, 1} do
  :counted ->
    assert_payload do
      user_data.counter ~> 2
      internals.pid ~> ^parent
    end
    # or: assert_payload %{user_data: %{counter: 2}, internals: %{pid: ^parent}}
    assert_receive {:increased, 2}
end
```

_See:_ `assert_transition/3` for examples of matches and arguments

# `init_finitomata`
*macro* 

> This macro is deprecated. Use `setup_finitomata/1` instead.

This macro initiates the _FSM_ implementation specified by arguments passed.

**NB** it’s not recommended to use low-level helpers, normally one should
  define an _FSM_ in `setup_finitomata/1` block, which would initiate
  the _FSM_ amongs other things.

_Arguments:_

- `id` — a `Finitomata` instance, carrying multiple _FSM_s
- `impl` — the module implementing _FSM_ (having `use Finitomata` clause)
- `name` — the name of the _FSM_
- `payload` — the initial payload for this _FSM_
- `options` — the options to control the test, such as
  - `transition_count` — the number of expectations to declare (defaults to number of states)

Once called, this macro will start `Finitomata.Suprevisor` with the `id` given,
  define a mox for `impl` unless already efined,
  `Mox.allow/3` the _FSM_ to call testing process,
  and expectations as a listener to `after_transition/3` callback,
  sending a message of a shape `{:on_transition, id, state, payload}` to test process.

Then it’ll start _FSM_ and ensure it has entered `Finitomata.Transition.entry/2` state.

# `setup_finitomata`
*macro* 

Setups `Finitomata` for testing in the case and/or in `ExUnit.Case.describe/2` block.

It would effectively init the _FSM_ with an underlying call to `init_finitomata/5`,
  and put `finitomata` key into `context`, assigning `:test_pid` subkey to the `pid`
  of the running test process, and mixing `:context` content into test context.

Although one might pass the name, it’s more convenient to avoid doing it, in this case
  the name would be assigned from the test name, which guarantees uniqueness of
  _FSM_s running in concurrent environment.

It should return the keyword which would be validated with `NimbleOptions` schema

* `:fsm` (non-empty `t:keyword/0`) - Required. The _FSM_ declaration to be used in tests.

  * `:id` (`t:term/0`) - The ID of the `Finitomata` tree. The default value is `nil`.

  * `:implementation` - Required. The implementatoin of `Finitomata` (the module with `use Finitomata`.)

  * `:name` (`t:String.t/0`) - The name of the `Finitomata` instance.

  * `:payload` (`t:term/0`) - Required. The initial payload for the _FSM_ to start with.

  * `:options` (`t:keyword/0`) - Additional options to use in _FSM_ initialization. The default value is `[]`.

    * `:transition_count` (`t:non_neg_integer/0`) - The expected by `Mox` number of transitions to handle.

* `:mocks` (list of `t:atom/0`) - Additional mocks to be passed to the test. The default value is `[]`.

* `:context` (`t:keyword/0`) - The additional context to be passed to actual `ExUnit.Callbacks.setup/2` call. The default value is `[]`.

_Example:_

```elixir
describe "MyFSM tests" do
  setup_finitomata do
    parent = self()

    [
      fsm: [implementation: MyFSM, payload: %{}],
      context: [parent: parent]
    ]
  end

  …
```

# `test_path`
*macro* 

Convenience macro to test the whole _Finitomata_ path,
  from starting to ending state.

Must be used with a `setup_finitomata/1` callback.

_Example:_

```elixir
  test_path "The only path", %{finitomata: %{test_pid: parent}} do
    {:start, self()} ->
      assert_state :started do
        assert_payload do
          internals.counter ~> 1
          pid ~> ^parent
        end

        assert_receive {:on_start, ^parent}
      end

    :do ->
      assert_state :done do
        assert_receive :on_do
      end

      assert_state :* do
        assert_receive :on_end
      end
  end
```

---

*Consult [api-reference.md](api-reference.md) for complete listing*
