DEV Community

milandhar
milandhar

Posted on

Unit Testing in Rails

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!

Eat, Sleep, Code, Unit Test

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:

  validates :title, presence: true
  validates :image_url, presence: true
  validates :project_link, presence: true
  validates :theme_str_id, presence: true
Enter fullscreen mode Exit fullscreen mode

These will ensure that Projects 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:

require 'test_helper'

class ProjectTest < ActiveSupport::TestCase
  test "the truth" do
    assert true
  end
end 
Enter fullscreen mode Exit fullscreen mode

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:

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
Enter fullscreen mode Exit fullscreen mode

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):

  test "should query the active projects API endpoint without error" do
    assert_nothing_raised do
      Project.queryActiveProjects(nextProject:nil)
    end
  end
Enter fullscreen mode Exit fullscreen mode

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:

    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
Enter fullscreen mode Exit fullscreen mode

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:

test "should create countries" do
    Country.delete_all
    Country.create_all
    assert_not_equal Country.all.count, 0, "Didn't create any countries"
  end
Enter fullscreen mode Exit fullscreen mode

Here I used assert_not_equal to ensure that the number of Countrys 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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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)

Collapse
 
usmanabdullah1 profile image
usmanabdullah1

Excellent work!
Helped a lot for beginners like me