Finished a Bootcamp? Time to level up your Testing

Finished a Bootcamp? Time to level up your Testing

I completed the Turing School of Software Design this past January. Since that time, I've gotten the chance to work on production applications that have helped me appreciate some strategies for testing at scale applications that as a student I would not have fully understood. In this post I will show a testing pattern that I used heavily as a student as well as the design pattern I now use in its stead. I want to make the point that the latter pattern is superior because it is more deliberate in terms of where various pieces of functionality get tested.

How I Used to Test ModeL Validations

Let's imagine an application that shows off photos. As an admin I can create, delete, and edit photos and as a regular user I can view photos and comment on them. Let's say that everyday a new photo is published. Lets also say that the application has views that allow users to see one photo as well as an index of photos.

Here's a test for the the photo model in spec/models/photo_spec.rb:

require 'rails_helper'

RSpec.describe Photo, type: :model do

  context "validations" do
    it { is_expected.to validate_presence_of(:url) }
    it { is_expected.to validate_presence_of(:title) }
    it { is_expected.to validate_presence_of(:description) }
    it { is_expected.to validate_presence_of(:date) }
    it { is_expected.to validate_uniqueness_of(:url)}
  end

end

That looks good. The above code checks to make sure that every photo has a url, title, description and date. It also checks that teach url is unique because we definitely do not want any duplicate photos in our application. There is nothing wrong with that kind of testing. 

How I now Test Model Validations

Except there kind of is something wrong. Imagine if this was a much more complicated model with many different attributes and validation rules. Sure we could just list all the validation rules, as we have done in the above code, but why not just add a little more organization to the test suite to show your fellow developer working on the code that you care. 

# spec/models/photo_spec.rb
RSpec.describe Photo, type: :model do
  describe "validations" do
    describe "presence" do
      it { should validate_presence_of    :title }
      it { should validate_presence_of    :caption }
      it { should validate_presence_of    :date }
    end
  end

  describe "associations" do
    it { should have_many(:comments).dependent(:destroy) }
  end

end

What I like about the above code better than the first snippet is that tests are grouped together into logical chunks that make it easier to read. First we are going to describe the validations, and in particular we'll describe "presence" validations. Then we go on to list the attributes whose presence are required. We can imagine having a uniqueness or some other section in this "validations" describe block. Next we have a separate describe block for associations.

It's not like this speeds up the testing, but it does make it easier for another developer to find what they are looking for in the code. That's really the biggest difference between bootcamp and actual professional grade software development. In a bootcamp, I'm writing software for myself. I may often pair with a fellow student, but it's rare to have to go back to code after more than a week or two. In that context, writing highly organized tests is not that important, but in the professional context organization and readability become paramount concerns.

Another test I now like to add on my models now is to check that there is a valid factory associated with each model. It's a quick win and a proverbial canary in the coal mine for schema changes down the development road.

describe "valid factory" do
  it "has a valid factory" do
    expect(build(:photo)).to be_valid
  end
end

How I Used to test methods on my model

Lets imagine that for one of our index pages we want to see photos within various time ranges. Maybe I want to see all the photos of a given month and year or the last photo of each month or a set of photos from a given month and year.

# spec/models/photo_spec.rb
require 'rails_helper'

RSpec.describe Photo, type: :model do
  it "returns unique month and year" do
    create :photo, date: Time.parse("2015-12-29")
    create :photo, date: Time.parse("2016-12-29")
    create :photo, date: Time.parse("2015-1-29")

    expect(Photo.unique_month_years).to eq(["12 2015", "12 2016", "01 2015"])
  end

  it "returns photos by month and year" do
    photo_1 = create :photo, date: Time.parse("2015-12-29")
    photo_2 = create :photo, date: Time.parse("2015-12-30")
    create :photo, date: Time.parse("2016-12-29")
    create :photo, date: Time.parse("2015-1-29")

    expect(Photo.month_year("12", "2015")).to eq([photo_1, photo_2])
  end

  it "returns the last photo of each month" do
    photo_1 = create :photo, date: Time.parse("2015-12-29")
    create :photo, date: Time.parse("2015-12-30")
    photo_2 = create :photo, date: Time.parse("2015-1-29")
    photo_3 = create :photo, date: Time.parse("2016-12-29")

    expect(Photo.get_first_photo_of_each_month).to eq([photo_1, photo_2, photo_3])
  end
end

That looks good. The above code has three tests that each create some photos with particular attributes, call the method in question on the Photo class and then make assertions on the results. Again, nothing wrong per se, but we can make it better.

