Functional Testing in Rails
Tags: rails, ruby, testing
Published: 2019-10-24 03:41:18

In [last week's post](https://dev.to/milandhar/unit-testing-in-rails-4oke), 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](https://balance3hplus.com/wp-content/uploads/2018/07/test-tubes-155769_1280-1024x854.png)


## Functional Testing 
In general, [functional testing](http://softwaretestingfundamentals.com/functional-testing/#targetText=FUNCTIONAL%20TESTING%20is%20a%20type,properly%20satisfied%20by%20the%20application.) 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](https://guides.rubyonrails.org/testing.html#functional-tests-for-your-controllers) 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 ](https://dev.to/milandhar/unit-testing-in-rails-4oke). 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](https://api.rubyonrails.org/classes/ActionController/TestCase.html) 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](https://guides.rubyonrails.org/testing.html#functional-tests-for-your-controllers) 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: 
``` ruby
    test "should get index" do
      get "/api/v1/countries"
      assert_response :success
    end
```

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: 

``` ruby
  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
```

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:

``` ruby
  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
```

### Creating a New User

``` ruby
  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
```

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

``` ruby
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
```

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
``` ruby
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
```
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
``` ruby
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
```
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](https://jwt.io/) so they can proceed to the website. Here is the test I wrote (using the same `user_params` as previous tests):

``` ruby
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
```

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!