# Test Driven Development

Welcome to Container Industries Ltd. As the new engineer here you've been assigned the job of developing the company's container functionality.

1. Items can be added to a container
2. Items are not added to a container if doing so would exceed the container's weight limit.

Seems nice and simple. Let's use this as an excuse to explore the world of Test Driven Development (TDD) together. There's a really nice rhythm I want to get across in the this post:

1. Write the tests first
2. Run the tests often
3. Only start adding new functionality when the tests are passing
4. When the tests are failing, your sole intention should be to get them to a passing state. Sometimes this means fudging the code, but that's OK; it's all about taking tiny steps and iterating frequently to meet the requirements

## 1. Items can be added to a container

Here's the initial test.

# container_spec.rb
describe Container do
end
end


After running it we get:

$rspec container_spec.rb /Users/andy/dev/Ruby/tdd/container_spec.rb:2:in <top (required)>': uninitialized constant Container (NameError) from /Users/andy/.gem/ruby/2.4.0/gems/rspec-core-3.5.4/lib/rspec/core/configuration.rb:1435:in load' from /Users/andy/.gem/ruby/2.4.0/gems/rspec-core-3.5.4/lib/rspec/core/configuration.rb:1435:in block in load_spec_files' from /Users/andy/.gem/ruby/2.4.0/gems/rspec-core-3.5.4/lib/rspec/core/configuration.rb:1433:in each' from /Users/andy/.gem/ruby/2.4.0/gems/rspec-core-3.5.4/lib/rspec/core/configuration.rb:1433:in load_spec_files' from /Users/andy/.gem/ruby/2.4.0/gems/rspec-core-3.5.4/lib/rspec/core/runner.rb:100:in setup' from /Users/andy/.gem/ruby/2.4.0/gems/rspec-core-3.5.4/lib/rspec/core/runner.rb:86:in run' from /Users/andy/.gem/ruby/2.4.0/gems/rspec-core-3.5.4/lib/rspec/core/runner.rb:71:in run' from /Users/andy/.gem/ruby/2.4.0/gems/rspec-core-3.5.4/lib/rspec/core/runner.rb:45:in invoke' from /Users/andy/.gem/ruby/2.4.0/gems/rspec-core-3.5.4/exe/rspec:4:in <top (required)>' from /Users/andy/.gem/ruby/2.4.0/bin/rspec:22:in load' from /Users/andy/.gem/ruby/2.4.0/bin/rspec:22:in <main>'  Looks like we need to create the Container definition. We are going to be iterating frequently, so let's do the simplest thing we can right now to get the tests to pass. We'll add the Container definition right at the top of the spec file. class Container end describe Container do it "can add items" do end end  $ rspec container_spec.rb
.

Finished in 0.00091 seconds (files took 0.66202 seconds to load)
1 example, 0 failures


Our first passing test! Yess

It isn't actually asserting anything though, so let's start adding real tests.

How about we work backwards, writing the test under the assumption that all the variables we need already exist?

class Container
end
describe Container do
expect(container.items).to include pasta
expect(container.items).to include cake
end
end

$rspec container_spec.rb F Failures: 1) Container can add items Failure/Error: container.add pasta NameError: undefined local variable or method container' for #<RSpec::ExampleGroups::Container:0x007fcafe150f60> Did you mean? contain_exactly # ./container_spec.rb:5:in block (2 levels) in <top (required)>' Finished in 0.00065 seconds (files took 1.28 seconds to load) 1 example, 1 failure Failed examples: rspec ./container_spec.rb:4 # Container can add items  Alright, looks like we need to define the container attribute in the test. class Container end describe Container do let(:container) { Container.new } it "can add items" do container.add pasta container.add cake expect(container.items).to include pasta expect(container.items).to include cake end end  $ rspec container_spec.rb
F

Failures:

NameError:
undefined local variable or method pasta' for #<RSpec::ExampleGroups::Container:0x007fd3a1974d58>
# ./container_spec.rb:7:in block (2 levels) in <top (required)>'

