Testing Update

Testing update is a bit of a combination of the testing for the create and read functions. Let’s first look at the function we’re going to test:

 def​ update(%User{} = existing_user, update_params) ​do
  existing_user
  |> User.update_changeset(update_params)
  |> Repo.update()
 end

You can see that, like create/1, this function is a very lightweight wrapper around calls to the schema’s changeset function (update_changeset/2 in this case) and then to Repo.update/1. Testing it will be similar, but you’ll need to insert an existing user to be updated by the code. Let’s write our success test first:

1: describe ​"​​update/2"​ ​do
test ​"​​success: it updates database and returns the user"​ ​do
existing_user = Factory.insert(​:user​)
5:  params =
Factory.string_params_for(​:user​)
|> Map.take([​"​​first_name"​])
assert {​:ok​, returned_user} = Users.update(existing_user, params)
10: 
user_from_db = Repo.get(User, returned_user.id)
assert returned_user == user_from_db
expected_user_data =
15:  existing_user
|> Map.from_struct()
|> Map.drop([​:__meta__​, ​:updated_at​])
|> Map.put(​:first_name​, params[​"​​first_name"​])
20:  for {field, expected} <- expected_user_data ​do
actual = Map.get(user_from_db, field)
assert actual == expected,
"​​Values did not match for field: ​​#{​field​}​​ expected: ​​#{
25:  inspect(expected)
}​​ actual: ​​#{​inspect(actual)​}​​"
end
refute user_from_db.updated_at == existing_user.updated_at
30:  assert %DateTime{} = user_from_db.updated_at
end
end

The test uses the factory to insert a user on line 3. On line 5, the test is creating a parameter map to pass in. It’s using the factory to provide the data for consistency, but it’s then using Map.take/2 to grab only a single key/value pair. This is to keep the test as simple and unlikely to become stale as possible. If the test updated every field allowed on the schema, the likelihood of the test becoming outdated with any changes to the schema is higher. By choosing a single field, and one that’s less likely to change, this test will be easier to keep current. Additionally, the testing for update_changeset/2 does cover the logic around which fields can be updated, so we don’t need to be robust in this test.

Our test needs to assert that all the original fields have the same value except :first_name (the updated field) and :updated_at (which will be handled separately). To do this, the easiest thing is to construct a map with the expected keys and values in it, and then compare that to the values pulled from the database. There are plenty of ways to do this, but we chose to start with the existing user because it carries almost all of the data we want.

At line 14, we transform that user’s data into a map and then update the one value that changed. On line 17, the test drops two of the fields, meaning they won’t be checked. :updated_at is dropped because it won’t be the same between the two sets of data, and it doesn’t need to be. :__meta__ is dropped, even though it’ll be the same on both sets, because it’s a hidden Ecto Schema field and not part of the concerns of the test. The test is focused on the data that exists in the database.

On line 29, we’re making a very basic assertion to make sure that the value of the :updated_at field has been changed and that it’s still the right shape of data, a DateTime struct. This ties back to our discussion of testing the timestamps when we were testing create/1. We are balancing between maintaining high coverage and reducing the work and maintenance involved in our assertions.

Let’s add our error test next. Add a test under your success test, in the same update/2 describe block, that looks like this:

1: test ​"​​error: returns an error tuple when user can't be updated"​ ​do
2:  existing_user = Factory.insert(​:user​)
3:  bad_params = %{​"​​first_name"​ => DateTime.utc_now()}
4: 
5:  assert {​:error​, %Changeset{}} = Users.update(existing_user, bad_params)
6: 
7:  assert existing_user == Repo.get(User, existing_user.id)
8: end

This test is pretty easy to understand. On line 3, it’s creating a parameter that can’t be cast. This is the same kind of logic that we had in the helper function, invalid_params/1, called from our schema tests, by means of our SchemaCase file. This test isn’t using that same case template. If you find yourself doing this sort of thing often in your query tests, it could be a good candidate for a function in either DataCase or in the factory, because it’s focused on data.

A common mistake when testing a failed update, like this test is doing, is to miss checking that the data in the database hasn’t changed. Given how little logic is in Users.update/2, it’s very unlikely that anything would have changed, but it’s very little effort on our part and it builds in safety from regressions. Line 7 shows a one-line assertion and call to the database to add that safety.

We’ve covered the major points of testing an update function. Let’s finish with testing our logic to delete a user.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
3.22.51.241