DEV Community

milandhar
milandhar

Posted on

Functional Testing in Rails

In last week's post, I described how I learned the basics of unit testing and then wrote unit tests for my EffectiveDonate project. As I mentioned in that post, unit tests in Rails deal with testing a model's validations and methods. This week, I will be diving into the concept of functional testing, which in Rails means testing controllers!

Test tubes cartoon

Functional Testing

In general, functional testing is a "type of software testing whereby the system is tested against the functional requirements/specifications." These requirements are tested by feeding them sample input based on the function's specifications, determining the output based on the behavior of the program, and then comparing the actual and expected results. This is a chance for developers to ensure that real-world user stories are fulfilled to satisfy the requirements of the application.

Rails' documentation recommends writing functional tests for the following:

  • was the web request successful?
  • was the user redirected to the right page?
  • was the user successfully authenticated?
  • was the appropriate message displayed to the user in the view?
  • was the correct information displayed in the response?

Functional Testing in Rails

To get started learning about the test directory that is built-in to Rails applications, refer to the "Test Directory" section in my previous blog . Similar to creation of unit tests for models, running a command like rails generate controller project will create a file called projects_controller_test.rb in the test/controllers/ directory.

In this newly created file, you will see a class declaration like class ProjectsControllerTest < ActionDispatch::IntegrationTest. Rails has encouraged the use of its "integration-style" controller tests because they perform actual requests, while the "functional-style" tests in Rails just simulate a request. But for the purposes of this post, I still refer to the tests I wrote as "Functional", following the Rails testing naming convention (although some of them do involve multiple components).

Functional Testing My Project

Now that my models for EffectiveDonate have been unit tested, it was time to functionally test my controllers! I used Rails' documentation to learn the syntax for functional testing in the Minitest suite, and would highly recommend reading it to anyone interested. While the assertions for functional testing are similar to those used in unit testing, there are several additional features of functional testing, such as request types (like get, post, put, etc), available instance variables (@controller, @request, @response), and three Hash objects (flash, cookies, and session).

While I wrote functional tests for more controllers than I've included in this post, some tend to follow the same patterns, and I tried to discuss the ones that were most distinct here. Let's dive into the controllers I tested:

Countries Controller

For my first functional test, I wanted to start with a simple request to the index endpoint of the Countries controller. In the conuntries_controller_test.rb file, I wrote the following:

    test "should get index" do
      get "/api/v1/countries"
      assert_response :success
    end
Enter fullscreen mode Exit fullscreen mode

This test sends a get request to the "/api/v1/countries" route. The :success parameter of assert_response specifies that the test will pass if the response comes back with a status code between 200-299. You can also specify :redirect (status code between 300-399), :missing (status code of 404), or :error (500-599).

A key function of the Countries controller is its ability to return the correct number of active projects in each country. Below is the test I wrote for this:

  test "should get project count" do
      Country.delete_all
      Country.create(name: "Mexico", iso3166CountryCode: "MEX")
      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/"
      project.title = "Nourish a Young Brain Protect One Ancient Culture"
      project.save
      mexico = Country.find_by(iso3166CountryCode: "MEX")
      mexico.projects << project
      get "/api/v1/get_project_count"
      res = JSON.parse(@response.body)
      assert_equal 1, res[0][1]
    end
Enter fullscreen mode Exit fullscreen mode

This test creates a single Country object and assigns it a new Project. Then it sends a request to the "get_project_count" endpoint. The assertion I wrote tests whether the JSON response includes the single project that belongs to Mexico.

Users Controller

I next wanted to test my endpoints in the Users controller, another vital set of functions in EffectiveDonate. The User actions I will be testing involve things like creating a new user, returning all of a user's saved projects, verifying whether a project has been starred by a user, and updating a user's information.

Many of the tests in this file will use the following data:

  user_params = { user:{
    username: "milan123",
    password: "abc123",
    first_name: "Milan",
    last_name: "Dhar",
    default_country: "USA",
    theme1: 14,
    theme2: 15,
    theme3: 16
    }
  }

  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/"
  project.title = "Nourish a Young Brain Protect One Ancient Culture"
  project.save
Enter fullscreen mode Exit fullscreen mode

Creating a New User

  test "should create new user" do
      post "/api/v1/users", params: user_params.to_json, headers: { "Content-Type": "application/json" }
      assert_response(:success, message = "failed to create user")
    end
Enter fullscreen mode Exit fullscreen mode

This test will attempt to create a new User object using the user_params listed above. If the response is not "success", this test will fail with a message of "failed to create user".

Updating a User

test "should update user" do
      user = User.create(user_params[:user])
      put "/api/v1/users/#{user.id}", params: {id: user.id, first_name: "Dylan"}.to_json, headers: { "Content-Type": "application/json" }
      res = JSON.parse(@response.body)
      assert_equal 'Dylan', res["first_name"]
   end
Enter fullscreen mode Exit fullscreen mode

This test sends a new first name as a parameter to the "/users/:user_id" endpoint. It then checks to make sure the first_name in the response JSON is equal to "Dylan", the new first name.

Return All a User's Saved Projects

test "should get all user's saved projects" do
      user = User.create(user_params[:user])
      user.projects << project
      starred_project_params = {user_id: user.id, project_id: project.id}
      post "/api/v1/get_user_projects", params: starred_project_params.to_json, headers: { "Content-Type": "application/json" }
      assert_response(:success, message = "failed to get all projects")
    end
Enter fullscreen mode Exit fullscreen mode

This test assigns the existing project as a favorite of a new user. It then sends a post request to the "get_user_projects" endpoint, sending the user_id and project_id as params. If the response is returned successfully, the test will pass.

Verify a Project is Starred

test "should verify that a project is starred" do
       User.delete_all
       user = User.new(user_params[:user])
       user.projects << project
       user.save
       post "/api/v1/check_star", params: {user_id: user.id, project_id: project.id}.to_json, headers: { "Content-Type": "application/json" }
       status_hash = JSON.parse(@response.body)
       assert_equal 'Project is starred.', status_hash["status"]
     end
Enter fullscreen mode Exit fullscreen mode

This test is similar to the previous one, but it pings the "check_star" endpoint and checks the "status" of the response to ensure that the project has been correctly recognized as "starred". The check_star method in the Users Controller includes the following render statement if the project is starred by the user: render json: {status: 'Project is starred.'}, so the test's response is able to parse out the correct status.

Auth Controller

The final controller in EffectiveDonate that I will discuss for testing purposes here is the Auth controller. This controller's only function is to authorize a user has signed in with the correct credentials, and if so, grant them a JWT so they can proceed to the website. Here is the test I wrote (using the same user_params as previous tests):

test "should authorize user on login" do
    user = User.create(user_params[:user])
    post "/api/v1/login", params: user_params.to_json, headers: { "Content-Type": "application/json" }
    assert_response :success, "Incorrectly denied user access"
  end
Enter fullscreen mode Exit fullscreen mode

This test sends the user_params to the "/login" endpoint, although it will just check for the username and password of the user. If the controller function works correctly, it will authorize the user with a jwt token and a successful status code, thus passing the test.

Conclusion

Overall, I found functional tests to be a natural next step from writing unit tests, as they were much more involved and as such, more likely to encounter errors. I made some silly mistakes, like forgetting my route paths started with "/api/v1/", and not converting my parameters to json, but these were good learning experiences. Also, writing these functional tests helped me to understand more deeply how my controllers worked, and to feel more confident that the user experience will be as intended. Thanks for reading!

Top comments (0)