我试图避免冗余测试(这意味着测试始终以相同的方式进行,但参数略有不同)。例如,当我想测试对控制器的调用是否正常时,我总是会遇到这样的情况:
setup do
count = 3
end
test "responds with right schema", %{conn: conn, swagger_schema: schema} do
conn
|> create_authenticated_conn()
|> get(~p"/something", %{a: 1})
|> validate_resp_schema(schema, "Schema")
|> json_response(200)
|> assert_length(count)
end
改变的事情是方法调用(get、post…)、参数、要评估的模式,以及可选的如果我想做出更多断言(如检查结果数组的长度)。
因此,我想到创建一个宏来简化所有这些工作,参数化需要参数化的值,并允许 do: block 元素添加额外的检查。我最终得到的是这个:
defmacro test_ok(schema_name, method, url, params \\ nil, do: block) do
quote do
test "responds with right schema", %{conn: conn, swagger_schema: schema} do
response =
conn
|> create_authenticated_conn()
|> unquote(method)(unquote(url), unquote(params))
|> validate_resp_schema(schema, unquote(schema_name))
|> json_response(200)
evaluate_response(response, block)
end
defp evaluate_response(var!(response), block) do
unquote(block)
end
end
end
如果我忽略响应部分和块部分(evaluate_response 方法),则一切正常。一旦我想将块部分添加到宏并将响应传递到主体内部,我就会遇到两个问题。首先,我们举一个代码示例:
test_ok("Schema", :get, ~p"/something") do
assert_length(response, count)
end
那么,第一个问题是块代码内的响应无法被识别。另一个是 count 也不可用,尽管以正常测试宏的方式可以完美识别。
我的错误是什么?
Kernel.var!/2
不会神奇地使参数全局可见。这只会让它不卫生。这意味着如果您在 quote/2
块内为其分配一个值,则调用者可以看到它。在您的情况下,您明确地将 response
作为第一个参数传递给对 evaluate_response/2
的调用,因此 var!/2
是无操作。evaluate_response/2
的调用中,您传递一个未定义的局部 block
变量作为第二个参数,该参数在实现中被忽略。块外部的整个上下文在内部不可用,这正是宏观卫生的本质。在 evaluate_response/2
的正文中,您直接取消引用 block
,它已被传递给 test_ok/5
的参数。count
应该在任何地方可用,setup/2
块应该返回映射/关键字列表,而不是定义局部变量。test/2
调用(相信我,ExUnit.Case.test/3
在引擎盖下做了很多黑暗魔法,您将失去包装它。)相反,包装 response
评估为 quote do: var!(response) = ...
,然后使用此响应变量调用其他内容(由于 var!
,这会从宏内部泄漏。