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.
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:
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
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:
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:
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:
and run $ rspec
again:
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:
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:
It is testing to make sure nothing blows up when instantiating the divide method.
Running:
Code: Create divide
method
To make the test pass, let’s include minimum amount of code, which would be to write:
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:
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:
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:
divide.rb
file contents:
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.