Finished in 0.00071 seconds (files took 0.27835 seconds to load)
1 example, 1 failure

Failed examples:

rspec ./container_spec.rb:6 # Container can add items


…and we need to define pasta and cake too.

# ...
describe Container do
let(:container) { Container.new }
let(:pasta) { "pasta" }
let(:cake) { "cake" }

expect(container.items).to include pasta
expect(container.items).to include cake
end
end

$rspec container_spec.rb F Failures: 1) Container can add items Failure/Error: container.add pasta NoMethodError: undefined method add' for #<Container:0x007f969fa0ba58> # ./container_spec.rb:9:in block (2 levels) in <top (required)>' Finished in 0.00078 seconds (files took 0.27569 seconds to load) 1 example, 1 failure Failed examples: rspec ./container_spec.rb:8 # Container can add items  The test is failing because we haven't implemented the add method. So let's add that next. class Container def add(item) end end # ...  $ rspec container_spec.rb
F

Failures:

Failure/Error: expect(container.items).to include pasta

NoMethodError:
undefined method items' for #<Container:0x007fce65a16b08>
# ./container_spec.rb:13:in block (2 levels) in <top (required)>'

Finished in 0.00068 seconds (files took 0.25235 seconds to load)
1 example, 1 failure

Failed examples:

rspec ./container_spec.rb:10 # Container can add items


Now, it's time to tell Container that it has some items

class Container
end
end
# ...

$rspec container_spec.rb F Failures: 1) Container can add items Failure/Error: expect(container.items).to include pasta expected nil to include "pasta", but it does not respond to include? # ./container_spec.rb:14:in block (2 levels) in <top (required)>' Finished in 0.25862 seconds (files took 0.32494 seconds to load) 1 example, 1 failure Failed examples: rspec ./container_spec.rb:11 # Container can add items  This test is failing because our add method isn't implemented yet. So let's tackle that next. class Container attr_reader :items def initialize @items = [] end def add(item) items << item end end # ...  $ rspec container_spec.rb
.

Finished in 0.00198 seconds (files took 0.22488 seconds to load)
1 example, 0 failures


Here's what container_spec.rb looks like so far:

class Container

def initialize
@items = []
end

items << item
end
end
describe Container do
let(:container) { Container.new }
let(:pasta) { "pasta" }
let(:cake) { "cake" }

expect(container.items).to include pasta
expect(container.items).to include cake
end
end


## 2. Items are not added to a container if doing so would exceed the container's weight limit

OK, here's my test:

  # ...
it "does not add items that would exceed the weight limit" do
expect(container.items).to include pasta
expect(container.items).to_not include house
end
# ...

$rspec container_spec.rb .F Failures: 1) Container does not add items that would exceed the weight limit Failure/Error: container.add house NameError: undefined local variable or method house' for #<RSpec::ExampleGroups::Container:0x007fe2a68d2cd8> # ./container_spec.rb:26:in block (2 levels) in <top (required)>' Finished in 0.00256 seconds (files took 0.24082 seconds to load) 2 examples, 1 failure Failed examples: rspec ./container_spec.rb:24 # Container does not add items that would exceed the weight limit  This fails because we haven't told the test what a house is. What does it mean to be a house? Let's say it means a string with the value "house" for now. describe Container do let(:container) { Container.new } let(:pasta) { "pasta" } let(:cake) { "cake" } let(:house) { "house" } # ...  $ rspec container_spec.rb
.F

Failures:

1) Container does not add items that would exceed the weight limit
Failure/Error: expect(container.items).to_not include house
expected ["pasta", "house"] not to include "house"
# ./container_spec.rb:29:in block (2 levels) in <top (required)>'

Finished in 0.1118 seconds (files took 0.44737 seconds to load)
2 examples, 1 failure

Failed examples:

rspec ./container_spec.rb:25 # Container does not add items that would exceed the weight limit


Cool, a proper failure! The house was added to the container, when instead we should have cast it asunder.

What's the quickest way we can get the test to pass? Before we figure out the logic for deciding if we should add an item based on its weight, let's get the tests green by only adding the item if it isn't equal to "house".

class Container

def initialize
@items = []
end

items << item unless item == "house"
end
end
# ...

$rspec container_spec.rb .. Finished in 0.00256 seconds (files took 0.21494 seconds to load) 2 examples, 0 failures  Whoa, the tests pass! That's a bit weird, right? We fudged the code to make the tests pass. The key thing here is that we have a really tight red, green, refactor cycle; we write the tests, do the bear minimum to get them passing, then refactor to clean things up. We're doing the smallest thing we can to get to a "safe" (tests passing) state, before adding more code. That way we don't stray too far away from the safety of passing tests and don't get sidetracked implementing everything at once. Now that the tests are passing we can refactor our add method to take into account an item's weight. class Container attr_reader :items def initialize @items = [] end def add(item) items << item unless item_too_heavy? item end private def item_too_heavy? item item == "house" end end # ...  $ rspec container_spec.rb
..

Finished in 0.00367 seconds (files took 0.24654 seconds to load)
2 examples, 0 failures


We've extracted the logic that checks if an item is too heavy into its own method. The tests pass, but we don't really have any way to define how heavy an item is. Let's amend the tests to specify the weights of items.

# ...
describe Container do
let(:container) { Container.new }
let(:pasta) { Item.new(5) }
let(:cake) { Item.new(5) }
let(:house) { Item.new(20) }

# ...
end

$rspec container_spec.rb FF Failures: 1) Container can add items Failure/Error: let(:pasta) { Item.new(5) } NameError: uninitialized constant Item # ./container_spec.rb:20:in block (2 levels) in <top (required)>' # ./container_spec.rb:25:in block (2 levels) in <top (required)>' 2) Container does not add items that would exceed the weight limit Failure/Error: let(:pasta) { Item.new(5) } NameError: uninitialized constant Item # ./container_spec.rb:20:in block (2 levels) in <top (required)>' # ./container_spec.rb:32:in block (2 levels) in <top (required)>' Finished in 0.00088 seconds (files took 0.23031 seconds to load) 2 examples, 2 failures Failed examples: rspec ./container_spec.rb:24 # Container can add items rspec ./container_spec.rb:31 # Container does not add items that would exceed the weight limit  The tests fail because we haven't defined Item yet. Let's do that now (still inside container_spec.rb for now): # ... class Item attr_reader :weight def initialize(weight) @weight = weight end end describe Container do let(:container) { Container.new } let(:pasta) { Item.new(5) } let(:cake) { Item.new(5) } let(:house) { Item.new(20) } # ... end  $ rspec container_spec.rb
.F

Failures:

1) Container does not add items that would exceed the weight limit
Failure/Error: expect(container.items).to_not include house

