Red Green Repeat Adventures of a Spec Driven Junkie

TDD In-depth Divide Part-1

This article is the first part of a series. I will go over how I expect Test Driven Development to be done in an in-depth manner.

Still Life - Violin and Music by William Michael Harnett

This is the same way I would do it if you paired with me and what I would expect from you if you were working with me.

How I will go about it:

  • with basic set up going (i.e. Ruby and Rspec running)
  • no files existing
  • add one test to ensure Rspec works
  • add another for the function

My goal: to demonstrate how I approach test driven development so others can learn and emulate. Test Driven Development can be done in a myriad of ways. Most always feel like reinventing the wheel.

The TDD wheel should not be reinvented every time. It’s a process that is repeatable.

The TDD process has been internalized by me and this is what I expect of others, which may not always be fair, especially on code reviews.

By writing this article, it is my way of showing my process and expectations. Seeing the final result will not lead one to understanding the process.

In this series of articles, I will focus on documenting the process, what my approach is, how I solve certain problems.

Enough preamble, let’s get started!

Requirements

Before starting, I like to have a consistent environment that is working. The environment I will be using is from a Vagrant box. The set up files are here:

https://github.com/a-leung/vagrant_files/tree/master/ruby_rspec_development

Installing Vagrant and Virtualbox information can be found at:

With the file downloaded, run: $ vagrant up, which will create a working environment. Connect to the environment using: $ vagrant ssh.

All of the commands and code will be working from within the vagrant environment.

Problem

For this series, I will be solving the problem:

Write a divide function without using the divide function.

I wrote about this problem: Interview Question: Divide without Divide

Just to keep things realistic.

Test: Run Ruby

Let’s do a quick check to make sure Ruby runs:

vagrant@ubuntu-xenial:~$ ruby --version
ruby 2.4.5p335 (2018-10-18 revision 65137) [x86_64-linux]

Great, let’s keep moving.

Test: Run Rspec

Let’s do a quick sanity check and make sure Rspec is running by using command: $ rspec

vagrant@ubuntu-xenial:~$ rspec
No examples found.

Finished in 0.00044 seconds (files took 0.05556 seconds to load)
0 examples, 0 failures

Perfect, this means Rspec is running with the correct setup.

If this is not working, get it working! All the steps afterwards depends on Rspec. It’s my Ruby testing framework of choice.

Test: Add spec file & single test

The next step is to create the folder where Rspec is looking for tests and create a single test to ensure the right file setup and there’s feedback.

RSpec expects it’s test files to be in the /spec folder and to have an ending of _spec.rb.

Create the folder with command: $ mkdir spec

Create file with command: $ touch spec/divide_spec.rb

I’m using divide_spec.rb as I expect to have another file named: divide.rb, which is the convention for Rspec: base.rb has test file: base_spec.rb.

It feels weird to create the test file first before the actual file it tests, but all about taking small steps confidently.

spec/divide_spec.rb contents:

describe 'divide' do
  it 'passes' do
	expect(true).to eq(false)
  end
end

I purposely have a failing test: expect(true).to eq(false) because this stresses the framework and I know things are connected properly.

Run $ rspec and see the result:

vagrant@ubuntu-xenial:~$ rspec
F

Failures:

  1) divide passes
	 Failure/Error: expect(true).to eq(false)

	   expected: false
		got: true

	   (compared using ==)
	 # ./spec/divide_spec.rb:6:in `block (2 levels) in <top (required)>'

Finished in 0.04435 seconds (files took 0.09495 seconds to load)
1 example, 1 failure

Failed examples:

rspec ./spec/divide_spec.rb:5 # divide passes

Rspec fails, Which is exactly what I expected since the test was created to fail.

Code: Rspec true

Let’s make the failing Rspec test pass:

spec/divide_spec.rb file contents:

describe 'divide' do
  it 'passes' do
	expect(true).to eq(true)
  end
end

and run $ rspec again:

vagrant@ubuntu-xenial:~$ rspec
.

Finished in 0.00276 seconds (files took 0.09965 seconds to load)
1 example, 0 failures

So, even though the code added to make the test pass was in the test file, it’s still code.

Setup Complete

Whew. This is quite a bit of work, just to get Rspec set up properly.

What are the benefits?

  • There’s only one folder: /spec/
  • that has one file: divide_spec.rb
  • that has one test: expect(true).to eq(true)

The main benefit is the absolute minimum is here. ALL the code written has been tested. Every line of code, file, and folder can be accounted for. Any code review done would be a breeze, even with the toughest reviewer!

Test: Does the divide method exist?

With Ruby and Rspec running and working for us. Let’s get more interesting and start testing!

First off, how do we test for a function that does not exist?? What kind of test would that look like?

Honestly, I don’t know how to write the test. Writing a test for a function that does not exist is not something I do often, but I do expect others to do it.

This is how I figured out how to test a function that does not exist in Ruby: play with the irb console!

Yes, I expect tests and when I don’t know how things work or even how to construct a test, I look for a sandbox where I can play freely to get a hint on where to go next.

I start up $ irb and type: divide and see what happens:

2.4.5 :001 > divide
NameError: undefined local variable or method `divide' for main:Object
	from (irb):1
	from /home/vagrant/.rvm/rubies/ruby-2.4.5/bin/irb:11:in `<main>'
2.4.5 :002 >

From this experiment, I learned in Ruby, where there’s no method defined with the name, a NameError exception is thrown. (Before this article, I did not know this detail, even after years of working with Ruby!)

With this info the test can be written as:

1
2
3
4
5
6
7
8
9
describe 'divide' do
  it 'passes' do
	expect(true).to eq(true)
  end

  it 'has divide function defined' do
	expect { divide }.to_not raise_error NameError
  end
end

If we take a look at the test:

expect { divide }.to_not raise_error NameError

It is testing to make sure nothing blows up when instantiating the divide method.

Running:

vagrant@ubuntu-xenial:~$ rspec
.WARNING: Using `expect { }.not_to raise_error(SpecificErrorClass)` risks false positives, since literally any other error would cause the expectation to pass, including those raised by Ruby (e.g. NoMethodError, NameError and ArgumentErr
or), meaning the code you are intending to test may not even get reached. Instead consider using `expect { }.not_to raise_error` or `expect { }.to raise_error(DifferentSpecificErrorClass)`. This message can be suppressed by setting: `RSp
ec::Expectations.configuration.on_potential_false_positives = :nothing`. Called from /home/vagrant/spec/divide_spec.rb:10:in `block (2 levels) in <top (required)>'.
F

Failures:

  1) divide has divide function defined
	 Failure/Error: expect { divide }.to_not raise_error NameError

	   expected no NameError, got #<NameError: undefined local variable or method `divide' for #<RSpec::ExampleGroups::Divide:0x00000000019bc908>> with backtrace:
	 # ./spec/divide_spec.rb:10:in `block (3 levels) in <top (required)>'
	 # ./spec/divide_spec.rb:10:in `block (2 levels) in <top (required)>'
	 # ./spec/divide_spec.rb:10:in `block (2 levels) in <top (required)>'

Finished in 0.01895 seconds (files took 0.09504 seconds to load)
2 examples, 1 failure

Failed examples:

rspec ./spec/divide_spec.rb:9 # divide has divide function defined

Code: Create divide method

To make the test pass, let’s include minimum amount of code, which would be to write:

def divide; end

In the same file as the tests. Why? I want the test to pass as the first step. I don’t even want to create a new file, or include arguments, or even to have anything in the body of the method.

I just want the smallest amount of code to have the test pass, nothing more, nothing less.

spec/divide_spec.rb file contents:

1
2
3
4
5
6
7
8
9
10
11
def divide; end

