Skip to the content.

Writing Tests: Moving from Methods to Classes

Once I decided to get started with TDD in Ruby, I developed a perhaps premature sense of pride in my ability to use TDD to write methods. Following the red-green-refactor model, I would write a test for a method, write the code to make it pass, revise for elegance and maintainability, and then repeat those steps until the method accomplished everything I wanted. But as soon as I got ambitious and started trying TDD with classes, I ran into some trouble: a NameError telling me of some “undefined local variable or method.”

What was going wrong? Well, as I progressed from files containing single methods to full classes, I was writing tests for instance methods without initializing an instance of that class upon which I would call those methods in my tests.

So, for example, lets say I wanted to write a method that returns “Hello, world!” Simple enough. First, I’d write a test in my project’s spec directory:

require 'hello_world'

describe "hello_world" do
  it "returns 'Hello, World!'" do
    expect(hello_world).to eq("Hello, world!")
  end
end

Watch it fail. Generate a blank file in my projects lib directory named hello_world.rb. Watch it fail again. Now add some code:

def hello_world
  "Hello, world!""
end

Run the tests again. Everything passes. Great!

But what if I wanted that method to be nested in a class? Well, I’ve already written the tests, so lets add the class and see what happens. Back to the hello_world.rb file:

class HelloWorld
  def hello_world
    "Hello, world!"
  end
end

Run the test and disaster ensues:

$ rspec spec
F

Failures:

  1) hello_world returns 'Hello, world!'
     Failure/Error: expect(hello_world).to eq("Hello, world!")
     NameError:
       undefined local variable or method 'hello_world' for #<RSpec::ExampleGroups::HelloWorld:0x000001011c6ba8>
     # ./spec/hello_world_spec.rb:5:in 'block (2 levels) in <top (required)>'

Finished in 0.00071 seconds (files took 0.15835 seconds to load)
1 example, 1 failure

Failed examples:

rspec ./spec/hello_world_spec.rb:4 # hello_world returns 'Hello, world!'

How can this be? I haven’t changed the method!

Well, now that I’ve nested my (instance) method inside of a class, the test needs to initialize an instance of that class upon which it can call my method. Changing the test like this solves our problem:

require 'hello_world'

describe "hello_world" do
  it "returns 'Hello, World!'" do
    my_instance = HelloWorld.new
    expect(my_instance.hello_world).to eq("Hello, world!")
  end
end

So, altogether, not too complicated. In order to test an instance method, your tests need to initialize an instance that can call that method.

Bonus side note: if you’re testing multiple instance methods, you can DRY up your code by initializing your instance just once:

require 'hello_world'

describe "hello_world" do
  let(:my_instance) {described_class.new}

  it "returns 'Hello, World!'" do
    expect(my_instance.hello_world).to eq("Hello, world!"")
  end
end

Though, I must advise that you proceed with caution, as I’ve heard that relying on “let” isn’t necessarily a best practice.