Would somebody please explain the said lines in this test?

Hi Everyone!

I am using Elixir for some months now, and I know about pattern matching and all. Right now I’m using the book series (Phoenix inside out) by Shankar Dhanasekaran.

At one step there is the test file called catalog_test.exs with the following code

defmodule Mango.CatalogTest do
  use ExUnit.Case
  alias Mango.Catalog
  alias Mango.Catalog.Product

  test "list_products/0 returns all products" do
    [p1, p2] = Catalog.list_products
    
    assert %Product{} = p1
    assert p1.name == "Tomato"
    assert %Product{} = p2
    assert p2.name == "Apple"
  end
end

this file tests catalog.ex which displays two products (structs) as follows

defmodule Mango.Catalog do
  alias Mango.Catalog.Product
  
  def list_products do
    product1 = %Product{ name: "Tomato", price: 50 }
    product2 = %Product{ name: "Apple", price: 100 }
    [product1, product2]
  end
end

the struct is defined at product.ex as

defmodule Mango.Catalog.Product do
  defstruct [:name, :price]
end

when I run the test as mix test test/mango/catalog/catalog_test.exs it works fine, which is great, but when I change the following two lines in the test files

assert %Product{} = p1
assert %Product{} = p2

to

assert p1 = %Product{}
assert p2 = %Product{}

it fails and gives the following output, but I was expecting it to pass

warning: variable "p1" is unused
  test/mango/catalog/catalog_test.exs:7

warning: variable "p2" is unused
  test/mango/catalog/catalog_test.exs:7



  1) test list_products/0 returns all products (Mango.CatalogTest)
     test/mango/catalog/catalog_test.exs:6
     Assertion with == failed
     code:  assert p1.name() == "Tomato"
     left:  nil
     right: "Tomato"
     stacktrace:
       test/mango/catalog/catalog_test.exs:10: (test)



Finished in 0.07 seconds
1 test, 1 failure

Randomized with seed 917119

Would somebody please explain why changing code in those two lines that way won’t work?

use == not = in assert

(Update)

When you use = then it’s binding with pattern match. You may use that for test but in that case you don’t need to use assert since it faills on unmatch anyway.

(Update 2)

Check out what = means in Elixir - https://elixir-lang.org/getting-started/pattern-matching.html

1 Like

In the book the said snippet is written as

test "list_products/0 returns all products" do
    [p1, p2] = Catalog.list_products
    
    assert %Product{} = p1
    assert p1.name == "Tomato"
    assert %Product{} = p2
    assert p2.name == "Apple"
end

So you mean if I use == it will work as follows ?

assert p1 == %Product{}
assert p2 == %Product{}

Those lines are asserting that p1 is in fact a %Product{}, if you reverse it it says p1 is now an empty %Product{}.

That’s why p1.name == "Tomato" fails, because p1 is an empty %Product{}.

Learn more here: https://elixir-lang.org/getting-started/pattern-matching.html

Oh, O, so I’m actually assigning p1 to %Product{} (not checking, but assigning to the new empty Product struct) and that’s why the next very line is getting failed ?
But when I used it with ==, it still failed.

It fails because p1 in that context is nothing.

assert %Product{} = p1
assert %Product{} = p2

is not equivalent to

assert p1 = %Product{}
assert p2 = %Product{}

In the first ones, it is is checking that p1 and p2 both match to a Product struct. In the second ones, p1 and p2 are being assigned to empty Product structs. When the check comes around during the test, p1.name is nil, which is why your test is failing.

but one line above p1 was assigned to a value (struct) from list_products function as,

[p1, p2] = Catalog.list_products

(I’m sorry if I sound stupid, but I’m not really getting the point).

But if that’s the case == should pass the test.

p1 and p2 are not even used, see the exception:

When I change = to == in the said two lines, the output becomes as follows (the warnings go away)

  1) test list_products/0 returns all products (Mango.CatalogTest)
     test/mango/catalog/catalog_test.exs:6
     Assertion with == failed
     code:  assert p1 == %Product{}
     left:  %Mango.Catalog.Product{name: "Tomato", price: 50}
     right: %Mango.Catalog.Product{name: nil, price: nil}
     stacktrace:
       test/mango/catalog/catalog_test.exs:9: (test)



Finished in 0.05 seconds
1 test, 1 failure

Randomized with seed 615984

Update: with == it fails both the ways.

