Testing Validations in Elixir and Ecto

Current Versions

As of the time of writing this, the current versions of our applications are:

  • Elixir: v1.1.1
  • Phoenix: v1.1.0
  • Ecto: v1.1.0
  • Comeonin: v2.0.0

If you are reading this and these are not the latest, let me know and I’ll update this tutorial accordingly.

Validating Our Phoenix Models

If you’ve been following along the “Writing a Blog Engine in Phoenix and Elixir” tutorial series that I’ve been writing, then one thing that may have occurred to you is that we’ve not really done a whole lot to handle validating our model data, nor have we done anything to really test it. We don’t validate our password confirmation, nor our uniqueness of username or email, and we should probably not allow passwords that that are only a single character long. We’ll start with our password confirmation.

Validating Password Confirmation

First, in test/models/user_test.exs, we’ll write in a test for when the two passwords do not match:

And we’ll run:

mix test test/models/user_test.exs

We’re expecting a failing test here, so that’s okay! Let’s modify our web/models/user.ex file and add to our changeset function:

We’ve added a call to validate_confirmation which takes the key that has a corresponding_confirmation field (:password in our case) and then supplies a message to place on the password_confirmation field in our errors list.

Run our tests again and we should see green tests now! We should probably make sure that the error we saw was actually on the password_confirmation and not some other random value, so we’ll add one more assertion to our test:

assert changeset.errors[:password_confirmation] == "does not match password!"

Run this again and our tests should still be green! Now, let’s tackle something else; we’ll deal with adding unique constraints to our email address and usernames!

Validating Uniqueness of Username and Email

Dealing with uniqueness is a bit trickier than the confirmation bit. The first gotcha is that uniqueness needs to be enforced at the database level, not the changeset level. That’s going to change our implementation and our testing strategy as well.

We’ll start off the same as the above: we’ll add our tests first to test/models/user_test.exs:

We need to explain something here, since our tests are pretty different from the password validation tests. The first is that uniqueness validation only happens when we attempt to insert the row. This allows us to offload that work onto the database, which is much more efficient. We assert using pattern matching, since inserting into the database with Repo.insert (note: not Repo.insert!) returns a tuple in the form of {:error, changeset}. If we don’t match, both assertions will fail, so that works perfectly for us. The rest remains the same.

If we run our tests these two will fail, as expected. Now, let’s make them green! First, we’ll need to add unique indexes to our username and email fields, and to do that we’ll need to create a migration:

mix ecto.gen.migration add_unique_index_to_username_and_email

Which gave us the following output:

Generated pxblog app
* creating priv/repo/migrations
* creating priv/repo/migrations/20151015151159_add_unique_index_to_username_and_email.exs

So let’s open up that file, and modify the change function to contain two calls to create unique indexes:

And then run this new migration with:

mix ecto.migrate

If we attempt to create two users with the same username or email address, we’ll get failures, but not validation failures! We need to also change our changeset function in web/models/user.ex to look for this constraint:

Rerun our tests and we’re back to green!

Validating Password Length

We should also make sure the user doesn’t enter in some sort of super small password. “a” as a password doesn’t tend to make for terribly secure passwords, so let’s at least require 4 characters. Still not the best security in the world, but it works for what we’re trying to accomplish here. Again, we’ll write our test first:

Again, we’ll run it to verify that it fails at the start, and then get the implementation working again. The nice thing about it is that it’s as simple as the password validation approach, so:

Note the addition of validate_length(:password, min: 4) at the bottom! Run our tests and we are green again!

Other Validations

We could spend entirely too long adding validations for every silly case we can think of, but we don’t want to bloat things any more than we have at this point. Regardless, here are some of the other validations that you can perform with Ecto:

validate_change(changeset, field, validator)
Validates the given field change

validate_change(changeset, field, metadata, validator)
Stores the validation metadata and validates the given field change

validate_confirmation(changeset, field, opts \\ [])
Validates that the given field matches the confirmation parameter of that field

validate_exclusion(changeset, field, data, opts \\ [])
Validates a change is not included in given the enumerable

validate_format(changeset, field, format, opts \\ [])
Validates a change has the given format

validate_inclusion(changeset, field, data, opts \\ [])
Validates a change is included in the given enumerable

validate_length(changeset, field, opts)
Validates a change is a string of the given length

validate_number(changeset, field, opts)
Validates the properties of a number

validate_subset(changeset, field, data, opts \\ [])
Validates a change, of type enum, is a subset of the given enumerable. Like validate_inclusion/4 for lists

And the validations that require database indexes or equivalent database constructs:

foreign_key_constraint(changeset, field, opts \\ [])
Checks for foreign key constraint in the given field

unique_constraint(changeset, field, opts \\ [])
Checks for a unique constraint in the given field

You can read more about these validations and what keys are valid/invalid at the Ecto.Changeset documentation.