small medium large xlarge

Generic-user-small
10 Jun 2015, 04:45
Daniel Perez (1 post)

Hi, I just started reading Metaprogramming Elixir, and I find it very interesting. I just finished chapter 2 but I am a bit confused with the final code.

  test "integers can be added and subtracted" do
    assert 1 + 1 == 2
    assert 2 + 3 == 5
    assert 5 - 5 == 10
  end

fails, but

  test "integers can be added and subtracted" do
    assert 1 + 1 == 2
    assert 2 - 3 == 5
    assert 5 + 5 == 10
  end

seems to pass normally. This actually seems natural, as I would expect unquote(test_block) in the test macro to return the result of the last statement of the block. So in the first example, it will return {:fail, "reason"} as expected, but in the second, it will return :ok and the {:fail, "reason"} returned by the second assertion will be ignored. This seems to be what #78424 and #78132 are about in the errata, but am I missing something here?

I tried to implement the test so that it fails properly, here is what I came up with. I am still quite new to Elixir, and even newer to metaprogramming in Elixir, so I would like to know if there is any more idiomatic or cleaner way to write it.

# Assertion module

  defmacro test(description, do: test_block) do
    test_func = String.to_atom(description)
    quote do
      @tests { unquote(test_func), unquote(description) }
      def unquote(test_func)() do
        unquote(elem(test_block, 2))
      end
    end
  end

# Assertion.Test module

  def run(tests, module) do
    Enum.each tests, fn {test_func, description} ->
      assertions = apply(module, test_func, [])
      Enum.each assertions, fn (assertion) ->
        case assertion do
          :ok -> IO.write(".")
          {:fail, reason} -> IO.puts """
          ===============================================
          FAILURE: #{description}
          ===============================================
          #{reason}
          """
        end
      end
    end
  end

Thank you.

Daniel

Generic-user-small
30 Nov 2015, 03:58
Sean Tan (1 post)

Seems that the code presented in the book only considers the result returned by the last assert.

To fix that, instead of returning the assert failure as a function result, change it to throw an exception of the form {:fail, reason}, and catch it in the run function.

defmodule Assertion.Test do

  def run(tests, module) do
    {time, {oks, fails}} = :timer.tc(fn ->
      for {test_fun, description} <- tests do
        Task.async(fn ->
          try do
            case apply(module, test_fun, []) do
              :ok ->
                IO.write "."
                :ok
              _ -> :none
            end
          catch
            {:fail, reason} ->
              IO.write """

              =================================
              Failure: #{description}
              =================================
              #{reason}
              """
              :fail
          end
        end)
      end
      |> Enum.map(&Task.await/1)
      |> Enum.reduce({0, 0}, fn(result, {oks, fails}=acc) ->
        case result do
          :ok -> {oks+1, fails}
          :fail -> {oks, fails+1}
          _ -> acc
        end
      end)
    end)
    IO.puts "\nPasses: #{oks}, Failures: #{fails}, Duration: #{time/1000}(s)"
  end

  def assert(:==, lhs, rhs) do
    cond do
      lhs == rhs ->
        :ok
      true ->
        throw {:fail,
        """
        Expected: #{lhs}
        to be equal to: #{rhs}
        """}
    end
  end

end
You must be logged in to comment