TDD: Is it worth it? Let me work it.

Posted by hcarnes on August 22, 2018

I'm currently looking to join an awesome team of developers to continue my career development. If that's your team, please book a meeting with me at your convenience.

Writing tests before you develop may help you write better code. I didn’t understand the full extent of the benefits of TDD until I actually wrote tests myself and then incrementally wrote code based on those tests. This post will share how I got started writing tests in Rails. I will walk you through getting started by creating a simple Rails API and then writing a test that determines whether or not requesting a specific resource will result in rendering the data you want.

This post discusses getting started in Test Driven Development using Minitest, the default testing library used by Rails. Another popular testing library is Rspec. There are pros and cons of both Minitest and Rspec and I recommend doing some research on which one would work best for your needs before choosing one or the other.

What is TDD?

Using software to run tests that generates errors if your code doesn’t give the results you want.

What is the alternative?

Manual testing, which involves running the code in the browser and seeing what happens. As a result, there will be clicking around and possibly not getting errors because maybe something isn’t working how you hoped but doesn’t generate an error.

Starting with the end goal in mind

My goal was to create a pizza shop API that will render pizza topping options (cheese, pepperoni, olives) in JSON, so that the frontend can use the data down the road.

Setting things up

I created a new API called pizza_shop, and I used Postgres for the database. I chose to get started with Postgres because I knew I would want to deploy this to Heroku down the road.

rails new pizza_shop --api --database=postgresql

With the goal of rendering JSON for each pizza topping in mind, I started by generating the Topping model and controller.

rails g model Topping moniker:text price:money

rails g controller Topping index

Next, I updated the controller so that it would render JSON for my toppings.

class ToppingsController < ApplicationController
 def index
   render json: Topping.all
 end
end

Next, I updated my config/routes.rb file.

Rails.application.routes.draw do
  get '/toppings', to: 'toppings#index'
end

Next, I created the database.

rake db:create

Next, I migrated the database.

rake db:migrate

The test skeleton

rails new…--api creates a test directory by default. If you generate a controller, Rails will add a corresponding test folder. When running rails new…--api, the controller test will inherit from ActionDispatch::IntegrationTest by default. If you followed the steps up to this point, the default test stub for the Topping controller will look similar to this:

require 'test_helper'
 
class ToppingsControllerTest < ActionDispatch::IntegrationTest
  # test "the truth" do
  #   assert true
  # end
end

According to the Rails docs, when you require test_helper.rb you are loading the default configuration to run your tests. This should be included with all the tests you write, so any methods added to the file you are working in are available to all tests.

The class above defines a test case because it inherits from ActionDispatch::IntegrationTest, which gives access to all the methods available from ActionDispatch::IntegrationTest. According to the Rails docs, an integration tests exercises the full stack from the dispatcher to the database.

Fixtures & Setup

In the test folder generated by Rails, there is also a fixtures folder that holds mock data to be tested. They are stored in YAML files and one file is generated per model. So, for the Topping model, we have a toppings.yml fixture. I added the following data to this file:

pepperoni:
  moniker: Pepperoni
  price: "5.00"

tofu:
  moniker: Tofu
  price:  "0.20"

sausage:
  moniker: Sausage
  price:  "5.20"

green_olives:
  moniker: Green Olives
  price:  "2.20"

anchovies:
  moniker: Anchovies
  price:  "3.00"

The test is going to access the Topping fixture data, so this can be set up using the setup method, which is provided by ActiveSupport::Callback. The setup method will pull the data from the Topping fixture.

require 'test_helper'

class ToppingControllerTest < ActionDispatch::IntegrationTest
 setup do
   @toppings = Topping.all
 end
end

Write the test

It’s time to write the test. Here is the full test I implemented. I will walk you through how I developed this full test, but I think it’s nice to see everything up front and then break things down step by step.

 test "should list toppings as json" do
   get topping_index_url, as: :json
   assert_response :success
   @toppings.each do |topping|
     assert_includes(response.parsed_body, {moniker: topping.moniker, price: topping.price.to_s}, ['here is the message'])
   end
 end

Notice the syntax for the test method looks different than a Ruby method. Under the hood, Rails is actually generating a method by replacing spaces with underscores. This let’s you write more explicit names for your tests without having to worry about adding an awkward amount of underscores to your methods. So, the test above is actually:

 def test_should_list_toppings_as_json
	#block 
 end

get topping_index_url, as: :json calls the :index action on the Toppings controller.

assert_response :success tests to make sure that our request was 200.

For the final block of code, I knew I wanted the toppings to including the topping id, moniker, and price. So, I needed to find an assertion method that will let me test this. There are a lot of assertions to explore in the Rails docs. The one that fits my needs here is assert_includes( collection, obj), which ensures that an object is in a collection. When I first wrote, this test it failed because I still needed to implement active model serializers for the Topping model. Once I did this, I was able to get my test to pass. This one example of how writing tests helps you incremently write your code.

As you write your tests, be sure and run rake test to see how your errors can help you.

Is it worth it?

This post covered getting started with test driven development in Rails. Is it worth it? I think it’s worth it. Based on my own research, it takes more time because you have to write tests as you write code, but keep in mind that the goal is for these tests to save you lots of time in the long run. If you have well-written tests, you can make changes to your code and feel confident that your code is still working as you originally planned. The only time that tests can make your life worse would be in the case that they are poorly written or that they are not maintained. In that case, they could possibly create more problems than they solve. Overall, I think Test Driven Development has some awesome benefits.