expected [#<Item:0x007fa7481ab5e8 @weight=5>, #<Item:0x007fa7481ab138 @weight=20>] not to include #<Item:0x007fa7481ab138 @weight=20>
Diff:
@@ -1,2 +1,2 @@
-[#<Item:0x007fa7481ab138 @weight=20>]
+[#<Item:0x007fa7481ab5e8 @weight=5>, #<Item:0x007fa7481ab138 @weight=20>]
# ./container_spec.rb:41:in block (2 levels) in <top (required)>'

Finished in 0.1253 seconds (files took 0.26481 seconds to load)
2 examples, 1 failure

Failed examples:

rspec ./container_spec.rb:37 # Container does not add items that would exceed the weight limit


Our implementation of the item_too_heavy? method only checks if an item is equal to "house". Since we're now using Items, and not strings, this test fails! Let's perform a similar check, but this time look for an item's weight being equal to 20 (the same as the house).

class Container

def initialize
@items = []
end

items << item unless item_too_heavy? item
end

private

def item_too_heavy? item
item.weight == 20
end
end
# ...

$rspec container_spec.rb .. Finished in 0.00213 seconds (files took 0.22381 seconds to load) 2 examples, 0 failures  Now we have some passing tests we can refactor our item_too_heavy? method. The container needs to know about its weight limit to check if an item is too heavy. class Container attr_reader :items def initialize @items = [] end def add(item) items << item unless item_too_heavy? item end private def item_too_heavy? item weight_limit = 19 item.weight > weight_limit end end # ...  $ rspec test.rb
..

Finished in 0.00234 seconds (files took 0.22688 seconds to load)
2 examples, 0 failures


Cool, we have our item_too_heavy? method taking in to account the container's weight limit. Since the tests are passing, we can look into cleaning this method up. One thing that jumps out here is that we aren't storing what the container's current weight is. That's important, so let's write a test for it.

# ...
describe Container do
# ...

expect(container.weight).to eq pasta.weight
expect(container.weight).to eq pasta.weight + cake.weight
end
end


$rspec container_spec.rb ..F Failures: 1) Container updates the weight when an item is added Failure/Error: expect(container.weight).to eq pasta.weight NoMethodError: undefined method weight' for #<Container:0x007fae3f1764d0> # ./container_spec.rb:47:in block (2 levels) in <top (required)>' Finished in 0.0026 seconds (files took 0.23364 seconds to load) 3 examples, 1 failure Failed examples: rspec ./container_spec.rb:45 # Container updates the weight when an item is added  The Container class doesn't know about its current weight, so the test is failing. Let's add a weight attribute to the Container. class Container attr_reader :items, :weight def initialize @items = [] @weight = 0 end def add(item) items << item unless item_too_heavy? item end private def item_too_heavy? item weight_limit = 19 weight + item.weight > weight_limit end end # ...  $ rspec container_spec.rb
..F

Failures:

Failure/Error: expect(container.weight).to eq pasta.weight

expected: 5
got: 0

(compared using ==)
# ./container_spec.rb:48:in block (2 levels) in <top (required)>'

Finished in 0.16023 seconds (files took 0.26932 seconds to load)
3 examples, 1 failure

Failed examples:

rspec ./container_spec.rb:46 # Container updates the weight when an item is added


Although we're storing the weight on the container, we're never updating the value when we add items. This is why we expected a weight of 5 when it was actually 0.

class Container
# ...

items << item unless item_too_heavy? item
@weight += item.weight
end

# ...
end
# ...

$rspec container_spec.rb ... Finished in 0.00337 seconds (files took 0.26162 seconds to load) 3 examples, 0 failures  At this point our add method is looking OK, and I'm happy to move on. We can now look into refactoring other parts of the class. I think item_too_heavy? would be a good next step. It breaks the Single Responsibilty Principle in that it's responsible for setting the weight limit of the container and checking if an item is too heavy. By extracting this assignment into the constructor we are ensuring that item_too_heavy? only has one reason to change. class Container attr_reader :items, :weight def initialize @items = [] @weight = 0 @weight_limit = 19 end def add item items << item unless item_too_heavy? item @weight += item.weight end private def item_too_heavy? item weight + item.weight > @weight_limit end end # ...  $ rspec container_spec.rb
...

Finished in 0.00394 seconds (files took 0.21574 seconds to load)
3 examples, 0 failures


That's a bit nicer, but I'm still not really happy. We're hard coding the weight limit of our containers, which introduces a reason for the class to change in the future. Say Barry wants to update the weight limit to 15, he might be tempted to dive into the class and change the value. This introduces a risk that he'll accidentally break something while he's in there (you know what he's like). A more robust approach would be to inject the value of the container weight limit, that way Barry can have containers of varying weight limits without having to modify the Container class. Score!

Let's update the tests to inject the weight limit into the container when we instantiate it.

describe Container do
let(:container) { Container.new(weight_limit: 19) }
let(:pasta) { Item.new(5) }
let(:cake) { Item.new(5) }
let(:house) { Item.new(20) }

# ...
end

$rspec container_spec.rb FFF Failures: 1) Container can add items Failure/Error: def initialize @items = [] @weight = 0 @weight_limit = 19 end ArgumentError: wrong number of arguments (given 1, expected 0) # ./container_spec.rb:4:in initialize' # ./container_spec.rb:28:in new' # ./container_spec.rb:28:in block (2 levels) in <top (required)>' # ./container_spec.rb:34:in block (2 levels) in <top (required)>' 2) Container does not add items that would exceed the weight limit Failure/Error: def initialize @items = [] @weight = 0 @weight_limit = 19 end ArgumentError: wrong number of arguments (given 1, expected 0) # ./container_spec.rb:4:in initialize' # ./container_spec.rb:28:in new' # ./container_spec.rb:28:in block (2 levels) in <top (required)>' # ./container_spec.rb:41:in block (2 levels) in <top (required)>' 3) Container updates the weight when an item is added Failure/Error: def initialize @items = [] @weight = 0 @weight_limit = 19 end ArgumentError: wrong number of arguments (given 1, expected 0) # ./container_spec.rb:4:in initialize' # ./container_spec.rb:28:in new' # ./container_spec.rb:28:in block (2 levels) in <top (required)>' # ./container_spec.rb:48:in block (2 levels) in <top (required)>' Finished in 0.00117 seconds (files took 0.21716 seconds to load) 3 examples, 3 failures Failed examples: rspec ./container_spec.rb:33 # Container can add items rspec ./container_spec.rb:40 # Container does not add items that would exceed the weight limit rspec ./container_spec.rb:47 # Container updates the weight when an item is added  Cool, we broke everything. Let's update the initialize method in Container to accept the weight limit. class Container attr_reader :items, :weight def initialize(weight_limit:) @items = [] @weight = 0 @weight_limit = weight_limit end # ... end # ...  $ rspec container_spec.rb
...

