An ExUnit test assertion failed unexpectedly:

     left:  %{issue: [:token], token: %Token{browser_id: "706de918-eadf-4553-9c82-96131ee1617a"}}
     right: %{issue: [:token], token: %Token{browser_id: "706de918-eadf-4553-9c82-96131ee1617a", customer_id: nil, login_id: nil}}

A struct should not have a different set of keys. There are a few different things going on here:




First, the assertion inspection must be over stating the left value as a struct because the struct does define the two missing keys. Second, somehow the left value is a corrupted struct with just enough to deceive the assert inspection.

Experiment

A simple struct to start:

iex(1)> defmodule P, do: defstruct [ :a, :b, :c ]
{:module, P,...}
iex(2)> P.__struct__
%P{a: nil, b: nil, c: nil}

Sanity checking:

iex(4)> n=struct( P, %{a: 1})
%P{a: 1, b: nil, c: nil}

How to get rid of those keys...

I was looking at the code that fed the broken assertion. What would remove those keys... then I found the Map.drop.

iex(5)> Map.drop(n,[:b])
%{__struct__: P, a: 1, c: nil}

Voila, a broken struct. This isn't doing something unexpected, but it is leaving things in a messy state. One that doesn't show in tests correctly.

Another example of a broken struct is:

iex(5)> Map.put(n, :r, 11)
%{__struct__: P, a: 1, b: nil, c: nil, r: 11}

The problem is that Map is happy to work on a struct, and in fact needs to since Map.get is the way to parameterize getting a value with a key not known at compile time, since the Access behaviour is not implemented for structs. For example:

iex(6)> n[:a]
** (UndefinedFunctionError) function P.fetch/2 is undefined (P does not implement the Access behaviour)
    P.fetch(%P{a: 1, b: nil, c: nil}, :a)
    (elixir) lib/access.ex:267: Access.get/3

But properly converting a struct to a Map:

iex(6)> j=Map.from_struct(n)
%{a: 1, b: nil, c: nil}
iex(7)> j[:a]
1

This all highlights ways that a struct is and is not a Map. The way this happened in my code is that it started out using plain maps and then was converted to using a struct. The Map.drop wasn't an obvious issue during that refactor. structs offer a well defined way of pattern matching that goes well beyond maps, but it is important that if you need to deform a struct that it be explicitly transformed to a Map using from_struct.