⚠️ This README.md requires updating to include JavaScript/psuedocode examples. Examples right now are written in Ruby.
"Test double" is an overarching (and debatably overloaded) term to define the action of replacing real/production code with a simplified stand-in (like a stunt double in a movie) for the purposes of creating an automated test suite, predominantly within the object-oriented programming paradigm.
The term is credited as appearing first in Gerard Meszaros's xUnit Test Patterns.
A consensus has formed around various patterns for employing test doubles, creating the terms dummy, stub, spy, mock and fake. Collectively they make up a core component of outside-in test-driven development, add some exciting new possibilities in verifying behaviour inside a test suite and eventually invite the user to participate in the red-hot debate of whether or not they should ever actually be used at all.
It's also worth noting that the term mock - or mocking library - has also become a catch-all term for test doubles in general, leading to numerous salty debates on pull request submissions. While there is probably some worthwhile nuance in the various terms, this workshop takes the opinion that it's (probably) not terminology worth sweating over at this point in time.
This workshop is designed to be interactive and collaborative, ideally working as a pair throughout the exercises. We'll be aiming for the following timings over 90 minutes:
- Check-In (5m)
- Setup Environments (5m)
- Warmup (10m)
- Regroup (5m)
- Test Double Exercises (20m)
- Regroup (10m)
- HVAC Exercise (20m)
- Feedback (10m)
You are not expected to get through everything in that time, and will hopefully want to re-visit this workshop down the line to check-in.
If this workshop does what it hopes to do, you'll finish up with the following:
- What we mean by terms test double, dummy, stub, spy, mock and fake.
- An understanding of why to use test doubles as part of your testing practice
- How test doubles enable outside-in test driven development
- Knowledge of where to go next to further your understanding of test doubles and outside-in test driven development
- You should feel at worst lightly comfortable with the classic style of TDD. You have probably attended an 'introduction to TDD' workshop, likely done a kata involving bowling or string calculating and written some tests for one of your own projects.
- You are not expected to be a devout TDD true believer. Some healthy scepticism is encouraged!
Tests are available in the javascript
and ruby
directories. They are roughly identical, with some slight edits here and there for the quirks of each language.
As pairing is encouraged in this workshop, it also will be advantageous to have a setup that removes some of the barriers to collaborative editing. At time of writing, the Live Share extension for VS Code is perhaps the easiest way to get up and running here.
Take a look at lib/00_warmup.rb
and test/00_warmup_test.rb
to get yourself familiar with the syntax of Ruby, Minitest and the layout of the exercises today.
You may not find yourself being able to comfortably do all of this in 10 minutes, this is OK! The main goal of the warmup is to start our journey into test doubles, not to pass the test suite.
Before going into test doubles properly, it will almost certainly be advantageous to refresh our memories on dependency injection.
We work for a reasonably sized enterprise company that, above all else, adores paperwork. In the below example we have two classes, Adder
and Auditer
. Adder
has an add
method which will return the sum of input parameters x
and y
. As part of our compliance regulations, however, we require an auditable record of each addition: this is handled by the Auditer
class and its record
method.
Unfortunately, Adder#record
performs some extremely complex computation and takes ~2 seconds to complete.
class Auditer
def record(result, timestamp)
sleep 2.seconds # do extremely complicated data engineering work
end
end
class Adder
def add(x, y)
result = x + y
Auditer.new.record(result, Time.now)
result
end
end
This is problematic for our unit testing for Adder
because:
- Unit testing is supposed to be fast! 2 seconds is too long!
- Our
Adder
class is now tightly coupled to ourAuditer
class. - If our
Auditer
class was our concrete/production code then we might be adding test events to our actual, for-realsies audit log. Try explaining that to your compliance team.
Instead, we can modify our design for Adder
slightly to use dependency injection - instead of instantiating the class directly in our code, we now assign an object to an instance variable @auditer
. Ruby is not at all worried about what that object is, only that it can respond to a record
message.
# This is our production code - we don't need this for our unit test cases for Adder
class Auditer
def record(result, timestamp)
sleep 2.seconds
end
end
# DoubleAuditer also responds to a record message
class DoubleAuditer
def record(result, timestamp)
# blank
end
end
class Adder
def initialize(auditer)
@auditer = auditer
end
def add(x, y)
result = x + y
@auditer.record(result, Time.now)
result
end
end
This is known as duck typing and a is common trait to dynamic languages (e.g. Ruby, Python, JavaScript). Duck typing can take a while to get your head around, but is a very powerful technique in object-oriented design.
In doing this, we have just experienced the use of a test double. In our tests, DoubleAuditer
stands in for Auditer
. This can be an effective thing to do because:
- It allows us to focus our unit tests for
Adder
specifically on the behaviour of theAdder
class. Two separate teams may even be working onAdder
andAuditer
, and this allows them to work in isolation. - We knock out the actual dependencies and replace them with doubles, which allows our tests to focus solely on the actual subject under test (
Adder
). - It significantly speeds up our test suite, which your CI runner will thank you for.
🙋 There are many sophisticated mocking/test double libraries that simplify the process of creating and using doubles in your test suites. These libraries can often have their own intricacies and bespoke APIs, however, which can detract from process of actually getting familiar with test doubles. For this workshop please lovingly (for the most part) craft your test doubles by hand - this can be a really effective way of demystifying them!
This repo contains a series of exercises designed to give you exposure to each test double, locaed in the lib
and test
folders. Counting up, try to move through each of the test files and make the failing tests pass.
🎤 If you can, discuss how you might use each test double in your current project with your pair. For instance, how might a stub help you write tests for your Tic Tac Toe? How might a spy be useful for developing an Echo Server?
.
├── lib
│ ├── 00_warmup.rb
│ ├── 01_dummy.rb
│ ├── 02_stub.rb
│ ├── 03_spy.rb
│ ├── 04_mock.rb
│ └── 05_fake.rb
└── test
├── 00_warmup_test.rb
├── 01_dummy_test.rb <-- start here!
├── 02_stub_test.rb
├── 03_spy_test.rb
├── 04_mock_test.rb
└── 05_fake_test.rb <-- stop here!
🤔 Try to give yourself 3-4 minutes with each test suite and move on. Do not worry if you cannot get the tests to pass!
A slightly bigger task sits within lib/06_hvac.rb
and test/06_hvac_test.rb
. Both files are commented with instructions. This task can either be done solo, in pairs, or as a mob - this can be a group decision 😊
Like with the other tasks, you can limit the test suite to test/06_hvac_test.rb
with the following command: $ RACK_ENV=test:hvac docker compose up --no-log-prefix
🧠 The goal of the activity during the workshop is to start thinking about incorporating test doubles into an exercise from scratch, so do not be discouraged if you do not commit many lines of code.
Whew! You're done! Please take a moment to provide feedback on the workshop: https://forms.gle/LLgkMWeaUKPk6cHP8
PRs and content suggestions are very much welcome, also.
- Growing Object Oriented Software, Guided by Tests (lovingly referred to as GOOS) is the seminal text detailing the use of test doubles within outside-in test driven development.