Pattern matching is a little tricky to grok when you first get into Elixir. But if you notice the keyword values, they DON’T match.

left:  %Mango.Catalog.Product{name: "Tomato", price: 50}
right: %Mango.Catalog.Product{name: nil, price: nil}

Check the calling code :wink:

assert p1 == %Product{}

p1 is a full struct with data. On the right it’s just an empty husk. :slight_smile:

with double equals (==) it fails both the ways.

assert %Product{} == p1
or
assert p1 == %Product{}

In both cases %Product{} is %Mango.Catalog.Product{name: nil, price: nil}.

and @chulkilee said it’s better to use == with assert.

@shankardevy @chrismccord would you please help me grasp this idea? I promise I’ve studied how the pattern matching actually work, but here I’m stuck.

Take a look at a simpler example of pattern matching, this might make things a little clearer:

iex(5)> a = %{one: 1, two: 2, three: 3}
%{one: 1, three: 3, two: 2}
iex(6)> %{} = a
%{one: 1, three: 3, two: 2}
iex(7)> %{one: one} = a
%{one: 1, three: 3, two: 2}
iex(8)> one
1
iex(9)> %{one: one} == a
false
iex(10)> %{} == a        
false

== works similar to other languages but = is only on the surface similar in some contexts, if you keep in mind that it’s not an assignment it will be clearer and clearer to the point where assignment gets weird :slight_smile:

Say %{} = a is “a map matches the thing that variable a containts”, %{one: one} = a "match key one to the thing in variable a and assign its contents to variable one", %{one: one} == a and this one is plain old "%{one: one} equals the thing in variable a", a boolean comparison.

Thank you @yurko!
The thing you explained in the reply was already clear to me, but why swaping positions of %Product{} with p1/p2 doesn’t work, is still not clear to me.

[p1, p2] = Catalog.list_products

Catalog.list_products returns a list with two items [%Product{name: "Tomato"}, %Product{name: "Apple"}]. In the line above it matches the first %Product{} of the list to p1 and the second %Product{} to p2. Now we have

p1 is %Product{name: "Tomato"}
p2 is %Product{name: "Apple"}

assert %Product{} = p1
assert %Product{} = p2

These two lines don’t assign anything. They just check that p1 and p2 are in fact structs of type %Product{} instead of %User{} or maybe simple maps. What we refer as assignment can happen only if the left side is a variable which is not the case above.

You swap the order
[p1, p2] = Catalog.list_products

Again p1,p2 are:

p1 is %Product{name: "Tomato"}
p2 is %Product{name: "Apple"}

but then you do

assert p1 = %Product{} 
assert p2 = %Product{} 

In this case %Product{} simply “returns/creates” an empty struct of type %Product{} and assigns it to p1 and another to p2. When we say empty we mean with all the keys set to nil or whatever their default value might be. So now your variables are

p1 is %Product{name: nil}
p2 is %Product{name: nil}

whoops.

Now your test fails because

assert p1.name == "Tomato"

is no longer true. p1.name now equals to nil

P.S.: Since structs are essentially maps here are some exmples in iex that might help you understand better.

iex(6)> colors = %{red: 1, blue: 2}
%{blue: 2, red: 1}
iex(7)> other_colors = colors
%{blue: 2, red: 1}
iex(8)> other_colors
%{blue: 2, red: 1}
iex(9)> %{red: 1, blue: 2} = more_colors
** (CompileError) iex:9: undefined function more_colors/0

iex(9)> other_colors = "something random"
"something random"
iex(10)> other_colors
"something random"
iex(11)> other_colors = colors
%{blue: 2, red: 1}
iex(12)> other_colors = "something random"
"something random"
iex(13)> %{} = other_colors
** (MatchError) no match of right hand side value: "something random"

iex(13)> other_colors = %{}
%{}
iex(14)>
1 Like

When you put…

%Product{} = p1

…you’re saying “does whatever is in p1 fit the shape of a Product struct?”

When you put…

p1 = %Product{}

…you’re saying “Does an empty Product struct fit the pattern of the left hand side, the variable p1, and if so, assign p1 to the empty struct.”

The = is directional, which is the thing that got me tripped up early on.

2 Likes

That’s cool!
Now I got the point. with single equal if the variable is at the left side, it will be assigned to the value on the right side which in the second case is an empty struct, but why does the same test not work with double equals as

assert %Product{} == p1
assert %Product{} == p2

And Thank you for your reply!