The Problem
I am working on my first side project using Elixir and Phoenix. I was writing some tests and encountered a flaky unit test (randomly fails or passes). In this test, I wanted to make a case differentiation, but today I learned (TIL) in Elixir, the =
behaves a bit differently. It is not an assignment operator; it is the match operator.
Pattern matching
When you use =
, Elixir tries to match the value on the right side of the =
with the pattern on the left side. If the pattern on the left does not match the value on the right, an error is raised.
1
2
3
x = 42 # Matches and binds the value 42 to the variable x
{a, b} = {1, 2} # Matches and binds 1 to a, and 2 to b
[head | tail] = [1, 2, 3] # Matches and binds head to 1, tail to [2, 3]
The pin operator ^
is used to assert that a variable on the left side of the pattern should match the value on the right side. It prevents the introduction of a new variable with the same name.
1
2
x = 42
^x = 42 # Matches because the value on the right matches the existing variable x
Without the pin operator ^
, the second line would introduce a new variable named x
rather than asserting that it should match the existing x
.
The Code
In the given code snippet, we have a example test case that involves generating a new game response. The response includes information about the winner and loser of the game, identified by their respective user IDs.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
defmodule ExampleTest do
use ExUnit.Case
def get_new_game_response() do
random_number = :rand.uniform(10)
if random_number <= 5 do
%{
"winner" => %{"user_id" => "foo"},
"looser" => %{"user_id" => "bar"}
}
else
%{
"winner" => %{"user_id" => "bar"},
"looser" => %{"user_id" => "foo"}
}
end
end
test "new game response flacky" do
player_1 = "foo"
player_2 = "bar"
response = get_new_game_response()
case response["winner"]["user_id"] do
player_1 ->
assert response["looser"]["user_id"] =~ player_2
player_2 ->
assert response["looser"]["user_id"] =~ player_1
_ ->
raise "Unexpected user_id"
end
end
test "new game response with pin operator" do
player_1 = "foo"
player_2 = "bar"
response = get_new_game_response()
case response["winner"]["user_id"] do
^player_1 ->
assert response["looser"]["user_id"] =~ player_2
^player_2 ->
assert response["looser"]["user_id"] =~ player_1
_ ->
raise "Unexpected user_id"
end
end
end
Pattern Matching Without Pin Operator
Let’s look at the original version of the test case without using the pin operator:
1
2
3
4
5
6
7
8
9
10
case response["winner"]["user_id"] do
player_1 ->
assert response["looser"]["user_id"] =~ player_2
player_2 ->
assert response["looser"]["user_id"] =~ player_1
_ ->
raise "Unexpected user_id"
end
In this version, we attempt to match the user IDs of the winners (player_1 and player_2) and take corresponding actions. However, this code has a subtle issue that might not be immediately apparent.
A new variable binding occurs as this variable is used more than once in the same pattern. In our original code, both player_1
and player_2
are used as variables in the case
pattern. So always the first case is executed, which randomly fails if player_2
is the winner but passes if not.
Using the Pin Operator
The pin operator (^) in Elixir is used to enforce a match against an existing variable’s value, preventing variable rebinding. Let’s revisit the modified test case that includes the pin operator:
1
2
3
4
5
6
7
8
9
10
case response["winner"]["user_id"] do
^player_1 ->
assert response["looser"]["user_id"] =~ player_2
^player_2 ->
assert response["looser"]["user_id"] =~ player_1
_ ->
raise "Unexpected user_id"
end
By using the pin operator, we explicitly state that we want to match the value of player_1 and player_2 against the corresponding user IDs without introducing new bindings. This prevents any unintentional variable rebinding and ensures that our pattern matching behaves as expected.