Skip to content

Instantly share code, notes, and snippets.

@darrencauthon
Created May 9, 2012 03:02
Show Gist options
  • Save darrencauthon/2641469 to your computer and use it in GitHub Desktop.
Save darrencauthon/2641469 to your computer and use it in GitHub Desktop.
Sample Ruby Unit Tests v. Specs
require 'minitest/autorun'
class Employee
attr_reader :first_name, :last_name
def initialize(first_name, last_name)
@first_name = first_name
@last_name = last_name
end
def full_name
"#{@last_name}, #{@first_name}"
end
end
describe "Employee" do
before do
@first_name = Object.new
@last_name = Object.new
@employee = Employee.new(@first_name, @last_name)
end
it "should set the first name" do
@employee.first_name.must_equal @first_name
end
it "should set the last name" do
@employee.last_name.must_equal @last_name
end
end
class TestEmployee < MiniTest::Unit::TestCase
def test_full_name_is_last_name_plus_first_name
Employee.new("John", "Galt").full_name.must_equal "Galt, John"
Employee.new("Howard", "Roark").full_name.must_equal "Roark, Howard"
end
end
@darrencauthon
Copy link
Author

We know that names are practically always treated as strings, and we naturally assume that users of the Employee class will pass in strings as the first name and last name as strings. But is there anything specific in my code that requires that they be strings? No. The class constructor will take in any two objects in the constructor, and will return the appropriate one with the first_name and last_name methods.

By testing these methods with an object that could only be instantiated by the test, I can protect myself from my a-hole adversarial pair programming partner! :D (j/k). The simplest way to implement the attr_reader won't be a hardcoded string, it will be the code that we'd want anyway.

@darrencauthon
Copy link
Author

But I think the bigger point, beyond that, is the TestEmployee class. This class contains a unit test that tests the full_name method, verifying that it returns the first and last names in a properly formatted string. I sent two examples through it in the unit test, both to cover the tiny bit of logic that I had to put into the full_name method.

My concern is that with the "describe Employee" spec, there's not much room to fit this type of quick test. Whether I use @employee, or let(:employee){...} or whatever, the additional complexity that RSpec or even MiniTest's describe would make a quick two-line, two-test method into a ceremonial dance... OR.... I just drop the second test and say that it's not worth the trouble.

I'm not saying that's the wrong thing to do in all cases, but there are a couple reasons why I'd want to avoid it:

1.) I don't write tests to satisfy a pair partner or even just to verify that it runs in a specific situation -- I want to flex the code. I want to make it run in many situations because that's what will happen in production. I don't think it takes a lot of flexing for a simple "full_name" method.

2.) The more "orthodox" approach to TDD gives you something special: It tests your tests. When you write simple tests implemented by simple code (even to the point of hardcoding the result), you're pushed into a process where an invalid test won't be able to be satisfied because it will conflict with the assertions you make in other tests. Obviously, you can still make a mistake, but you'd have to make 2 or 3 mistakes in a row for the problem to hit production. Compare that to one.

I was reminded of this when I wrote the test above. I swapped "Howard" and "Roark" in the constructor, and after I implemented the code I thought should work I still got a failing test. The code looked right, so I checked my test... ah, there's the problem.

The way I look at it: If I'm not able to write the correct code without tests, how can I be expected to write the correct test code without tests for it? This isn't a when-do-you-stop situation, it's a just-use-TDD situation.

@mbleigh
Copy link

mbleigh commented May 9, 2012

describe Employee do
  it '#full_name should be first name plus last name' do
    Employee.new("John", "Galt").full_name.should == "Galt, John"
    Employee.new("Howard", "Roark").full_name.should == "Roark, Howard"
  end
end

Same number of lines, more descriptive. The let, subject etc. are there to DRY up several or dozens of tests, for a single test yeah there's no reason to use them.

@darrencauthon
Copy link
Author

Would I keep this within the same scope as the other? With @employee set? Or would this just be another describe block?

@darrencauthon
Copy link
Author

BTW :) I don't mean to ask piddling questions -- this issue, and how I've seen it handled by good Ruby/Rails devs, has been hard on my mind for a while.

@mbleigh
Copy link

mbleigh commented May 9, 2012

It's all just convention, do what feels right. I typically only nest describes when I'm writing more than one test for a method. A single test I just do a root-level "it" block.

describe Employee do
  describe '#initialize' do  
    it '#first_name should be set from the first argument' do
      Employee.new('Bob', 'Charlie').first_name.should == 'Bob'
      Employee.new('Rob', 'Charlie').first_name.should == 'Rob'
    end

    it '#last_name should be set from the second argument' do
      Employee.new('Bob', 'Marley').last_name.should == 'Marley'
      Employee.new('Rob', 'Charlie').last_name.should == 'Charlie'
    end
  end

  it '#full_name should be first name plus last name' do
    Employee.new("John", "Galt").full_name.should == "Galt, John"
    Employee.new("Howard", "Roark").full_name.should == "Roark, Howard"
  end
end

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment