Test to verify unique constraint

Hello guys, I’m learning about testing, however I haven’t been able to write a test for a unique constraint, I’ve already defined it in my model

def changeset(struct, params \\ %{}) do
  ...
  |> unique_constraint(:email, message: "Email is already taken")
end

And my actual test is:

test "email has been taken" do
    user = User.changeset(%User{}, @valid_attrs)
    Repo.insert(user)
    
    imposter = User.changeset(%User{}, @valid_attrs)
    
    assert {:error, changeset} = Repo.insert(imposter)
    assert {:email, "Email is already taken"} in errors_on(%User{}, changeset)
end

However, I get failed test saying

** (Ecto.CastError) expected params to be a map, got: #Ecto.Changeset<action: :insert, changes: %{email: "user@stories.com", name: "Jon Doe", password: "safepassword", password_confirmation: "safepassword", password_hash: "$2b$12$qkYBGNiNG9D5Z2Sfv8TSEu6IqTxf7Nylxk471z.KpK7ex3Y37kFV6"}, errors: [email: {"Email is already taken", []}], data: #StoryTime.User<>, valid?: false>

And for my @valid_attrs:

@valid_attrs %{email: "user@stories.com", name: "Jon Doe", password: "safepassword", password_confirmation: "safepassword"}

I don’t understand what map is it talking about.

Thanks for your time and help.

You left out the most important part, which is the full body of the def changeset function.

Sorry about that, here it is

def changeset(struct, params \\ %{}) do
    struct
    |> cast(params, [:name, :email, :password, :password_confirmation])
    |> validate_required([:name, :email, :password, :password_confirmation], message: "Are required")
    |> unique_constraint(:email, message: "Email is already taken")
    |> validate_format(:email, ~r/@/, message: "The email is invalid")
    |> validate_length(:email, min: 4, max: 40)
    |> validate_length(:name, min: 6, max: 40)
    |> validate_format(:name, ~r/^[a-zA-Z]+$/, message: "Name must only contain letters")
    |> validate_length(:password, min: 6, max: 100)
    |> validate_confirmation(:password, message: "Passwords must match")
    |> hash_password
end

That looks like the correct changeset you got back from trying to insert the duplicate record in this line:

assert {:error, changeset} = Repo.insert(imposter)

Are you sure the map that the error is referring to is not related to the errors_on/2 function?

Also, in regard to your changeset logic, shouldn’t the unique_constraint come after all the email formatting validation? Any email that would violate that constraint would have already passed the subsequent validations.

Does your errors_on/2 look something like this?

def errors_on_changeset(struct, data) do
  struct.__struct__.changeset(data)
  |> Ecto.Changeset.traverse_errors(&AppName.ErrorHelpers.translate_error/1)
  |> Enum.flat_map(fn {key, errors} -> for msg <- errors, do: {key, msg} end)
end

It is the default one, yes:

def errors_on(struct, data) do
    struct.__struct__.changeset(struct, data)
    |> Ecto.Changeset.traverse_errors(&StoryTime.ErrorHelpers.translate_error/1)
    |> Enum.flat_map(fn {key, errors} -> for msg <- errors, do: {key, msg} end)
end

That’s my issue, I have no idea what map it’s referring to, and I’m trying to use errors_on/2 because someone pointed that out in the Phoenix channel.

can you change your assertion to the following and see if your test runs?

assert changeset.errors[:email] == {"Email is already taken", []}
1 Like

Yes, thank you!

For the record:

test "email has been taken" do
    user = User.registration_changeset(%User{}, @valid_attrs)
    assert {:ok, user } = Repo.insert(user)
    
    imposter = User.registration_changeset(%User{}, @valid_attrs)
    
    assert {:error, changeset} = Repo.insert(imposter)
    assert changeset.errors[:email] == {"Email is already taken", []}
end
def changeset(struct, params \\ %{}) do
    struct
    |> cast(params, [:name, :email])
    |> validate_required([:name, :email], message: "Are required")
    |> unique_constraint(:email, message: "Email is already taken")
    |> validate_format(:email, ~r/@/, message: "The email is invalid")
    |> validate_length(:email, min: 4, max: 40)
    |> validate_length(:name, min: 6, max: 40)
    |> validate_format(:name, ~r/^[a-zA-Z]+$/, message: "Name must only contain letters")
  end
  
def registration_changeset(struct, params \\ %{}) do
    struct
    |> changeset(params)
    |> cast(params, [:password, :password_confirmation])
    |> validate_required([:password, :password_confirmation], message: "Are required")
    |> validate_length(:password, min: 6, max: 100)
    |> validate_confirmation(:password, message: "Passwords must match")
    |> hash_password
end
1 Like