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.