ArcBlock presents Elixir ExUnit. Learn how to write a unit test for Elixir code? This talk will give an introduction to ExUnit in Elixir, what components are included in it and how to use them.
2. What
From wiki
In computer programming, unit testing is a software testing method by which
individual units of source code, sets of one or more computer program modules
together with associated control data, usage procedures, and operating
procedures, are tested to determine whether they are fit for use.
Unit testing simply verifies that individual units of code work as expected.
“
2
3. Why
• improve code quality
• self-confidence
• assurance for modifing
• learning by test cases
3
5. Entrance
start
Mix will load the test_helper.exs file before executing the tests.
# test/test_helper.exs
ExUnit.start()
5
6. Cases
options
:async - configures tests in this module to run concurrently with tests in other modules. Tests
in the same module do not run concurrently. It should be enabled only if tests do not change
any global state. Defaults to false.
use ExUnit.Case, async: true
“
6
8. Cases
context
All tests receive a context as an argument. There are some ways to build the context before
run all test cases in individual test module.
with context:
test "unit test case with context", context do
IO.inspect(context, label: "The context is:")
assert true
end
8
10. Assertions
assert
test "unit test assert" do
assert true
assert 1
assert ""
assert !nil
assert !false
assert %{} = %{a: :a, b: :b}
assert %{} != %{a: :a, b: :b}
end
10
11. Assertions
assert_raise
test "unit test assert_raise" do
assert_raise ArithmeticError, "bad argument in arithmetic expression", fn -> 1 + :a end
assert_raise RuntimeError, fn -> raise "oops" end
end
11
12. Assertions
assert_receive(d)
test "unit test assert_receive" do
main_pid = self()
spawn(fn ->
Process.sleep(1_000)
send(main_pid, {:ok, self()})
end)
assert_receive {:ok, _}, 2_000
end
test "unit test assert_received" do
send(self(), {:ok, self()})
assert_received {:ok, _}
end
12
15. Callbacks
There are many usual callbacks for ExUnit:
• setup
• setup_all
• on_exit
• start_supervised
• stop_supervised
15
16. Callbacks
purpose
Build context for test case.
The context is a map, can be used in each test case.
test "unit test case with context", context do
IO.inspect(context, label: "The context is:")
assert true
end
16
17. Callbacks
setup
• Optionally receive a map with test state and metadata.
• All setup callbacks are run before each test.
• Return keyword list / map / {:ok, keywords | map} / :ok .
17
18. Callbacks
setup
setup do
# IO.puts("Generate random number 1 in setup")
[random_num_1: Enum.random(1..1_000)]
end
setup :generate_random_number_2
defp generate_random_number_2(context) do
# IO.puts("Generate random number 2 in setup")
context
|> Map.put(:random_num_2, Enum.random(1..1_000))
end
test "setup case 1", %{random_num_1: random_num_1, random_num_2: random_num_2} = _context do
IO.puts("The random numbers in case #1 are: {#{random_num_1}, #{random_num_2}}")
assert true
end
test "setup case 2", %{random_num_1: random_num_1, random_num_2: random_num_2} = _context do
IO.puts("The random numbers in case #2 are: {#{random_num_1}, #{random_num_2}}")
assert true
end
18
19. Callbacks
setup_all
• Optionally receive a map with test state and metadata.
• Return keyword list / map / {:ok, keywords | map} / :ok .
• Invoked only once per module.
19
20. Callbacks
setup_all
setup_all do
[setup_all_random_num_1: Enum.random(1..1_000)]
end
setup_all :generate_setup_all_random_number_2
defp generate_setup_all_random_number_2(context) do
context
|> Map.put(:setup_all_random_num_2, Enum.random(1..1_000))
end
test "setup_all case 1", %{setup_all_random_num_1: num_1, setup_all_random_num_2: num_2} do
IO.puts("The random numbers in case setup_all #1 are: {#{num_1}, #{num_2}}")
end
test "setup_all case 2", %{setup_all_random_num_1: num_1, setup_all_random_num_2: num_2} do
IO.puts("The random numbers in case setup_all #2 are: {#{num_1}, #{num_2}}")
end
20
22. Mock
mock one module with test
test "unit test mock with one mocked module" do
with_mock(ModuleA, cross_gwf: fn -> :ok end, kill_gwf: fn _ -> true end) do
assert :ok == ModuleA.cross_gwf()
assert true == ModuleA.kill_gwf(nil)
end
end
22
23. Mock
mock one module with test_with_mock
test_with_mock "unit test mock using macro test_with_mock", ModuleA, [],
cross_gwf: fn -> :ok end,
kill_gwf: fn _ -> true end do
assert :ok == ModuleA.cross_gwf()
assert true == ModuleA.kill_gwf(nil)
end
23
24. Mock
mock multi-mocked modules
test "unit test mock with multi-mocked modules" do
with_mocks([
{ModuleA, [], [cross_gwf: fn -> :ok end, kill_gwf: fn _ -> true end]},
{ModuleB, [], [cross_gwf: fn -> :ok end, kill_gwf: fn _ -> true end]}
]) do
assert :ok == ModuleA.cross_gwf()
assert true == ModuleA.kill_gwf(nil)
assert :ok == ModuleB.cross_gwf()
assert true == ModuleB.kill_gwf(nil)
end
end
24
25. Mock
use callback setup_with_mocks
setup_with_mocks([
{ModuleA, [], [cross_gwf: fn -> 1 end, kill_gwf: fn _ -> 2 end]},
{ModuleB, [], [cross_gwf: fn -> 1 end, kill_gwf: fn _ -> 2 end]}
]) do
:ok
end
test "unit test mock use setup with setup with mocks modulea" do
assert 1 == ModuleA.cross_gwf()
assert 2 == ModuleA.kill_gwf(nil)
end
test "unit test mock use setup with setup with mocks moduleb" do
assert 1 == ModuleB.cross_gwf()
assert 2 == ModuleB.kill_gwf(nil)
end
25
26. Reversed to codes
reasonable input and output for one function
test "unit test for reversed to codes" do
assert :"1" == LearnExunit.to_to_atom(build_params_for_func("1"))
assert :a == LearnExunit.to_to_atom(build_params_for_func("a"))
assert :"1" == LearnExunit.to_atom("1")
assert :a == LearnExunit.to_atom("a")
end
26
27. Reversed to codes
well-de ned boundaries
defp login_by_email(user, %{password: password} = args) do
login_verify_passwd(is_correct_password?(password), user, args)
end
@doc false
defp login_verify_passwd(true, %{if_mfa: false} = user, args) do
after_login_successfully(user, args, "email", %{})
end
defp login_verify_passwd(true, user, _args) do
{:ok, %{user: user, if_need_verify_mfa: true}}
end
defp login_verify_passwd(false, _, _) do
{:error, %{message: "Password is incorrect.", status: 400}}
end
27