The value of unit tests and TDD are well documented. Unfortunately, it’s still daunting to start practicing it. Here’s a primer.
Our desired code
We need to write a method that:
- takes a string as an argument
- reverses the string
- saves the string to a file on the hard drive
- returns the reversed string
Our tool and techniques
We’ll use RSpec to test our method’s output. We’ll use red/green/refactor methodology. We’ll write unit tests that are fast, isolated, repeatable, self-verifying, and timely.
Starting with desired end state
What’s a good first test we can write? We know that given a particular input (a string “example string”) should product a given output (the string “gnirts elpmaxe”):
describe StringChanger do
it 'reverses strings' do
string_changer = StringChanger.new
reversed_string = string_changer.reverse_and_save('example string')
expect(reversed_string).to eq 'gnirts elpmaxe'
end
end
Now let’s run our test. We’ll use the documentation format, which is more verbose, but for the sake of this example will give us more information about the tests:
% rspec string_changer_spec.rb --format documentation
We get the following output:
string_changer_spec.rb:3:in `<top (required)>':
uninitialized constant StringChanger (NameError)
The test is telling us to create a StringChanger
class. Let’s write it:
class StringChanger
end
When we run our test, we get this output:
StringChanger
reverses strings (FAILED - 1)
Failures:
1) StringChanger reverses strings
Failure/Error: reversed_string = string_changer.reverse_and_save('example string')
NoMethodError:
undefined method `reverse_and_save'
for #<StringChanger:0x007fafe5c66ff0>
# ./string_changer_spec.rb:8:in `block (2 levels) in <top (required)>'
Finished in 0.00032 seconds
1 example, 1 failure
Failed examples:
rspec ./string_changer_spec.rb:5 # StringReverAndSave reverses strings
Our test is telling us we need to write a method called reverse_and_save
.
Let’s write it:
class StringChanger
def reverse_and_save(string_to_reverse)
end
end
We run our test again:
StringChanger
reverses strings (FAILED - 1)
Failures:
1) StringChanger reverses strings
Failure/Error: expect(reversed_string).to eq 'gnirts elpmaxe'
expected: "gnirts elpmaxe"
got: nil
(compared using ==)
# ./string_changer_spec.rb:10:in `block (2 levels) in <top (required)>'
Finished in 0.00301 seconds
1 example, 1 failure
Failed examples:
rspec ./string_changer_spec.rb:5 # StringChanger reverses strings
The simplest thing that could work
Our test is telling us our method’s logic does match expectations. Let’s make it pass:
class StringChanger
def reverse_and_save(string_to_reverse)
'gnirts elpmaxe'
end
end
We run our test:
StringChanger
reverses strings
Finished in 0.00079 seconds
1 example, 0 failures
Refactoring
We have a passing test, but there is a smell we can refactor out. Currently we are returning a hard-coded value. This will not work. Let’s do a little refactor and then depend on our tests to tell us if things are still good:
class StringChanger
def reverse_and_save(string_to_reverse)
string_to_reverse.reverse
end
end
We run our test:
StringChanger
reverses strings
Finished in 0.00079 seconds
1 example, 0 failures
Our test still passes but our method is not fully functional until we write our reversed string to a file.
Stubbing dependencies such as the file system
We’re writing a unit test, which should be isolated. We don’t want to create a new file every time we run our test, so we stub it out.
The RSpec Mocks library gives
us the ability to send the stub
message to an object and replace the method
we’re stubbing with a dummy method that doesn’t do anything:
it 'saves string to the file system' do
string_changer = StringChanger.new
File.stub(:write)
string_changer.reverse_and_save('example string')
expect(File).
to have_received(:write).
with('example_file', 'gnirts elpmaxe').
once
end
We expect the File
object to receive the message write
one time with the
arguments 'example_file'
and our reversed string by using the stub’s ability
to report on the messages it’s received. This is called
white-box testing.
We run our test:
StringChanger
reverses strings
saves string to the file system (FAILED - 1)
Failures:
1) StringChanger saves string to the file system
Failure/Error: expect(File).to have_received(:write).with('example_file', 'gnirts elpmaxe').once
(<File (class)>).write("example_file", "gnirts elpmaxe")
expected: 1 time with arguments: ("example_file", "gnirts elpmaxe")
received: 0 times with arguments: ("example_file", "gnirts elpmaxe")
# ./string_changer_spec.rb:19:in `block (2 levels) in <top (required)>'
Finished in 0.00106 seconds
2 examples, 1 failure
Failed examples:
rspec ./string_changer_spec.rb:13 # StringChanger saves string to the file system
Our test is telling us to invoke the File
object’s write
method. Let’s
write it:
class StringChanger
def reverse_and_save(string_to_reverse)
File.write('example_file', string_to_reverse.reverse)
end
end
We run our test:
StringChanger
reverses strings (FAILED - 1)
saves string to the file system
Failures:
1) StringChanger reverses strings
Failure/Error: expect(reversed_string).to eq 'gnirts elpmaxe'
expected: "gnirts elpmaxe"
got: 14
(compared using ==)
# ./string_changer_spec.rb:10:in `block (2 levels) in <top (required)>'
Finished in 0.00136 seconds
2 examples, 1 failure
Failed examples:
rspec ./string_changer_spec.rb:5 # StringChanger reverses strings
We made our new test pass but broke our old test because we are no longer returning the reversed string. This shows the benefit of old tests helping guard against regressions in our code.
Let’s fix our original test:
class StringChanger
def reverse_and_save(string_to_reverse)
string_to_reverse.reverse.tap do |reversed_string|
File.write('example_file', reversed_string)
end
end
end
We run our test:
StringChanger
reverses strings
saves string to the file system
Finished in 0.0011 seconds
2 examples, 0 failures
We now have two passing tests and a fully functional method. We did it writing the tests first. This is a simple example, but by following this process, we can conquer any sized programming task. Writing the tests first will also help us write testable code and help us keep our methods small.
What’s next
If you found this useful, you might also enjoy: