Skip to content

Latest commit

 

History

History
154 lines (126 loc) · 3.76 KB

File metadata and controls

154 lines (126 loc) · 3.76 KB

编译时处理

Elixir 允许我们使用一个特殊的模块变量 @before_compile ,这个变量可以用来告诉编译器,在编译结束前还需要执行什么操作。@before_compile 接收一个模块名作为参数,并且还必须定义一个 before_compile 宏。这个宏就会在编译完成前(生成目标代码前)的最后一步被调用,看看代码比如我们继续修改前面的 assertion 代码。

defmodule Assertion do
  defmacro __using__ do
    quote do
      import unquote(__MODULE__)

      Module.register_attribute __MODULE__, :tests, accumulate: true
      @becore_compile unquote(__MODULE__)

    end
  end

  defmacro __before_compile__(_env) do
    quote do
      def run do
        IO.puts "Running tests (#{inspect @tests})"
      end
    end
  end

  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(test_block)
    end
  end
end

简直完美,这样的效果就跟前面的是一样的。

IO.puts "Running tests (#{inspect @tests})" 这句之所以能将 @tests 执行成功,是因为他是定义在 run 内,在真正执行这行代码的时候,早就是运行时了,@tests 也已经被静态替换成了一堆数据了

那最后一步,就是完成我们的 Assertion.Test 模块了。其实这种代码拆分的技巧学着学着就知道该啥时候怎么拆分比较合理了。

defmodule Assertion do
  defmacro __using__(_env) do
    quote do
      import unquote(__MODULE__)

      Module.register_attribute __MODULE__, :tests, accumulate: true
      @before_compile unquote(__MODULE__)

    end
  end

  defmacro __before_compile__(_env) do
    quote do
      def run do
        Assertion.Test.run(@tests, __MODULE__)
      end
    end
  end

  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(test_block)
    end
  end

  # {:==, [context: Elixir, import: Kernel], [5, 5]}
  defmacro assert({operator, _, [lhs, rhs]}) do
    quote bind_quoted: [operator: operator, lhs: lhs, rhs: rhs] do
      Assertion.Test.assert(operator, lhs, rhs)
    end
  end
end


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

  def assert(:==, lhs, rhs) when lhs == rhs do
    :ok
  end
  def assert(:==, lhs, rhs) do
    {:fail, """
    FAILURE:
    Expected:         #{lhs}
    to be equal to:   #{rhs}
    """}
  end

  def assert(:>, lhs, rhs) when lhs > rhs do
    :ok
  end
  def assert(:>, lhs, rhs) do
    {:fail, """
    FAILURE:
    Expected:         #{lhs}
    to be greater than:   #{rhs}
    """}
  end
end

然后我们在尝试我们的测试代码

defmodule MathTest do
  use Assertion

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

  test "integers can be multiplied and divided" do
    assert 5 * 5 == 25
    assert 10 / 2 == 5
  end
end

吓哭几次之后,终于跑起来了

iex(1)> c "assertion.exs"
[Assertion.Test, Assertion]
iex(2)> c "math_test.exs"
[MathTest]
iex(3)> MathTest.run
.==================================
Failure: integers can be added and subtracted
==================================
FAILURE:
Expected:         0
to be equal to:   10


:ok

我们最小的测试框架就这么完工了