Finished in 0.01101 seconds (files took 0.24302 seconds to load)
3 examples, 0 failures


Now that the tests are passing, we can perform more clean up. For example, we used a named argument for weight_limit in Container. Let's do the same thing for Item and its weight.

First, the tests…

# ...
describe Container do
let(:container) { Container.new(weight_limit: 19) }
let(:pasta) { Item.new(weight: 5) }
let(:cake) { Item.new(weight: 5) }
let(:house) { Item.new(weight: 20) }

# ...
end

$rspec container_spec.rb FFF Failures: 1) Container can add items Failure/Error: weight + item.weight > @weight_limit TypeError: Hash can't be coerced into Integer # ./container_spec.rb:18:in +' # ./container_spec.rb:18:in item_too_heavy?' # ./container_spec.rb:11:in add' # ./container_spec.rb:34:in block (2 levels) in <top (required)>' 2) Container does not add items that would exceed the weight limit Failure/Error: weight + item.weight > @weight_limit TypeError: Hash can't be coerced into Integer # ./container_spec.rb:18:in +' # ./container_spec.rb:18:in item_too_heavy?' # ./container_spec.rb:11:in add' # ./container_spec.rb:41:in block (2 levels) in <top (required)>' 3) Container updates the weight when an item is added Failure/Error: weight + item.weight > @weight_limit TypeError: Hash can't be coerced into Integer # ./container_spec.rb:18:in +' # ./container_spec.rb:18:in item_too_heavy?' # ./container_spec.rb:11:in add' # ./container_spec.rb:48:in block (2 levels) in <top (required)>' Finished in 0.00106 seconds (files took 0.29553 seconds to load) 3 examples, 3 failures Failed examples: rspec ./container_spec.rb:33 # Container can add items rspec ./container_spec.rb:40 # Container does not add items that would exceed the weight limit rspec ./container_spec.rb:47 # Container updates the weight when an item is added  Now we can fix up the Item class by updating the initialize method, just like we did for the Container class. # ... class Item attr_reader :weight def initialize(weight:) @weight = weight end end # ...  $ rspec container_spec.rb
...

