In this chapter, we are going to create the test cases for the models, jobs
and services that we had created in the previous chapter.
Adding tests for the User model
Let's incrementally add the following test cases into
test/models/user_test.rb.
Let's test if user preference is created when a new user is created:
Let's add another test to verify the default preference saved for the user is
the same as the DEFAULT_NOTIFICATION_DELIVERY_HOUR which we defined in our
config/initializers/constants.rb file:
We have used the assert_equal method to check if the two values provided are
equal.
Adding tests for the Preference model
Open test/models/preference_test.rb and let's create a preference instance
in the setup method so that it's available to all the test cases:
Let's test that a preference is invalid if notification_delivery_hour is not
present.
Add the following code to the test file:
In the above code, we have used assert_includes.
The assert_includes assertion takes in two arguments and checks whether the
second argument is included in the first argument, which is a collection.
We have used errors.messages, to get all the errors in hash format.
In this hash, the key is the attribute's name and its value is an array of error
messages specific to that attribute.
Below is an example showing how the hash value looks like:
Here, since preference is invalid, the model should be ideally raising a
validation error.
We are just making sure that the error is actually raised and the message raised
is the same as what we had.
In our Preference model the value of notification_delivery_hour should only
take in default business hour values which are in the range of 0 to 23.
Thus we have to test that the Preference model will be invalid if an invalid
value is provided to notification_delivery_hour.
When testing for invalid values, we should always prefer to test the n-number of
such values.
This is to make sure that we are weeding out any edge cases. The same logic
applies when testing valid values.
Add the following code to the test file:
Adding tests for the UserNotification model
Let us first create a new factory to generate instances of UserNotification
model. Create and open test/factories/user_notification.rb and add the
following contents to it:
As you can see, we haven't used faker here. We can pass any Ruby expression as
the fields value for generated objects in the factory definition.
Open test/models/user_notification_test.rb and let's create setup for the
test cases:
In the above code, we have used Time.zone.today instead of using the default
Ruby Time, Date and DateTime classes.
If we use Ruby default classes, it will not show time in the time zone specified
by config.time_zone in application.rb.
That's the reason why we should always use Active Support methods of Time.zone
to pick up the Rails time zone.
Let's test that a user_notification is invalid if
last_notification_sent_date is not present:
Let's add another test to check last_notification_sent_date must be a parsable
date:
When we provide a string value to a datetime field, Rails will try to parse
that string to make sure it's a valid date.
In the case where the string passed in is not a valid date, Rails will store
nil value to that particular field. That's the default Rails behaviour.
This is actually the way
Rails typecasts.
We can use the _before_type_cast accessor to apply the validation. But since
that brings in some obfuscation, let's go with the default behaviour of Rails.
In our test case, we are trying to store a date string whose month is 13,
which is invalid. Thus Rails will typecast it into nil when storing it in the
field.
So that is the reason why we are using the assert_equal method for
last_notification_sent_date attribute to verify it is nil for an invalid date.
Notice that we have tested against multiple data. This re-emphasizes the need
for general test cases.
Let's add another test to make sure user_notification is invalid if
last_notification_sent_date is set to a past date:
Adding tests for the TodoNotificationsJob
Open test/jobs/todo_notifications_job_test.rb and paste the following
into it:
In the above test case, we have populated the required models in the setup
method, and then tested whether our job is actually sending mails
successfully, if it's the actual delivery time.
As you can see, we are using the travel_to helper in our setup method.
travel_to helper changes the current time to the given time by stubbing
Time.now, Date.today, and DateTime.now to return the time or date passed
into this method. The stubs are automatically removed at the end of the test.
We know that the default mail delivery time is 10 AM. So if the job runs at
this specific time, then it should be able to send the mail.
Thus, in our test case, instead of waiting for the time in our system to be 10
AM, we are temporarily setting the time as 10 AM by using the travel_to
helper.
assert_difference method allows us to check that a value has changed by a
given amount after a block has been executed.
Here in our code, we passed the lambda function
-> { @user.user_notifications.count } which returns the total records present
in the user_notifications table for that user.
Lambda functions are the method created using the literal lambda operator
-> {}.
You can also use the lambda keyword instead of -> but the literal operator
is succinct and commonly used.
Once the mail is successfully sent to the user, the
last_notification_sent_date attribute in the user_notifications table will
be set with today's date for that particular user.
That's the reason why we are asserting that the count in user_notifications
for that particular user has been changed by one, given that we are only sending
one mail.
Adding tests for the UserNotificationsJob
Open test/jobs/user_notifications_job_test.rb and paste the following
into it:
Similar to how we tested TodoNotificationsJob, here also we used the
assert_difference method to check whether the mail has been successfully sent
to the user.
Creating support or helper files for tests
In Rails, we can create support files for testing. These are helper modules
which can be used by any test file by importing it.
Let's create the support directory which will have the helper modules.
An example of such a support test module is a SidekiqHelper.
When testing Sidekiq queues, we are often required to manipulate the Redis queue
before and after the test cases. Rather than doing it manually each time, we
delegate it to a support method:
The following is an example of how the module, which will be located at
test/support/sidekiq_helper.rb, will look like. You don't have to create this
file since we won't be testing any Sidekiq queue related processing.
It is important to understand when you would want to manipulate the Redis queue
in your test cases. This will help you decide whether to include the above
module or not. One common scenario where such methods would be useful is when
dealing with flaky tests. Let us see this in detail.
Sidekiq offers three different testing
modes:
- A fake mode that pushes all jobs into a jobs array. Here no job processing
is performed. This is the default mode.
- An inline mode that runs the job immediately instead of enqueuing it.
- A disabled mode where jobs are enqueued in Redis and processed normally.
To change the testing mode in Sidekiq, you can use the Sidekiq::Testing
module. But it should be noted that such a change is a global change and can
cause unexpected behavior for randomized or parallelized tests.
Assume we have to test the presence of a job in the queue. In this case, we do
not need to execute the job. We only need to enqueue it and then test whether
the length of the queue is increased by one. Hence, the default fake mode
would suffice over here. But one should note that the Sidekiq job queues are global
and will therefore persist between tests. So if you are writing another test
case, that utilizes the same queue, but focuses on the execution of the job, it
might be a good idea to clear the Sidekiq job queue using after_teardown provided
above and then switch to the inline mode. Otherwise, your tests will bleed
state, and your test suite will become order dependent. Using
Sidekiq::Testing.inline! can help prevent flaky tests by ensuring that
background jobs are processed immediately, rather than being enqueued for
processing by Sidekiq jobs.
It's generally a best practice to change the mode within the context of a block
so that the change only affects the specific test or set of tests that you're
working on. This helps to ensure that the behavior of your tests is consistent
and predictable, even when they are run in randomized or parallelized
environments.
In the cases where you'd want to use these support modules, we have to require
it in the test file and include it within the class.
Here is an example of importing the helper module:
Requiring these modules manually in the test files can be a repetitive chore.
Thus in cases where we want to use support modules, we can automate the
require part, by adding the following in our test_helper:
Note that adding the above statement, would only automatically require the
files.
You'd still have to manually include the required modules within your test class
when needed.
Adding tests for the TodoNotificationService
Let's create todo_notification_service_test file in the tests/services directory :
While testing this service we are going to make sure that the user preferences
are always respected before sending mail.
We will also ensure that idempotency is handled.
Open the file and paste the following into it:
Adding tests for the TaskMailer
We can also write a test case for our TaskMailer to check if ActionMailer is
actually delivering our mails or not.
Open test/mailers/task_mailer_test.rb and paste the following into it:
Let us also test if user_notification is generated after an email regarding
pending tasks has been sent to the user. Add the following test case to
TaskMailerTest:
Now, add a test case to verify that no email is being sent in case the receiver
or user with given user_id is not present:
In the above code, we have used the pending_tasks_email method created in
TaskMailer, which will find the user with the user id and all the pending
tasks associated with that user, to send the mail.
Also, we have used deliver_now on the TaskMailer to send the mail.
We can also use the deliver_later helper method to enqueue the mail to be
delivered through Active Job. When the job runs it will send the mail using
deliver_now.
Here, we are using the deliver_now method, as we did not want to wait for mail
delivery.
ActionMailer::Base.deliveries keeps an array of all the mails sent out through
the ActionMailer.
Thus we are asserting that after successfully sending the mail
ActionMailer::Base.deliveries? is not empty, since it's an array of emails
already sent out.
This was the last test case. So we have completely tested and fortified our
Sidekiq email sending feature.
You can run all the test cases by using bundle exec rails t -v.
You can also append a specific test file's path to this command to make it run
only a single test file.
Now let's commit these changes: