lib/smart_city/test_helper.ex

defmodule SmartCity.TestHelper do
  @moduledoc """
  Common utilities for use in test cases
  """

  # no_return() is because eventually() catches ExUnit.AssertionError
  @type asserting_function :: (() -> no_return() | boolean())

  @doc """
  Helper function for asynchronous testing
  - Repeats the given block (zero-arity function) until a timeout occurs or it returns a truthy value.
  - Catches ExUnit assertions and continues to try the block
  - Re-runs block one last time if it times out so assertions are run and throw helpful diffs, etc.

  """
  @spec eventually(asserting_function(), integer, integer) :: :ok | none()
  def eventually(function, dwell \\ 2_000, max_tries \\ 5) do
    case Patiently.wait_for(
           wrap_assertions_as_falsey(function),
           dwell: dwell,
           max_tries: max_tries
         ) do
      :ok ->
        :ok

      _ ->
        case function.() do
          false ->
            raise ExUnit.AssertionError,
                  "no assertion was made in the eventually block but the block evaluated as false"

          result ->
            result
        end
    end
  end

  defp wrap_assertions_as_falsey(function) do
    fn ->
      try do
        function.()
      rescue
        _e in [ExUnit.AssertionError, MatchError] ->
          false
      end
    end
  end
end