Finished in 0.01256 seconds (files took 0.25995 seconds to load)
3 examples, 0 failures


### Time to refactor!

This is what our Container's item_too_heavy? method currently looks like:

def item_too_heavy? item
weight + item.weight > @weight_limit
end


Notice that we aren't using an accessor method to get the weight limit; we are calling @weight_limit to access it directly. Although this is fine here, it's generally best to use an accessor because it changes the statement from calling data (@weight_limit) to calling behaviour (weight_limit, the attr_reader method). This is useful because it means there is one place in our code that defines what it means to be a weight_limit. This perhaps sounds pedantic, but what if (for safety reasons) there was an update to weight limits, which meant that they were reduced by 5 when the inspectors were around. We would need to find everywhere we were calling the data @weight_limit and subtract 5 if the inspectors were near. We'd be duplicating behaviour and it'd be likely that something would break. By using an accessor method we could simply override the weight_limit method to include the suitable logic, and our other methods that relied on the accessor would not need to change.

Let's make use of Ruby's accessor methods to get the weight limit.

class Container

def initialize(weight_limit:)
@items = []
@weight = 0
@weight_limit = weight_limit
end

items << item unless item_too_heavy? item
@weight += item.weight
end

private

def item_too_heavy? item
weight + item.weight > weight_limit
end
end
# ...

$rspec container_spec.rb ... Finished in 0.00201 seconds (files took 0.13739 seconds to load) 3 examples, 0 failures  While we're on removing unnecessary direct calls to instance variables, how about we clean up the add method by replacing @weight += item.weight  with a call to an accessor method too? class Container attr_reader :items, :weight, :weight_limit def initialize(weight_limit:) @items = [] @weight = 0 @weight_limit = weight_limit end def add item items << item unless item_too_heavy? item self.weight += item.weight end private def item_too_heavy? item weight + item.weight > weight_limit end def weight=(value) @weight = value end end # ...  $ rspec container_spec.rb
...

Finished in 0.00586 seconds (files took 0.28069 seconds to load)
3 examples, 0 failures


At this point, our container_spec.rb file is getting pretty hefty, so let's extract out the classes into their own files.

# container.rb
class Container

def initialize(weight_limit:)
@items = []
@weight = 0
@weight_limit = weight_limit
end

items << item unless item_too_heavy? item
self.weight += item.weight
end

private

def item_too_heavy? item
weight + item.weight > weight_limit
end

def weight=(value)
@weight = value
end
end

# item.rb
class Item
def initialize(weight:)
@weight = weight
end
end

# container_spec.rb
require "./container"
require "./item"

describe Container do
let(:container) { Container.new(weight_limit: 19) }
let(:pasta) { Item.new(weight: 5) }
let(:cake) { Item.new(weight: 5) }
let(:house) { Item.new(weight: 20) }

expect(container.items).to include pasta
expect(container.items).to include cake
end

it "does not add items that would exceed the weight limit" do
expect(container.items).to include pasta
expect(container.items).to_not include house
end

expect(container.weight).to eq pasta.weight
expect(container.weight).to eq (pasta.weight + cake.weight)
end
end

\$ rspec container_spec.rb
...

Finished in 0.00203 seconds (files took 0.11261 seconds to load)
3 examples, 0 failures


Amazing, the tests still pass! Container Industries Ltd are proud of your hard work and you've been rewarded with an almost brand new tupperware container.

I really enjoy the TDD approach. In the past I've found myself trying to implement all the features at once and getting in too deep quite early on. In the end the code coverage was usually not as high and the code quality was lower than when I used TDD.

Do you use TDD regularly? What do you think of it? Do you use an alternative approach?

Previous post: Strategising your way to clean code

Next post: Typed Arrays in ECMAScript