Chained testing in Elixir

I am currently walking through the book Functional Web Development, and I wrote this test file:

defmodule GameTest do
  use ExUnit.Case
  alias IslandsEngine.Game

  @player1 "Frank"
  @player2 "Dweezil"

  test "API" do
    IO.puts "start_link/1"
    assert {:ok, game} = Game.start_link(@player1)
    IO.puts "."

    IO.puts "add_player/2"
    Game.add_player(game, @player2)
    state = :sys.get_state(game)
    assert state.player2.name == @player2
    assert state.rules.state  == :has_players
    IO.puts "."

    IO.puts "position_island/1"
    Game.position_island(game, :player1, :square, 1,1)
    state = :sys.get_state(game)
    assert Map.fetch!(state.player1.board, :square)
    IO.puts "."

    assert Game.position_island(game, :player1, :dot, -3,3) == {:error, :invalid_coord}
    IO.puts "+ handles invalid coordinates"

    assert Game.position_island(game, :player1, :wrong, 3,3) == {:error, :invalid_island}
    IO.puts "+ handles invalid islands"
  end
end

I used this structure because each assertion builds off of the last but tests a different function.

I was wondering if there was another way to pass the state/context from one test to the next. setup and setup_all don’t accomplish this.

Worst-case, I could add this to my list of custom macros (for readability). What I have in mind is:

chain "", context0 do
   test "", context0 do
     # ...
   end

   test "", context1 do
     # ...
   end
end

which, under the hood, would do context |> test |> test. Albeit test/2 doesn’t return its context, so I’d need a new macro for that too.

Clearly, the use case is synchronous testing.

I’ve just done this with functions:

test "game works", stuff do
  stuff
  |> add_player1
  |> add_player2
  |> fight!
end
1 Like

I think I’ll still try implementing a chain macro and a step/2 macro – test/2 except it returns the context instead of :ok

Here’s the refactor.

defmodule GameTest do
  use ExUnit.Case
  alias IslandsEngine.{Game, Rules}

  @module "Game, Rules"
  @player1 "Frank"
  @player2 "Dweezil"
  @coupled_island :square
  @coupled_row 1
  @coupled_col 1

  test @module do
    IO.puts "TESTING: " <> @module

    start_link(@player1)
    |> guessing_rule(:player1, 1,1)
    |> add_player(@player2)
    |> place_island(:player1, @coupled_island, @coupled_row, @coupled_col)
    |> invalid_coord(:player1, :dot, -3,3)   # error test
    |> invalid_island(:player1, :wrong, 3,3) # error test
    |> unplaced_islands(:player1)            # error test
    |> set_islands(:player1)           # coupled island
    |> turns_rule
    |> guess_coordinate(:player1, 9,9) # coupled row, col
  end

  # Game
  defp start_link(player) do
    IO.puts "start_link/1"
    assert {:ok, game} = Game.start_link(player)
    IO.puts "."
    game
  end

  defp add_player(game, player) do
    IO.puts "add_player/2"
    Game.add_player(game, player)
    state = :sys.get_state(game)
    assert player = state.player2.name
    assert :has_players = state.rules.state
    IO.puts "."
    game
  end

  # DRY?
  defp place_island(game, player, island, row, col) do
    IO.puts "place_island/1"
    assert :ok = Game.place_island(game, player, island, row, col)
    state = :sys.get_state(game)
    assert Map.fetch!(state.player1.board, island)
    IO.puts "."
    game
  end
  defp invalid_coord(game, player, island, row, col) do
    assert {:error, :invalid_coord} = Game.place_island(game, player, island, row, col)
    IO.puts "+ handles invalid coordinates"
    game
  end
  defp invalid_island(game, player, island, row, col) do
    assert {:error, :invalid_island} = Game.place_island(game, player, island, row, col)
    IO.puts "+ handles invalid islands"
    game
  end

  defp unplaced_islands(game, player) do
    IO.puts "set_islands/2"
    assert {:error, :unplaced_islands} = Game.set_islands(game, player)
    IO.puts "+ handles unplaced islands"
    game
  end

  defp set_islands(game, player) do
    setup_board(game, player)

    assert {:ok, _board} = Game.set_islands(game, player)
    state = :sys.get_state(game)
    assert :has_players = state.rules.state
    IO.puts "."
    game
  end
  defp setup_board(game, player) do
    Game.place_island(game, player, :atoll, 1,3)
    Game.place_island(game, player, :dot, 2,3)
    Game.place_island(game, player, :L, 1,5)
    Game.place_island(game, player, :S, 4,1)
  end

  defp guess_coordinate(game, player, row, col) do
    IO.puts "guess_coordinate/4"
    assert {:miss, :none, false} = Game.guess_coordinate(game, player, row, col)
    assert :error = Game.guess_coordinate(game, player, row, col)
    assert {:hit, :none, false} = Game.guess_coordinate(game, Game.opponent(player), @coupled_row, @coupled_col)
    IO.puts "."
    game
  end

  # Rules
  defp guessing_rule(game, player, row, col) do
    IO.puts "Rule: can't guess during board-setting phase"
    assert :error = Game.guess_coordinate(game, player, row, col)
    IO.puts "."
    game
  end

  defp turns_rule(game) do
    IO.puts "Rule: can't place island during turn-taking phase"
    new_state = :sys.replace_state(game, fn state ->
      %{state | rules: %Rules{state: {:turn, :player1} }}
    end)
    assert {:turn, :player1} = new_state.rules.state
    assert :error = Game.place_island(game, :player1, :dot, 5,5)
    IO.puts "."
    game
  end
end

My suggested macros would remove boilerplate IO.putss and terminating games, greatly improving readability. Additionally, the current state could be stored there, saving a call or two of :sys.get_state/1