How I now test methods on my models

For this, I'm going to show one example of a test and contrast some of the style choices I make with the above tests.

# spec/models/photo_spec.rb
require 'rails_helper'

describe Photo, "#by_year_and_month(year, month)", type: :model do
  context "when the class method by_year_and_month is invoked" do
    it "returns all the photos in a given year month pair" do
      create :photo, date: "2016-11-30"
      create :photo, date: "2016-11-2"
      create :photo, date: "2015-11-30"
      create :photo, date: "2016-12-1"

      result = Photo.by_year_and_month("2016", "11")

      expect(result.length).to eq(2)
      expect(result.pluck(:date)).to include(Date.new(2016,11,30))
      expect(result.pluck(:date)).to include(Date.new(2016,11,2))
    end
  end
end

Here I'm making much better use of the Rspec domain specific language by writing more articulate describe, context, and it blocks. First the describe block clearly states what method is about to be tested. Next I have a context block, which I guess could be a second describe block, describing the situation I'm trying to test. I think contexts are actually better suited for testing various sad and happy paths in a method, but I digress. Finally, I have an inner it block that explains what the method is expected to return. None of that speeds up the test, but it does clue in my fellow developers exactly what the method does.

Also, notice that I take a page out of Justin Searls' playbook where I divide my tests into three clear sections. First I have the setup section. Next I call the method. Third, I make my assertions. I use the variable name "result" to be very clear that this is the thing that is the outcome of my test. If I needed to call this method on a specific photo, I might also use a variable "subject" to signify that this is the subject of the test. This isn't game changing, but it is small readability enhancements that add professional polish to the suite.

How I used to Request test

Let's imagine an endpoint that serves up one photo per month given. In the past I might have written something like this:

require 'rails_helper'

describe "GET api/v1/photos/one-per-month", type: :request do
  it "returns the first photo of each month for a given year as json" do
    photo_1 = create :photo date: Date.new(2016,1,1)
    photo_2 = create :photo date: Date.new(2016,2,1)
    create :photo date: Date.new(2016,1,2)
    create :photo date: Date.new(2017,1,3)

    sign_in user

    get "/api/v1/photos/one-per-month?year=2016"
    photos = JSON.parse(response.body)

    expect(response).to be_success
    expect(photos.first["id"]).to eq(photo_1.id)
    expect(photos.second["id"]).to eq(photo_2.id)
  end
end

Here I create four photos. I perform the get request. I parse the response, and then I make assertions on that parsed response.

How I Now Write Request Specs

require 'rails_helper'

describe "GET api/v1/photos/one-per-month", type: :request do
  let(:photos) { double }
  let(:user) { create :user }
  it "returns the first photo of each month for a given year as json" do
    expect(Photo).to receive(:one_per_month).with("2016").and_return(photos)
    sign_in user

    get "/api/v1/photos/one-per-month?year=2016"

    expect(response).to be_ok
  end

  it "returns 302 if a user is not signed in" do
    allow(Photo).to receive(:one_per_month).with("2016").and_return(photos)

    get "/api/v1/photos/one-per-month?year=2016"

    expect(response).to have_http_status(302)
  end
end

I've already tested the Photo.one_per_month method on my model. I don't need to test it in my request because I already know it works. The only thing I need to test in my request is the piping that fits everything together. This is the biggest testing epiphone for me since finishing Turing.

In the above code, I use let, which lazily invokes photos and a user. That means that they aren't actually created unless they are called. This is faster than creating things and not calling them. Next my photos aren't photos at all! They are a double, which is just a thing. This again is much faster than actually creating photos. Now the only thing that I really need to test here is that when a get request is made to this endpoint, the method one_per_month is called on the Photo class and that the response is ok. I don't care about what is in the response here, because I know what is in response from my model testing of this method. 

So I'm going to expect that the photo class receives the method call one_per_month with argument "2016". This shows me that the year parameter on the get request is being parsed correctly and handed off to the right class method. I know the method works, so I'm not going to bother testing that here.

The result of this approach is that the former request spec might take a second or two to run, where as the latter runs in much less than a second. 

The application I work on professionally has close to 2000 tests. The largest application I worked on at Turing had about 100 or so tests. When working with 100 tests, second or two long tests still gives you a test suite that runs in a minute or two. Running a similarly structured test suite that is ten times as large would mean a total run time of ten minutes. The application I work on has a test suite runtime of 30 - 45 seconds. That's a big deal.

Thanks for reading along. I hope this helps my fellow Turing comrades and other new developers thinking about upping their testing game.