For my blog this week, I decided to explore the topic of unit testing in Rails. Throughout my time at the Flatiron School, I completed labs that included RSpec tests to determine if the code was correct. Although I indirectly used hundreds of these RSpec tests, I never quite dove into the topic of writing tests myself. I was always curious how to write my own tests and knew it is an extremely skill to learn for Test-Driven Development (TDD), so I decided I needed to teach myself how to write them!
Unit Testing
I wanted to begin my testing journey with the concept of unit testing. Broadly speaking, unit testing is a testing method where individual units of source code, program modules, usage procedures, and operating procedures are tested to determine if they are fit for use. They make sure that a section of an application, or a “unit”, is behaving as intended. In a Rails context, unit tests are what you use to test your models. Although it is possible in Rails to run all tests simultaneously, each unit test case should be tested independently to isolate issues that may arise. The goal of unit testing is to isolate each part of the program and show that the individual parts are correct.
Advantages of Unit Testing
Unit testing has become a standard in many development methods because it encourages well-structured code and good development practices. One advantage of unit testing is that it finds problems early in the development cycle rather than later. It’s always better to find bugs earlier so that features that are added on later in the cycle are using the correct component parts. Additionally, unit testing forces developers to structure functions and objects in better ways, since poorly written code can be impossible to test. Finally, unit testing provides a living documentation of the system as it is developed. The description of the tests can give an outsider a basic understanding of the unit’s API and its features.
The Test Directory
In a normal situation, when creating a new Rails app, you would run the rails new application_name
command after installing Rails. Once the skeleton of the new Rails application has been built, you will see a folder called test
in the root of the new application, and many subfolders and files within the test
directory. For the purposes of this post, I will be focusing on adding tests to the models
directory, since unit testing in Rails primarily involves testing models. However, if you are interested in learning about the other directories within the "test" folder (such as helpers
, channels
, controllers
, etc), you can do so here.
Rails automatically adds new test files to the models
directory when new models are generated. So if you were to run a rails generate model User
, Rails would add a user_test.rb
file in the test/models
directory. The unit tests will be written in these files for each model.
Unit Testing My Project
I decided a good place to start practicing unit testing would be my project called EffectiveDonate, a nonprofit donation website. I originally developed this project without any tests, and it has a fairly complicated Rails backend with many models, so there was a plethora of features I could test.
It was slightly more work to add in the tests at this point, since I initially ran the rails new
command without adding tests. I ended up just creating a new blank project with the test directory, and then dragging it into the root of my EffectiveDonate backend. Then I added files for the models that I wanted to test into the models
directory, such as project_test.rb
.
To learn how to unit test my models, I followed the Rails Guides for Unit Testing Models. I won't repeat all the information I read there, but I would highly recommend reading it since it is an excellent resource for how to write Rails testing code, testing terminology, how to prepare your app for testing, running tests, and test assertions. An important piece of advice in the documentation is to include a test for everything which could possibly break. And that it is a best practice to have at least one test for each of your validations and at least one test for every method in your model. So with that framework, I began writing tests!
Project Model
The most complex model in my backend for EffectiveDonate is the Project
model. This model has several has_many / belongs_to associations with other models, and the primary call to the external API (GlobalGiving) that feeds all the data in my project happens in this model.
But as I began writing tests for the Project
model, I realized I didn't have any validations! So theoretically it would be possible for a Project
instance to be created without having key attributes like a title or images. While GlobalGiving's API generally has very clean data, I want to make sure all the Project
data in my app has the attributes I need. So I went ahead and wrote the following validations:
ruby
validates :title, presence: true
validates :image_url, presence: true
validates :project_link, presence: true
validates :theme_str_id, presence: true
These will ensure that Project
s without these attributes will not be saved.
Now it was time to test these validations. In my test/models/project_test.rb
file, I had the following class set up:
ruby
require 'test_helper'
class ProjectTest < ActiveSupport::TestCase
test "the truth" do
assert true
end
end
This is the skeleton that Rails adds for each model that is generated. The "the truth" test is an example test that will always pass and allows you to verify that your Test class is up and running. In Rails testing, assertions are the most important part of the test. They actually perform the checks to make sure things are going as planned. The example test above will pass because it just ensures that the argument in the assertion (true
) is true. There are many other types of assertions that you get for free with Rails unit tests, each with their own purpose.
In order to test my new Project
validations, I wrote similar tests for each validation. Each test created a new Project
, then assigned the projects all of the other required attributes, except for the one that I am testing for. Then I wrote an assert_not
to ensure that the Project
would not save without the missing attribute.
For example, here is my validation for my test to ensure a Project
will not save without a title:
ruby
test "should not save a project without a title" do
project = Project.new
project.image_url = "https://files.globalgiving.org/pfil/34796/pict_large.jpg?m=1534281939000"
project.theme_str_id = "env"
project.project_link = "https://www.globalgiving.org/projects/nourish-a-young-brain-protect-one-ancient-culture/"
assert_not project.save, "Saved the project without a title"
end
If I ran the test and the new project
instance did get saved, I would know there was a problem with my validation, and would receive the message in the argument of the assertion: "Saved the project without a title".
To test the other methods in my Project
model, I wrote different tests. These methods perform the fetches from the GlobalGiving API, and return JSONs with the project data. I used the assert_nothing_raised
assertion to ensure my queryActiveProjects
method runs without raising an error (this will ensure the API endpoint and my credentials are correct):
ruby
test "should query the active projects API endpoint without error" do
assert_nothing_raised do
Project.queryActiveProjects(nextProject:nil)
end
end
I also wanted to ensure that the JSON returned by this method has attributes that the application needs, such as nextProjectId
, and that the JSON includes a project
object with a valid id
:
ruby
test "should return a json of projects that has a next project ID" do
assert Project.queryActiveProjects(nextProject:nil)["projects"]["nextProjectId"]
end
test "should return a json of projects whose first project has an ID" do
assert Project.queryActiveProjects(nextProject:nil)["projects"]["project"].first["id"]
end
These tests use assert
to validate that both of these attributes exist.
Country Model
My Country
model includes similar API fetch methods to Project
above, so I wrote a similar unit test using assert_nothing_raised
. I also wrote a test to ensure that my create_all
method actually creates Country
instances:
ruby
test "should create countries" do
Country.delete_all
Country.create_all
assert_not_equal Country.all.count, 0, "Didn't create any countries"
end
Here I used assert_not_equal
to ensure that the number of Country
s created was greater than 0.
User Model
My User
model already included a validation to ensure that a User
cannot create a profile with the same username
as someone else: validates :username, uniqueness: { case_sensitive: false }
To test this, I created one User
and then used an assert_not
to ensure that a second User
with the same username could not be saved:
test "should not allow duplicate user names" do
user1 = User.create(username: "barryobama", password: "bo")
user2 = User.new(username: "barryobama", password: "michelle")
user1.save
assert_not user2.save, "Saved a duplicate user name"
end
If user2
was able to be saved, the test would fail and I would see the message "Saved a duplicate user name".
User Starred Project Model
The final model in EffectiveDonate that I needed to test was User_Starred_Project
. A user should not be able to star the same project multiple times, so I had already added the following validation to the model: validates :user_id, uniqueness: {scope: :project_id}
.
In order to test this validation, I created a new User
and a valid Project
with all the required attributes. I then successfully created one instance of UserStarredProject
using the id
of both the User
and Project
that I created. I then attempted to save a duplicate UserStarredProject
, using assert_not
to ensure that it would not save:
test "should prevent a user from starring multiple projects" do
user1 = User.create(username: "barryobama10", password: "bo")
project1 = Project.new
project1.title = "Nourish a young brain, protect one ancient culture"
project1.theme_str_id = "env"
project1.image_url = "www.google.com/img"
project1.project_link = "https://www.globalgiving.org/projects/nourish-a-young-brain-protect-one-ancient-culture/"
project1.save
user_star_1 = UserStarredProject.create(project_id: project1.id, user_id: user1.id)
user_star_2 = UserStarredProject.new(project_id: project1.id, user_id: user1.id)
assert_not user_star_2.save, "User starred a duplicate project"
end
If there was a problem in my validation, I would see the message "User starred a duplicate project", and the test would fail.
Conclusion
Ultimately, this process of learning about unit testing and testing my own project's models was very helpful. For some reason, testing seemed like an intimidating skill to learn, but I realized it is actually not too difficult once you get started. I also am starting to understand its importance to maintaining well-organized and clean code. I probably wouldn't have added all of those validations to my Project
model unless I started to test it, so my app is already more resilient to bad data than it was before. Also, the tests I wrote to check the API calls will be really helpful to ensure that my API credentials are still valid and that the endpoints I am reaching are correct.
I am looking forward to exploring more facets of testing in the near future, so stay tuned for more testing-related posts. Until next time!
Top comments (1)
Excellent work!
Helped a lot for beginners like me