describe 'divide' do
  it 'passes' do
	expect(true).to eq(true)
  end

  it 'has divide function defined' do
	expect { divide }.to_not raise_error NameError
  end
end

Running $ rspec gives result:

vagrant@ubuntu-xenial:~$ rspec
.WARNING: Using `expect { }.not_to raise_error(SpecificErrorClass)` risks false positives, since literally any other error would cause the expectation to pass, including those raised by Ruby (e.g. NoMethodError, NameError and ArgumentErr
or), meaning the code you are intending to test may not even get reached. Instead consider using `expect { }.not_to raise_error` or `expect { }.to raise_error(DifferentSpecificErrorClass)`. This message can be suppressed by setting: `RSp
ec::Expectations.configuration.on_potential_false_positives = :nothing`. Called from /home/vagrant/spec/divide_spec.rb:11:in `block (2 levels) in <top (required)>'.
.

Finished in 0.00408 seconds (files took 0.10515 seconds to load)
2 examples, 0 failures

Tests are running all green. So this could be a good time to ask: is there something that can be refactored?

Refactor: Rspec Warnings

For me, the Rspec warning message tells me the test can be set up better:

WARNING: Using `expect { }.not_to raise_error(SpecificErrorClass)`
risks false positives, since literally any other error would cause
the expectation to pass, including those raised by Ruby
(e.g. NoMethodError, NameError and ArgumentError), meaning the
code you are intending to test may not even get reached. Instead
consider using `expect { }.not_to raise_error` or `expect { }.to
raise_error(DifferentSpecificErrorClass)`. This message can be
suppressed by setting:
`RSpec::Expectations.configuration.on_potential_false_positives =
:nothing`. Called from /home/vagrant/spec/divide_spec.rb:10:in
`block (2 levels) in <top (required)>'.

Getting this error each test run is annoying. Turning it off through the configuration would work, but the message is a bit more insightful. To me, it’s saying, don’t specify the error type, just go with raise_error, like so:

spec/divide_spec.rb file contents:

1
2
3
4
5
6
7
8
9
10
11
12
def divide
end

describe 'divide' do
  it 'passes' do
	expect(true).to eq(true)
  end

  it 'has divide function defined' do
	expect { divide }.to_not raise_error
  end
end

Running $ rspec again:

vagrant@ubuntu-xenial:~$ rspec
..

Finished in 0.00433 seconds (files took 0.09769 seconds to load)
2 examples, 0 failures

Great, no more big Warning message.

Refactor: Follow Convention

With everything passing cleanly, we can move forward onto another test.

BUT there’s a good refactor we can do here: move the def divide; end into it’s own file to follow the standard Rspec convention:

  • divide.rb - has the code
  • spec/divide_spec.rb - has the tests

Right now, there’s only spec/divide_spec.rb and it contains both code and test. Let’s clean this up a bit:

spec/divide_spec.rb file contents:

require './divide'

describe 'divide' do
  it 'passes' do
	expect(true).to eq(true)
  end

  it 'has divide function defined' do
	expect { divide }.to_not raise_error
  end
end

divide.rb file contents:

def divide
end

Conclusion

An approach to setting up Ruby and Rspec for solving a simple problem has been presented. Testing Ruby and Rspec for configuration, adding files as needed, this approach is more… methodical.

There is only wrote two lines of production code and twelve lines of tests. Isn’t that abysmal?

It all depends:

  • if this was hobby code, it’s a absymal. More code was spent on tests than actual functionality. Literally six times more lines of tests than for each piece of code.

  • if this was production code, it’s fantastic. The bare minimum code is there, each line of code can be justified with a test.

What’s the value in this approach?

  • absolute confidence in code produced is minimal and test code covers the base scenarios.
  • coverage of extreme edge cases
  • get started in understanding Ruby guts
  • literally test all the code written

Moving forward, I will go over solving the divide without divide problem more, following the same approach.