Ectogram: Testing Ecto
9 min readI will be honest when I say any testing experience I have is like 4 years old, it is with Jest in JavaScript Land, and only on the frontend…so yeah this will be a learning experience. I guarantee you there are better approaches to testing using ExUnit that I just have not discovered yet. I am making it a point to make sure there are some basic tests in place for all the contexts that represent Ectogram. Like Allen Iverson once said:
and yeah Allen, I need lots of practice. 🤣 What better way to learn testing than when recreating a platform that serves over 1 billion active users.🥲
Understanding DataCase
We already got our test environment setup in this post and one of the things we added was a ExUnit.CaseTemplate we can import into each of our tests. It’s purpose is just as it says to be a template that brings in all the tools we need for a specific kind of test. Since we are working with Ecto it would be smart to make sure we have all the tools we need to work with Ecto.
It also adds an errors_on/1
helper that will make for easy inspection of invalid changesets later on in our tests.
Creating A User Fixture
I am basing a lot of what I am doing off of the scaffolding you get when generating a new Phoenix project. In Phoenix you will get these things called fixtures generated for free when you use the mix phx.gen.context
generator so that’s where I am starting.
Since I am already using faker
for seeds I decided to just copy and paste that code from seeds.exs
into the @valid_attrs
attribute. The first pass I was like yeah this is dope it is working and then I realized to fail tests I would need to pass in invalid fields. I opted to use a map and then use Map.merge/2 to update the original map.
I realize at the time of writing this and reading the docs on
Map.merge/2
it is safer to useKernel.struct/2
because technically thevalid_attrs
is a representation of the%User{}
struct. The current implementation will merge any key given in the second map into the first. I made a note to myself to update this in the code base 🥲
The user_fixture/1
will act just as register_user/1
in returning either a user that now exists in the database or changeset with an error message.
We do need to update the test_helper.exs
file so faker
is added to the test environment when our test suite starts up:
Testing Reads
Since we can now bootstrap a user or if needed multiple users into the database we can get started by making sure we can read the user(s) from the database.
So very similarly to JavaScript Land their is a describe, test, and setup block. The setup/1
block is amazing in my opinion because it cuts down on having to create a user or resource in every test manually. As you can see in the code sample above we return a map with a user
every time the setup block is invoked, and it just so happens that will occur right before every test is ran.
The great thing about how Ecto handles testing in our sandbox is every test is executed as a transaction. At the beginning of the transaction we already have a user present in the database to test against. We then perform whatever query we want to test against that user and on completion of the test Ecto will rollback the transaction. This means if we updated the user’s name when the test finishes the user’s name will return to the initial value. This protects us from issues surrounding artifacts from previous tests interfering with future tests.
The SQL Sandbox is precisely what allows our tests to write to the database without affecting any of the other tests. In a nutshell, at the beginning of every test, we start a transaction in the database. When the test is over, we automatically rollback the transaction, effectively erasing all of the data created in the test. ~ Testing Contexts
Since our get_user!/1
is defined with a !
we expect that if a user is not found with the given id it should raise and error from Ecto. We can verify that with assert_raise/2
. I’ll admit I didn’t fully understand this one at first but basically the first argument is the expected error to be raised an then we execute the query inside the second argument that is an anonymous function. My assumption is that the anonymous function keeps the error scoped locally so it can be compared against the given error. If it was not done this way it would actually break the test because the error would bubble up out of the scope of the test crashing the test suite…but I could be wrong. Need to ask smarter people than me. Ecto.UUID
gives us the ability to have a random uuid available to fail the test.
The second test is pretty straight forward and honestly could be reduced down to the following:
I just went for an approach that was more explicit to a first time reader.
Testing Inserts
Our insert method for the user is register_user/1
and there are a lot of cases to test with all the validation and unique constraints that are present. For brevity I have removed a lot of the tests, but they can be found in the pull request link at the end of the post.
In the first test we just check that if we hand nothing to register_user/1
that it fails with all the required fields corresponding validation errors. In the second test we are checking that the super cool SQL constraint we wrote is actually working.
A quick note: I found that if I needed to call
user_fixture/1
inside of a test I needed to add new values for the uniquely constrained fields. I am not entirely sure why this is, but my assumption is thatuser_fixture/1
is cached in memory with the valuesfaker
initially populates the user with so calling it over and over again will just attempt to create the same user again and again which will fail the unique constraints. I need to consult smart people to verify, but that’s my theory.
The last test is just an example of one of the many checks of the unique constraints being verified.
Testing Updates And Deletes
I did not write code for these actions with the User context and schema. At the moment I don’t want to do anything surrounding that. In future contexts though there will be the ability to update and delete resources so I will cover that in those modules.
Running Tests In Async Mode
I actually forgot all about enabling this and didn’t realize it until writing this post. Yet another thing to circle back too. async
mode by adding the following to our test:
What this is doing is telling ExUnit that it can run each test suite with the async
tag concurrently to one another. The individual tests in each test suite are still ran serially though. So say Ectogram ends up having like 200 test files/suites. If we didn’t add the async
tag the suites would be ran serially as well as their internal tests which could be a pretty lengthy process. By running the test suites asynchronously we will speed up the time it takes our test suite to complete by a lot!
And that does it for testing at least for the user context and schema. In the next post I am going to add most if not all of the rest of Ectogram’s data models and we will cover probably what kicks my ass the most when it comes to SQL in general: associations. As always the code for this post can be found in PR#3.
Related Articles
Ectogram: Generating The Users Table
Part 3 in the Ectogram series where I generate the user table migration and schema.
Ectogram: Setting Up Ecto
Part 2 in the Ectogram series where I setup the project and make customizations to Ecto.
Ectogram: Introduction
A clone of the popular social media platform, Instagram, written in Elixir & Ecto.
Cody is a Christian, USN Veteran, Jayhawk, and an American expat living outside of Bogotá, Colombia. He is currently looking for new opportunities in the tech industry.