Learn Rails testing by example - Part 1

When I first started learning web development, I had no idea what tests were. I’d heard of them and “tests” or “specs” would always show up in articles that I was reading, but I never took the time to learn what they were or why they were important. This stayed true when I got to Rails. In fact, proof of my disregard for testing still exists. I went through Michael Hartl’s incredible Rails Tutorial and could not wrap my head around testing. The concept of trying to get things to fail was backwards to me and it seemed like a waste of time. So, naturally, I skipped those sections. If you go to my Github profile you can still see those sample projects that have essentially no tests in them.

When I started writing Pelorus I skipped testing completely. Every time I made a change I’d just click through the app and make sure it worked. This led to one of the most embarrassing moments of my developer-career.

We decided to hire Thoughtbot to write our mobile app and spent the first week in San Francisco going through their Design Sprint process (which I highly recommend by the way). At the end, we’d decided on a base data structure and I casually told our developer, “this’ll be easy. I’ll be done in a couple hours.” He looked at me with some confused eyes. “Why don’t we start with some tests first” he said, “there’s an actual app, an API and in general there’s going to be a lot going on, we want to make sure we get it right.” I stared blankly, knowing my attempt to show off backfired and now I had to explain that I didn’t know what a test was. That was eight weeks ago…

What are tests and why are they so important?

In the development world, tests are pieces of code whose purpose is to test the functionality of the application code. The goal of testing is to build an automated “test suite” that can run and make sure your entire application is functioning correctly all of the time. It’s easy to think, “well why don’t we just, you know, click through the app and test it?” This works well enough for small applications but in the real world, the time it would take to test every part of a complex app is far greater than the time it takes to write automated tests as we go.

That process of testing as you build your app and writing tests first is called “test-driven development” or TDD. TDD has some benefits that I think make it a worthwhile practice.

  • It helps think through the behaviours of your app. Because tests are usually more readable than app code, it’s easier to tell the stories of how your users will interact with the app. users should see their posts when they land on the posts page and users should be directed to the new post form are valid descriptions that we may want to test for. A handy bi-product of this is that yours tests can function as documentation. It’s easy to go to a well tested app, jump into the test folder and reason about what’s going on.
  • As stated above, it saves a ton of time. Pelorus has an admin dashboard and an API that serves our mobile app. To manually test every bit of functionality in our app it’d require a significant time investment. We’d have to: log in, create some data, make sure graphs rendered properly, open Postman to GET and POST stuff to the various API routes. Waaay too much to do and more importantly, it’s easy to miss stuff. We’re not a “big” app by any means, but we have about 200 tests than run in ~15 seconds.
  • It allows you to refactor your code and ensure that whatever your changing still works properly. Every app needs some amount of refactoring whether you’re extracting functions, renaming variables or using a different algorithm. If you write your tests so they pass and refactor your code after, you have a guarantee that your refactor didn’t break anything (because if afterwards your tests fail, then you know you did something wrong)

What tests look like

There are a few different kinds of tests and to be totally honest, I’m not sure what all of them are or what they all mean. In my head, the two types that matter the most are Unit tests and Integration tests.

Unit Tests

In most cases, a Unit test tests the functionality of a specific method. Here’s an example: say you have a User class that has first_name and last_name attributes. We want to write a method called full_name that combines first_name and last_name with a space separating them. A test for that method might look something like this.

describe User do
  describe "#full_name" do
    it "should return a string with the users first and last name" do
      user = User.create(first_name: "John", last_name: "Doe")

      expect(user.full_name).to eq("John Doe")
    end
  end
end

What’s happening here should be pretty clear, and that’s the beauty of testing. We’re describing a method called full_name in our User class. We then describe what it is we expect to happen, in this case returning a string with the first and last names. In the test itself, we create a user with the first name “John” and the last name “Doe.” Then we write what’s called an assertion, or what we expect the result to be. In this case, we expect that when we call the full_name method on the user we just created, we expect to get the string “John Doe”. If we ran the test now, it’d obviously fail because there isn’t a full_name method yet.

A first pass at that method might look something like…

def full_name
  "#{first_name} #{last_name}"
end

If we ran our test now, we expect it to pass, and it does! Now let’s try to write the same method a different way.

def full_name
  first_name + last_name
end

Makes sense, we’re just adding the first_name and the last_name so let’s run our test again and see what happens. Oh no it failed! The test was expecting “John Doe” and got “JohnDoe” instead. We forgot to put the space between the first and last names! Oh well, easy fix.

def full_name
  first_name + " " + last_name
end

Run the test again and just like that, the test passes again. This process that we just went through is commonly referred to in TDD as “Red, Green, Refactor.” We write a failing test (red), get it to pass (green) and then refactor afterwards. This is a basic example, and in no way a great refactor. Just trying to demonstrate the Red, Green Refactor process and also show an example of how testing can help us find bugs in our code.

Integration Tests

Integration tests are a level above Unit tests. They test the integration between various units of our application. To be honest, in practice I’m not really sure what this means. I like this practical definition of Integration tests from the Rails guides: [Integration tests] are used to test how various parts of your application interact. They are generally used to test important workflows within our application. What I actually write is probably a combination between Integration tests and Functional tests but at the end of the day, I’m not sure what we call them matters.

I usually write these as Feature tests. They test a specific feature of the app and how the different pieces (view, controller, model etc) interact. We’re not going to go through a full, example like we did above - that’ll be for Part 2 of this series. But here’s an example of what a Feature test might look like (with some different syntax).

feature "user creates a new post" do
  scenario "they see the new post on the page" do
    user = create(:user)

    visit new_post_path(as: user)
    fill_in "Title", with: "My first post"
    fill_in "Body", with: "This is my first post about testing!"
    click_button "Submit"

    expect(user.posts.size).to eq(1)
    expect(page).to have_content "My first post"
  end
end

Again, what’s happening here is clear. We’re describing a feature where the user creates a new post and we want to make sure that when they create one, the post appears on the page. While diving into this test, I’d like to reference the pattern I use because we’ll use it in almost all tests in this series. It’s called Four phase testing. The phases are: setup, exercise, verify and teardown. In the first phase, we’ll setup the units that we need to complete the test. In this case, all we need to do is create a user. The next phase is exercise. Here we actually do the stuff that’s being tested. In the example, we first have to go to the new post page, then fill in the form with some basic content and then press the submit button. This is the same behaviour that we expect our users to have when they use the app for real. The last phase is to verify (or assert) that the result is what we expected. Here we expect the user to have exactly one post, and we want that post to appear on the screen.

That’s it. It may seem like a lot at first, but can you imagine actually going to your app, filling in a form and checking whether or not it worked every time you make a change? That’d be crazy! This one Feature testwill make sure our users can always post stuff correctly and will save us countless time and energy in the future.

What’s next?

This post is already a little too long so we’ll continue where we left off next time. Eight weeks ago I didn’t know what a test was, and no I couldn’t be a bigger proponent. Hopefully you’ve learned a bit about what testing is and why it’s a crucial part of development. In the rest of this series we’ll build a app from scratch following TDD principles. The following posts should help whether you’re just new to testing or new to Ruby on Rails altogether. Stay tuned, should have the next post up sometime this weekend!