How does anyone learn to test?

How does anyone learn to test?
Photo by Hans-Peter Gauster / Unsplash

This week I got to the part of the "Learn Enough to Be Dangerous" Rails Tutorial where you write a couple of tests for a static page controller. The test it asks you to write looks like this:

class StaticPagesControllerTest < ActionDispatch::IntegrationTest
  test "should get home" do
    get static_pages_home_url
    assert_response :success
    assert_select "title", "Home | Ruby on Rails Tutorial Sample App"
  end
end

That second assertion isn't strong as it looks. It's possible for it pass even if the title that displays to the user is something entirely different. It checks that a tag of that name contains that value. If you happen, perhaps purely out of curiosity, to add a second title tag instead of changing the first title tag, the site will display the first title, but the test will still pass.

So I did a little research, decided there should never be more than one title tag, and added an assertion for that.

class StaticPagesControllerTest < ActionDispatch::IntegrationTest
  test "should get home" do
    get static_pages_home_url
    assert_response :success
    assert_select "title", "Home | Ruby on Rails Tutorial Sample App"
    assert_select "title", 1
  end
end

Note that the assert_select method does something very different when it's passed an integer than when it's passed a string. It doesn't check for equality of the tag value, it checks that the tag returns that number of results.

This got me curious about what exactly that method was doing. It also got me itching to switch over to RSpec, because the plain English meaning of these tests isn't at all clear, and "clear plain-English meaning" is one of the main reasons I like writing Ruby in the first place.

"Switching out for RSpec at this point will be a good exercise," I said to myself. "I can follow along the rest of the tutorial but I'll have to write my own tests."

I set up RSpec. I generate some stub files for the controller I'm working on. I notice that it also generates tests for the views, and I spend a minute thinking about whether I want to test the behavior I care about in a request test or in a view test.

I decide on request. The view tests look like they're running specifically against the parts of the views that don't come from the shared layout file. Some of the things I'm trying to test are coming from the shared layout file at the moment, but I don't care about that particular detail and I don't want it encoded into my tests right now.

The RSpec tests ended up looking something like this

require "rails_helper"

RSpec.describe "static pages", :type => :request do

  it "works" do
    get "/static_pages/home"
    expect(response).to have_http_status(:success)
    expect(response).to have_tag("title", "Home | Ruby on Rails Tutorial Sample App")
  end
end

Unfortunately, this doesn't work. The response object doesn't respond to methods that the have_tag relies on.

"Hmm," I say to myself. "That's interesting. Since have_tag is allegedly a wrapper around assert_select, this should be a good opportunity to learn more about how assert_select works under the covers."

I start trying to figure out what "response" is. It's an ActionDispatch::TestResponse, but what exactly is that? How is that object getting created and assigned to the variable?

get looks like it returns a response, but there's no assignment expression in my code, so it's getting into the execution environment some other way. get is understandably hard to search for, so I fire up pry and run ls

From: /Users/notabennett/workspace/environment/sample_app/spec/requests/static_pages_spec.rb:7 :

     2: require 'pry'
     3:
     4: RSpec.describe "StaticPages", type: :request do
     5:   describe "GET /home" do
     6:     it "returns http success" do
 =>  7:       binding.pry
     8:       get "/static_pages/home"
     9:       expect(response).to have_http_status(:success)
    10:       assert_select "title", "Home | Ruby on Rails Tutorial Sample App"
    11:     end
    12:   end
    
    [1] pry(#<RSpec::ExampleGroups::StaticPages::GETHome>)> ls

Ah, and there it is:

Rails::Controller::Testing::Integration#methods: delete  get  head  patch  post  put

So I go look this thing up.

"This gem brings back assigns to your controller tests as well as assert_template to both controller and integration tests."

Hmmm.

"These methods were removed in Rails 5."

Hmmmmm.

It's at this point that I give up, close out the RSpec branch in my git repo, and work on something else for a while.

As I'm writing this up I notice that the linked issue specifically recommends switching to assert_select, which opens up a whole new avenue of investigation. I can delete the rails-controller-test without anything breaking, since it turns out this test isn't using it.

But at the time, I was just thinking: I hate this.

Is there some kind of version incompatibility between rspec-rails and rails-controller-test? Is there something wrong with the way the tutorial is teaching me to write tests? Is there just something slightly wrong with that rspec test, and I just need to understand a little better what the test is doing? What am I testing, anyway? Are these tests still going to work two years from now?

Should I write larger tests, using capybara? Should I write smaller tests, for just the views?

Is this "controller tests or not" argument that these guys are having on the issue a real argument about a real technical judgement that I need to understand?

Or is it just posturing over aesthetics?

I don't have any actionable takeaway here. I've been thinking about it all week, and I still don't know what it means, man.

On some level this is why I love testing, and I love writing tests first. It helps me get in deep into the details of what I'm doing, rather than staying at the surface "yay Rails magic" level.

But:

When I google "ActionDispatch::TestResponse", not a single response in the first ten responses explains why and how Rails is generating this object instead of a "real" one. Or what the "real" response would look like, or be.

And I don't even know that searching on that specific term will solve my problem. I don't know enough about the domain to be able to learn more efficiently.

The expectation of this whole surface layer of the ecosystem appears to be "you're learning, so just jam bits of code you found in a README or a tutorial together, you can worry about how it all fits together later." And then as soon as I start digging in, the expectation shifts to "obviously you've been doing this for years on large code bases, so we don't need to explain the basics"

Where is that middle layer in between "brand new" and "expert?" How does anyone – who doesn't have hours a week to devote to learning – make any sense of this?