The value of unit tests and TDD are well documented. Unfortunately, it’s still daunting to start practicing it. Here’s a primer.

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

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.

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

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

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.

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.

If you found this useful, you might also enjoy: