Skip to content

Instantly share code, notes, and snippets.

@iffy
Created June 16, 2011 15:36
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save iffy/1029511 to your computer and use it in GitHub Desktop.
Save iffy/1029511 to your computer and use it in GitHub Desktop.
TDD example
body {
margin: 0;
padding: 1em;
font-family: sans-serif;
}
h1, h2, h3, h4, h5 {
font-family: Arial;
margin-left: 12px;
margin-right: 12px;
margin-top: 48px;
}
h1.title {
font-size: 2em;
margin-top: 0px;
}
h2 {
font-size: 1.5em;
}
h3 {
font-size: 1em;
}
p {
smax-width: 700px;
margin: 0px 12px 12px;
}
img {
margin: 1em;
}
.literal {
font-family: monospace;
}
tt.literal {
font-weight: bold;
font-size: 1.05em;
padding-left: 2px;
padding-right: 2px;
/*font-size: 0.95em;*/
}
.literal-block {
font-family: monospace;
margin: 12px 12px;
background: #cef;
border: 1px solid #9ac;
border-width: 1px 0px;
padding: 5px;
color: #333;
line-height: 120%;
}
.highlight pre {
font-family: monospace;
margin: 12px 12px;
background: #efc;
border: 1px solid #ac9;
border-width: 1px 0px;
padding: 5px;
color: #333;
line-height: 120%;
}
.hll { background-color: #ffffcc }
.c { color: #408090; font-style: italic } /* Comment */
.err { border: 1px solid #FF0000 } /* Error */
.k { color: #007020; font-weight: bold } /* Keyword */
.o { color: #666666 } /* Operator */
.cm { color: #408090; font-style: italic } /* Comment.Multiline */
.cp { color: #007020 } /* Comment.Preproc */
.c1 { color: #408090; font-style: italic } /* Comment.Single */
.cs { color: #408090; background-color: #fff0f0 } /* Comment.Special */
.gd { color: #A00000 } /* Generic.Deleted */
.ge { font-style: italic } /* Generic.Emph */
.gr { color: #FF0000 } /* Generic.Error */
.gh { color: #000080; font-weight: bold } /* Generic.Heading */
.gi { color: #00A000 } /* Generic.Inserted */
.go { color: #303030 } /* Generic.Output */
.gp { color: #c65d09; font-weight: bold } /* Generic.Prompt */
.gs { font-weight: bold } /* Generic.Strong */
.gu { color: #800080; font-weight: bold } /* Generic.Subheading */
.gt { color: #0040D0 } /* Generic.Traceback */
.kc { color: #007020; font-weight: bold } /* Keyword.Constant */
.kd { color: #007020; font-weight: bold } /* Keyword.Declaration */
.kn { color: #007020; font-weight: bold } /* Keyword.Namespace */
.kp { color: #007020 } /* Keyword.Pseudo */
.kr { color: #007020; font-weight: bold } /* Keyword.Reserved */
.kt { color: #902000 } /* Keyword.Type */
.m { color: #208050 } /* Literal.Number */
.s { color: #4070a0 } /* Literal.String */
.na { color: #4070a0 } /* Name.Attribute */
.nb { color: #007020 } /* Name.Builtin */
.nc { color: #0e84b5; font-weight: bold } /* Name.Class */
.no { color: #60add5 } /* Name.Constant */
.nd { color: #555555; font-weight: bold } /* Name.Decorator */
.ni { color: #d55537; font-weight: bold } /* Name.Entity */
.ne { color: #007020 } /* Name.Exception */
.nf { color: #06287e } /* Name.Function */
.nl { color: #002070; font-weight: bold } /* Name.Label */
.nn { color: #0e84b5; font-weight: bold } /* Name.Namespace */
.nt { color: #062873; font-weight: bold } /* Name.Tag */
.nv { color: #bb60d5 } /* Name.Variable */
.ow { color: #007020; font-weight: bold } /* Operator.Word */
.w { color: #bbbbbb } /* Text.Whitespace */
.mf { color: #208050 } /* Literal.Number.Float */
.mh { color: #208050 } /* Literal.Number.Hex */
.mi { color: #208050 } /* Literal.Number.Integer */
.mo { color: #208050 } /* Literal.Number.Oct */
.sb { color: #4070a0 } /* Literal.String.Backtick */
.sc { color: #4070a0 } /* Literal.String.Char */
.sd { color: #4070a0; font-style: italic } /* Literal.String.Doc */
.s2 { color: #4070a0 } /* Literal.String.Double */
.se { color: #4070a0; font-weight: bold } /* Literal.String.Escape */
.sh { color: #4070a0 } /* Literal.String.Heredoc */
.si { color: #70a0d0; font-style: italic } /* Literal.String.Interpol */
.sx { color: #c65d09 } /* Literal.String.Other */
.sr { color: #235388 } /* Literal.String.Regex */
.s1 { color: #4070a0 } /* Literal.String.Single */
.ss { color: #517918 } /* Literal.String.Symbol */
.bp { color: #007020 } /* Name.Builtin.Pseudo */
.vc { color: #bb60d5 } /* Name.Variable.Class */
.vg { color: #bb60d5 } /* Name.Variable.Global */
.vi { color: #bb60d5 } /* Name.Variable.Instance */
.il { color: #208050 } /* Literal.Number.Integer.Long */
<?xml version="1.0" encoding="utf-8" ?>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
<meta name="generator" content="Docutils 0.7: http://docutils.sourceforge.net/" />
<title>An Example of Test Driven Development</title>
<link rel="stylesheet" href="style.css" type="text/css" />
</head>
<body>
<div class="document" id="an-example-of-test-driven-development">
<h1 class="title">An Example of Test Driven Development</h1>
<p><a class="reference external" href="index.html">Back to index</a></p>
<div class="contents topic" id="contents">
<p class="topic-title first">Contents</p>
<ul class="simple">
<li><a class="reference internal" href="#steps" id="id1">Steps</a></li>
<li><a class="reference internal" href="#doughnut" id="id2">Doughnut</a><ul>
<li><a class="reference internal" href="#red" id="id3">Red</a></li>
<li><a class="reference internal" href="#green" id="id4">Green</a></li>
<li><a class="reference internal" href="#adding-features" id="id5">Adding features</a></li>
<li><a class="reference internal" href="#general-case-then-exceptions" id="id6">General case then exceptions</a></li>
<li><a class="reference internal" href="#refactor" id="id7">Refactor</a></li>
</ul>
</li>
<li><a class="reference internal" href="#make-the-least-change-possible" id="id8">Make the least change possible</a></li>
<li><a class="reference internal" href="#test-writing-test-driving" id="id9">Test writing != test driving</a></li>
<li><a class="reference internal" href="#coverage" id="id10">Coverage</a></li>
<li><a class="reference internal" href="#what-other-people-say" id="id11">What other people say</a></li>
</ul>
</div>
<p>Test Driven Development (TDD) is a method of code development. You lead with
tests, and follow with code.</p>
<div class="section" id="steps">
<h1>Steps</h1>
<ol class="arabic simple">
<li>Red - write a failing test</li>
<li>Green - make the test pass</li>
<li>Refactor - make it DRY instead of WET; optimize; document</li>
</ol>
</div>
<div class="section" id="doughnut">
<h1>Doughnut</h1>
<p>In the following example, we'll test-drive the production of a Doughnut. There
are different kinds of doughnuts, but most are glazed.</p>
<div class="section" id="red">
<h2>Red</h2>
<p>The first step is to write a failing test for a <tt class="docutils literal">Doughtnut</tt> class that
is 'glazed'.</p>
<div class="highlight"><pre><span class="c"># test_doughnut.py</span>
<span class="kn">from</span> <span class="nn">unittest</span> <span class="kn">import</span> <span class="n">TestCase</span>
<span class="kn">from</span> <span class="nn">flour</span> <span class="kn">import</span> <span class="n">Doughnut</span>
<span class="k">class</span> <span class="nc">DoughnutTest</span><span class="p">(</span><span class="n">TestCase</span><span class="p">):</span>
<span class="k">def</span> <span class="nf">test_glazed</span><span class="p">(</span><span class="bp">self</span><span class="p">):</span>
<span class="sd">&quot;&quot;&quot;</span>
<span class="sd"> Doughtnuts are glazed by default</span>
<span class="sd"> &quot;&quot;&quot;</span>
<span class="n">d</span> <span class="o">=</span> <span class="n">Doughnut</span><span class="p">()</span>
<span class="bp">self</span><span class="o">.</span><span class="n">assertEqual</span><span class="p">(</span><span class="n">d</span><span class="o">.</span><span class="n">kind</span><span class="p">,</span> <span class="s">&#39;glazed&#39;</span><span class="p">)</span>
</pre></div>
<p>Run the test to see it fail. I like using Twisted's <tt class="docutils literal">trial</tt> to run the tests.</p>
<pre class="literal-block">
$ trial test_doughnut
test_doughnut ... [ERROR]
===============================================================================
[ERROR]
Traceback (most recent call last):
...
File &quot;/Volumes/FatMan/home/matt/tmp/dnut/test_doughnut.py&quot;, line 4, in &lt;module&gt;
from flour import Doughnut
exceptions.ImportError: No module named flour
test_doughnut
-------------------------------------------------------------------------------
Ran 1 tests in 0.026s
FAILED (errors=1)
</pre>
<p>Look at the last line:</p>
<pre class="literal-block">
exceptions.ImportError: No module named flour
</pre>
<p>The test fails because there is no module named flour. You might be tempted to
make a class named <tt class="docutils literal">Doughnut</tt> with an attribute <tt class="docutils literal">kind</tt> set to 'glazed' in a
file named <tt class="docutils literal">flour.py</tt> -- <strong>DON'T!</strong> Resolve only the error at hand by
creating a file named <tt class="docutils literal">flour.py</tt>.</p>
<div class="highlight"><pre><span class="c"># flour.py</span>
</pre></div>
<p>Run the tests again to see that we solved the &quot;No module named flour&quot; error:</p>
<pre class="literal-block">
===============================================================================
[ERROR]
Traceback (most recent call last):
...
File &quot;/Volumes/FatMan/home/matt/tmp/dnut/test_doughnut.py&quot;, line 4, in &lt;module&gt;
from flour import Doughnut
exceptions.ImportError: cannot import name Doughnut
test_doughnut
-------------------------------------------------------------------------------
</pre>
<p>It complains that we there's nothing named <tt class="docutils literal">Doughnut</tt>. Let's make a
<tt class="docutils literal">Doughnut</tt> class:</p>
<div class="highlight"><pre><span class="c"># flour.py</span>
<span class="k">class</span> <span class="nc">Doughnut</span><span class="p">:</span>
<span class="sd">&quot;&quot;&quot;</span>
<span class="sd"> I am a doughnut</span>
<span class="sd"> &quot;&quot;&quot;</span>
</pre></div>
<p>On to the next failure:</p>
<pre class="literal-block">
[ERROR]
Traceback (most recent call last):
...
File &quot;/Volumes/FatMan/home/matt/tmp/dnut/test_doughnut.py&quot;, line 13, in test_glazed
self.assertEqual(d.kind, 'glazed')
exceptions.AttributeError: Doughnut instance has no attribute 'kind'
test_doughnut.DoughnutTest.test_glazed
</pre>
<p>To make the <tt class="docutils literal">AttributeError</tt> go away add a <tt class="docutils literal">kind</tt> attribute to <tt class="docutils literal">Doughnut</tt>:</p>
<div class="highlight"><pre><span class="c"># flour.py</span>
<span class="k">class</span> <span class="nc">Doughnut</span><span class="p">:</span>
<span class="sd">&quot;&quot;&quot;</span>
<span class="sd"> I am a doughnut</span>
<span class="sd"> &quot;&quot;&quot;</span>
<span class="n">kind</span> <span class="o">=</span> <span class="bp">None</span>
</pre></div>
<pre class="literal-block">
[FAIL]
Traceback (most recent call last):
...
File &quot;/Volumes/FatMan/home/matt/tmp/dnut/test_doughnut.py&quot;, line 13, in test_glazed
self.assertEqual(d.kind, 'glazed')
exceptions.AssertionError: None != 'glazed'
test_doughnut.DoughnutTest.test_glazed
</pre>
<p>It wants a glazed doughnut. Glaze the <tt class="docutils literal">Doughnut</tt>:</p>
<div class="highlight"><pre><span class="c"># flour.py</span>
<span class="k">class</span> <span class="nc">Doughnut</span><span class="p">:</span>
<span class="sd">&quot;&quot;&quot;</span>
<span class="sd"> I am a doughnut</span>
<span class="sd"> &quot;&quot;&quot;</span>
<span class="n">kind</span> <span class="o">=</span> <span class="s">&#39;glazed&#39;</span>
</pre></div>
<pre class="literal-block">
test_doughnut
DoughnutTest
test_glazed ... [OK]
-------------------------------------------------------------------------------
Ran 1 tests in 0.026s
PASSED (successes=1)
</pre>
</div>
<div class="section" id="green">
<h2>Green</h2>
<p>Our first test passed! Now we would refactor, except this code is so simple
there's nothing to refactor. I usually update the documentation in this step.</p>
<div class="highlight"><pre><span class="c"># flour.py</span>
<span class="k">class</span> <span class="nc">Doughnut</span><span class="p">:</span>
<span class="sd">&quot;&quot;&quot;</span>
<span class="sd"> I am a doughnut.</span>
<span class="sd"> @type kind: C{str}</span>
<span class="sd"> @ivar kind: The kind of doughnut this is.</span>
<span class="sd"> &quot;&quot;&quot;</span>
<span class="n">kind</span> <span class="o">=</span> <span class="s">&#39;glazed&#39;</span>
</pre></div>
</div>
<div class="section" id="adding-features">
<h2>Adding features</h2>
<p>Some doughnuts are better than others: new doughnuts are better than old ones;
maple bars are better than glazed; etc... Let's enhance our <tt class="docutils literal">Doughnut</tt> by
providing a method for testing tastiness. This will involve writing
several more tests. I've found it helpful to write the most common case first
followed by a test for every exception to the common case. Glazed doughtnuts
are tasty by default:</p>
<div class="highlight"><pre><span class="c"># test_doughnut.py snippet</span>
<span class="k">def</span> <span class="nf">test_tasty</span><span class="p">(</span><span class="bp">self</span><span class="p">):</span>
<span class="sd">&quot;&quot;&quot;</span>
<span class="sd"> Doughtnuts are tasty by default</span>
<span class="sd"> &quot;&quot;&quot;</span>
<span class="n">d</span> <span class="o">=</span> <span class="n">Doughnut</span><span class="p">()</span>
<span class="bp">self</span><span class="o">.</span><span class="n">assertEqual</span><span class="p">(</span><span class="n">d</span><span class="o">.</span><span class="n">tasty</span><span class="p">(),</span> <span class="bp">True</span><span class="p">)</span>
</pre></div>
<p>Running the tests fails with an <tt class="docutils literal">AttributeError</tt>:</p>
<pre class="literal-block">
exceptions.AttributeError: Doughnut instance has no attribute 'tasty'
</pre>
<p>For brevity, I won't show all the steps to get this test to pass. Here's the
passing, documented <tt class="docutils literal">flour.py</tt>:</p>
<div class="highlight"><pre><span class="c"># flour.py snippet</span>
<span class="k">def</span> <span class="nf">tasty</span><span class="p">(</span><span class="bp">self</span><span class="p">):</span>
<span class="sd">&quot;&quot;&quot;</span>
<span class="sd"> Tests doughnut tastiness.</span>
<span class="sd"> @rtype: C{bool}</span>
<span class="sd"> @return: C{True} if this doughnut is tasty.</span>
<span class="sd"> &quot;&quot;&quot;</span>
<span class="k">return</span> <span class="bp">True</span>
</pre></div>
<p>Various factors contribute to or detract from a doughnut's tastiness including
the age of the doughnut, the location of the doughnut and the cost of the
doughnut. Let's give the <tt class="docutils literal">Doughnut</tt> some attributes so that we can
later change <tt class="docutils literal">tasty</tt> to consider them. Do this by modifying and
renaming the first test <tt class="docutils literal">test_glazed</tt> to <tt class="docutils literal">test_default</tt>:</p>
<div class="highlight"><pre><span class="c"># test_doughnut.py snippet</span>
<span class="k">def</span> <span class="nf">test_default</span><span class="p">(</span><span class="bp">self</span><span class="p">):</span>
<span class="sd">&quot;&quot;&quot;</span>
<span class="sd"> Doughtnuts are glazed, new, not free and in a box.</span>
<span class="sd"> &quot;&quot;&quot;</span>
<span class="n">d</span> <span class="o">=</span> <span class="n">Doughnut</span><span class="p">()</span>
<span class="bp">self</span><span class="o">.</span><span class="n">assertEqual</span><span class="p">(</span><span class="n">d</span><span class="o">.</span><span class="n">kind</span><span class="p">,</span> <span class="s">&#39;glazed&#39;</span><span class="p">)</span>
<span class="bp">self</span><span class="o">.</span><span class="n">assertEqual</span><span class="p">(</span><span class="n">d</span><span class="o">.</span><span class="n">age</span><span class="p">,</span> <span class="mi">0</span><span class="p">)</span>
<span class="bp">self</span><span class="o">.</span><span class="n">assertEqual</span><span class="p">(</span><span class="n">d</span><span class="o">.</span><span class="n">free</span><span class="p">,</span> <span class="bp">False</span><span class="p">)</span>
<span class="bp">self</span><span class="o">.</span><span class="n">assertEqual</span><span class="p">(</span><span class="n">d</span><span class="o">.</span><span class="n">location</span><span class="p">,</span> <span class="s">&#39;box&#39;</span><span class="p">)</span>
</pre></div>
<p>The tests fail until, line by line, we've change <tt class="docutils literal">flour.py</tt> into this (I've
removed the docstrings for brevity):</p>
<div class="highlight"><pre><span class="c"># flour.py</span>
<span class="k">class</span> <span class="nc">Doughnut</span><span class="p">:</span>
<span class="n">kind</span> <span class="o">=</span> <span class="s">&#39;glazed&#39;</span>
<span class="n">age</span> <span class="o">=</span> <span class="mi">0</span>
<span class="n">free</span> <span class="o">=</span> <span class="bp">False</span>
<span class="n">location</span> <span class="o">=</span> <span class="s">&#39;box&#39;</span>
<span class="k">def</span> <span class="nf">tasty</span><span class="p">(</span><span class="bp">self</span><span class="p">):</span>
<span class="k">return</span> <span class="bp">True</span>
</pre></div>
</div>
<div class="section" id="general-case-then-exceptions">
<h2>General case then exceptions</h2>
<p>Let's now change <tt class="docutils literal">tasty()</tt> to use these new attributes in the following way:</p>
<ol class="arabic simple">
<li>Most doughnuts older than 2 days are not tasty</li>
<li>Maple bars are tasty for 5 days</li>
<li>Doughnuts found in dumpsters are not tasty if you have to pay for them.</li>
<li>Free doughnuts in dumpsters are very tasty</li>
<li>Apple fritters are questionable</li>
</ol>
<p>Notice that requirement 2 is an exception to 1, which is the more general case.
So let's write a test for 1 first, make it pass, then add a test for 2.</p>
<p>Test and passing code to implement requirement 1:</p>
<div class="highlight"><pre><span class="c"># test_doughnut.py snippet</span>
<span class="k">def</span> <span class="nf">test_old_doughnuts</span><span class="p">(</span><span class="bp">self</span><span class="p">):</span>
<span class="n">d</span> <span class="o">=</span> <span class="n">Doughnut</span><span class="p">()</span>
<span class="n">d</span><span class="o">.</span><span class="n">age</span> <span class="o">=</span> <span class="mi">3</span>
<span class="bp">self</span><span class="o">.</span><span class="n">assertEqual</span><span class="p">(</span><span class="n">d</span><span class="o">.</span><span class="n">tasty</span><span class="p">(),</span> <span class="bp">False</span><span class="p">)</span>
</pre></div>
<div class="highlight"><pre><span class="c"># flour.py snippet</span>
<span class="k">def</span> <span class="nf">tasty</span><span class="p">(</span><span class="bp">self</span><span class="p">):</span>
<span class="k">if</span> <span class="bp">self</span><span class="o">.</span><span class="n">age</span> <span class="o">&gt;</span> <span class="mi">2</span><span class="p">:</span>
<span class="k">return</span> <span class="bp">False</span>
<span class="k">return</span> <span class="bp">True</span>
</pre></div>
<p>Then the case for maple bars (requirement 2):</p>
<div class="highlight"><pre><span class="c"># test_doughnut.py snippet</span>
<span class="k">def</span> <span class="nf">test_old_maplebars</span><span class="p">(</span><span class="bp">self</span><span class="p">):</span>
<span class="n">d</span> <span class="o">=</span> <span class="n">Doughnut</span><span class="p">()</span>
<span class="n">d</span><span class="o">.</span><span class="n">kind</span> <span class="o">=</span> <span class="s">&#39;maple&#39;</span>
<span class="n">d</span><span class="o">.</span><span class="n">age</span> <span class="o">=</span> <span class="mi">3</span>
<span class="bp">self</span><span class="o">.</span><span class="n">assertEqual</span><span class="p">(</span><span class="n">d</span><span class="o">.</span><span class="n">tasty</span><span class="p">(),</span> <span class="bp">True</span><span class="p">)</span>
<span class="n">d</span><span class="o">.</span><span class="n">age</span> <span class="o">=</span> <span class="mi">6</span>
<span class="bp">self</span><span class="o">.</span><span class="n">assertEqual</span><span class="p">(</span><span class="n">d</span><span class="o">.</span><span class="n">tasty</span><span class="p">(),</span> <span class="bp">False</span><span class="p">)</span>
</pre></div>
<div class="highlight"><pre><span class="c"># flour.py snippet</span>
<span class="k">def</span> <span class="nf">tasty</span><span class="p">(</span><span class="bp">self</span><span class="p">):</span>
<span class="k">if</span> <span class="bp">self</span><span class="o">.</span><span class="n">age</span> <span class="o">&gt;</span> <span class="mi">2</span><span class="p">:</span>
<span class="k">if</span> <span class="bp">self</span><span class="o">.</span><span class="n">kind</span> <span class="o">==</span> <span class="s">&#39;maple&#39;</span><span class="p">:</span>
<span class="k">if</span> <span class="bp">self</span><span class="o">.</span><span class="n">age</span> <span class="o">&gt;</span> <span class="mi">5</span><span class="p">:</span>
<span class="k">return</span> <span class="bp">False</span>
<span class="k">return</span> <span class="bp">True</span>
<span class="k">return</span> <span class="bp">False</span>
<span class="k">return</span> <span class="bp">True</span>
</pre></div>
</div>
<div class="section" id="refactor">
<h2>Refactor</h2>
<p>Ugly code! Let's refactor the tests by removing duplicate code first.
We can reuse the concept of an old doughnut:</p>
<div class="highlight"><pre><span class="c"># test_doughnut.py snippet</span>
<span class="k">def</span> <span class="nf">getOld</span><span class="p">(</span><span class="bp">self</span><span class="p">):</span>
<span class="n">d</span> <span class="o">=</span> <span class="n">Doughnut</span><span class="p">()</span>
<span class="n">d</span><span class="o">.</span><span class="n">age</span> <span class="o">=</span> <span class="mi">3</span>
<span class="k">return</span> <span class="n">d</span>
<span class="k">def</span> <span class="nf">test_old_doughnuts</span><span class="p">(</span><span class="bp">self</span><span class="p">):</span>
<span class="n">d</span> <span class="o">=</span> <span class="bp">self</span><span class="o">.</span><span class="n">getOld</span><span class="p">()</span>
<span class="bp">self</span><span class="o">.</span><span class="n">assertEqual</span><span class="p">(</span><span class="n">d</span><span class="o">.</span><span class="n">tasty</span><span class="p">(),</span> <span class="bp">False</span><span class="p">)</span>
<span class="k">def</span> <span class="nf">test_old_maplebars</span><span class="p">(</span><span class="bp">self</span><span class="p">):</span>
<span class="n">d</span> <span class="o">=</span> <span class="bp">self</span><span class="o">.</span><span class="n">getOld</span><span class="p">()</span>
<span class="n">d</span><span class="o">.</span><span class="n">kind</span> <span class="o">=</span> <span class="s">&#39;maple&#39;</span>
<span class="bp">self</span><span class="o">.</span><span class="n">assertEqual</span><span class="p">(</span><span class="n">d</span><span class="o">.</span><span class="n">tasty</span><span class="p">(),</span> <span class="bp">True</span><span class="p">)</span>
<span class="n">d</span><span class="o">.</span><span class="n">age</span> <span class="o">=</span> <span class="mi">6</span>
<span class="bp">self</span><span class="o">.</span><span class="n">assertEqual</span><span class="p">(</span><span class="n">d</span><span class="o">.</span><span class="n">tasty</span><span class="p">(),</span> <span class="bp">False</span><span class="p">)</span>
</pre></div>
<p>Make sure the tests still pass:</p>
<pre class="literal-block">
trial test_doughnut
test_doughnut
DoughnutTest
test_default ... [OK]
test_old_doughnuts ... [OK]
test_old_maplebars ... [OK]
test_tasty ... [OK]
-------------------------------------------------------------------------------
Ran 4 tests in 0.026s
PASSED (successes=4)
</pre>
<p>Now let's clean up <tt class="docutils literal">tasty()</tt>:</p>
<div class="highlight"><pre><span class="c"># flour.py snippet</span>
<span class="k">def</span> <span class="nf">tasty</span><span class="p">(</span><span class="bp">self</span><span class="p">):</span>
<span class="k">if</span> <span class="bp">self</span><span class="o">.</span><span class="n">kind</span> <span class="o">==</span> <span class="s">&#39;maple&#39;</span><span class="p">:</span>
<span class="k">if</span> <span class="bp">self</span><span class="o">.</span><span class="n">age</span> <span class="o">&gt;</span> <span class="mi">5</span><span class="p">:</span>
<span class="k">return</span> <span class="bp">False</span>
<span class="k">elif</span> <span class="bp">self</span><span class="o">.</span><span class="n">age</span> <span class="o">&gt;</span> <span class="mi">2</span><span class="p">:</span>
<span class="k">return</span> <span class="bp">False</span>
<span class="k">return</span> <span class="bp">True</span>
</pre></div>
<p>The tests still pass:</p>
<pre class="literal-block">
test_doughnut
DoughnutTest
test_default ... [OK]
test_old_doughnuts ... [OK]
test_old_maplebars ... [OK]
test_tasty ... [OK]
-------------------------------------------------------------------------------
Ran 4 tests in 0.026s
PASSED (successes=4)
</pre>
<p>Requirements 1 and 2 are now implemented. After test-driving requirements 3,
4 and 5, we end up with a fully armed and operational doughnut:</p>
<div class="highlight"><pre><span class="c"># test_doughnut.py</span>
<span class="kn">from</span> <span class="nn">unittest</span> <span class="kn">import</span> <span class="n">TestCase</span>
<span class="kn">from</span> <span class="nn">flour</span> <span class="kn">import</span> <span class="n">Doughnut</span><span class="p">,</span> <span class="n">Questionable</span>
<span class="k">class</span> <span class="nc">DoughnutTest</span><span class="p">(</span><span class="n">TestCase</span><span class="p">):</span>
<span class="k">def</span> <span class="nf">test_default</span><span class="p">(</span><span class="bp">self</span><span class="p">):</span>
<span class="n">d</span> <span class="o">=</span> <span class="n">Doughnut</span><span class="p">()</span>
<span class="bp">self</span><span class="o">.</span><span class="n">assertEqual</span><span class="p">(</span><span class="n">d</span><span class="o">.</span><span class="n">kind</span><span class="p">,</span> <span class="s">&#39;glazed&#39;</span><span class="p">)</span>
<span class="bp">self</span><span class="o">.</span><span class="n">assertEqual</span><span class="p">(</span><span class="n">d</span><span class="o">.</span><span class="n">age</span><span class="p">,</span> <span class="mi">0</span><span class="p">)</span>
<span class="bp">self</span><span class="o">.</span><span class="n">assertEqual</span><span class="p">(</span><span class="n">d</span><span class="o">.</span><span class="n">free</span><span class="p">,</span> <span class="bp">False</span><span class="p">)</span>
<span class="bp">self</span><span class="o">.</span><span class="n">assertEqual</span><span class="p">(</span><span class="n">d</span><span class="o">.</span><span class="n">location</span><span class="p">,</span> <span class="s">&#39;box&#39;</span><span class="p">)</span>
<span class="k">def</span> <span class="nf">test_tasty</span><span class="p">(</span><span class="bp">self</span><span class="p">):</span>
<span class="n">d</span> <span class="o">=</span> <span class="n">Doughnut</span><span class="p">()</span>
<span class="bp">self</span><span class="o">.</span><span class="n">assertEqual</span><span class="p">(</span><span class="n">d</span><span class="o">.</span><span class="n">tasty</span><span class="p">(),</span> <span class="bp">True</span><span class="p">)</span>
<span class="k">def</span> <span class="nf">getOld</span><span class="p">(</span><span class="bp">self</span><span class="p">):</span>
<span class="n">d</span> <span class="o">=</span> <span class="n">Doughnut</span><span class="p">()</span>
<span class="n">d</span><span class="o">.</span><span class="n">age</span> <span class="o">=</span> <span class="mi">3</span>
<span class="k">return</span> <span class="n">d</span>
<span class="k">def</span> <span class="nf">test_old_doughnuts</span><span class="p">(</span><span class="bp">self</span><span class="p">):</span>
<span class="n">d</span> <span class="o">=</span> <span class="bp">self</span><span class="o">.</span><span class="n">getOld</span><span class="p">()</span>
<span class="bp">self</span><span class="o">.</span><span class="n">assertEqual</span><span class="p">(</span><span class="n">d</span><span class="o">.</span><span class="n">tasty</span><span class="p">(),</span> <span class="bp">False</span><span class="p">)</span>
<span class="k">def</span> <span class="nf">test_old_maplebars</span><span class="p">(</span><span class="bp">self</span><span class="p">):</span>
<span class="n">d</span> <span class="o">=</span> <span class="bp">self</span><span class="o">.</span><span class="n">getOld</span><span class="p">()</span>
<span class="n">d</span><span class="o">.</span><span class="n">kind</span> <span class="o">=</span> <span class="s">&#39;maple&#39;</span>
<span class="bp">self</span><span class="o">.</span><span class="n">assertEqual</span><span class="p">(</span><span class="n">d</span><span class="o">.</span><span class="n">tasty</span><span class="p">(),</span> <span class="bp">True</span><span class="p">)</span>
<span class="n">d</span><span class="o">.</span><span class="n">age</span> <span class="o">=</span> <span class="mi">6</span>
<span class="bp">self</span><span class="o">.</span><span class="n">assertEqual</span><span class="p">(</span><span class="n">d</span><span class="o">.</span><span class="n">tasty</span><span class="p">(),</span> <span class="bp">False</span><span class="p">)</span>
<span class="k">def</span> <span class="nf">getDumpster</span><span class="p">(</span><span class="bp">self</span><span class="p">):</span>
<span class="n">d</span> <span class="o">=</span> <span class="n">Doughnut</span><span class="p">()</span>
<span class="n">d</span><span class="o">.</span><span class="n">location</span> <span class="o">=</span> <span class="s">&#39;dumpster&#39;</span>
<span class="k">return</span> <span class="n">d</span>
<span class="k">def</span> <span class="nf">test_dumpster</span><span class="p">(</span><span class="bp">self</span><span class="p">):</span>
<span class="n">d</span> <span class="o">=</span> <span class="bp">self</span><span class="o">.</span><span class="n">getDumpster</span><span class="p">()</span>
<span class="bp">self</span><span class="o">.</span><span class="n">assertEqual</span><span class="p">(</span><span class="n">d</span><span class="o">.</span><span class="n">tasty</span><span class="p">(),</span> <span class="bp">False</span><span class="p">)</span>
<span class="k">def</span> <span class="nf">test_free_dumpster</span><span class="p">(</span><span class="bp">self</span><span class="p">):</span>
<span class="n">d</span> <span class="o">=</span> <span class="bp">self</span><span class="o">.</span><span class="n">getDumpster</span><span class="p">()</span>
<span class="n">d</span><span class="o">.</span><span class="n">free</span> <span class="o">=</span> <span class="bp">True</span>
<span class="bp">self</span><span class="o">.</span><span class="n">assertEqual</span><span class="p">(</span><span class="n">d</span><span class="o">.</span><span class="n">tasty</span><span class="p">(),</span> <span class="bp">True</span><span class="p">)</span>
<span class="k">def</span> <span class="nf">test_apple_fritters</span><span class="p">(</span><span class="bp">self</span><span class="p">):</span>
<span class="n">d</span> <span class="o">=</span> <span class="n">Doughnut</span><span class="p">()</span>
<span class="n">d</span><span class="o">.</span><span class="n">kind</span> <span class="o">=</span> <span class="s">&#39;apple fritter&#39;</span>
<span class="bp">self</span><span class="o">.</span><span class="n">assertRaises</span><span class="p">(</span><span class="n">Questionable</span><span class="p">,</span> <span class="n">d</span><span class="o">.</span><span class="n">tasty</span><span class="p">)</span>
</pre></div>
<div class="highlight"><pre><span class="c"># flour.py</span>
<span class="k">class</span> <span class="nc">Questionable</span><span class="p">(</span><span class="ne">Exception</span><span class="p">):</span> <span class="k">pass</span>
<span class="k">class</span> <span class="nc">Doughnut</span><span class="p">:</span>
<span class="n">kind</span> <span class="o">=</span> <span class="s">&#39;glazed&#39;</span>
<span class="n">age</span> <span class="o">=</span> <span class="mi">0</span>
<span class="n">free</span> <span class="o">=</span> <span class="bp">False</span>
<span class="n">location</span> <span class="o">=</span> <span class="s">&#39;box&#39;</span>
<span class="k">def</span> <span class="nf">tasty</span><span class="p">(</span><span class="bp">self</span><span class="p">):</span>
<span class="k">if</span> <span class="bp">self</span><span class="o">.</span><span class="n">kind</span> <span class="o">==</span> <span class="s">&#39;apple fritter&#39;</span><span class="p">:</span>
<span class="k">raise</span> <span class="n">Questionable</span><span class="p">(</span><span class="s">&#39;Apple fritters are of questionable tastiness&#39;</span><span class="p">)</span>
<span class="k">elif</span> <span class="bp">self</span><span class="o">.</span><span class="n">kind</span> <span class="o">==</span> <span class="s">&#39;maple&#39;</span><span class="p">:</span>
<span class="k">if</span> <span class="bp">self</span><span class="o">.</span><span class="n">age</span> <span class="o">&gt;</span> <span class="mi">5</span><span class="p">:</span>
<span class="k">return</span> <span class="bp">False</span>
<span class="k">elif</span> <span class="bp">self</span><span class="o">.</span><span class="n">age</span> <span class="o">&gt;</span> <span class="mi">2</span><span class="p">:</span>
<span class="k">return</span> <span class="bp">False</span>
<span class="k">if</span> <span class="bp">self</span><span class="o">.</span><span class="n">location</span> <span class="o">==</span> <span class="s">&#39;dumpster&#39;</span><span class="p">:</span>
<span class="k">if</span> <span class="ow">not</span> <span class="bp">self</span><span class="o">.</span><span class="n">free</span><span class="p">:</span>
<span class="k">return</span> <span class="bp">False</span>
<span class="k">return</span> <span class="bp">True</span>
</pre></div>
</div>
</div>
<div class="section" id="make-the-least-change-possible">
<h1>Make the least change possible</h1>
<p>When trying to first make <tt class="docutils literal">test_old_doughnuts</tt> pass, we could have explicitly
limited oldness to 'glazed' doughnuts:</p>
<div class="highlight"><pre><span class="c"># flour.py snippet</span>
<span class="k">def</span> <span class="nf">tasty</span><span class="p">(</span><span class="bp">self</span><span class="p">):</span>
<span class="k">if</span> <span class="bp">self</span><span class="o">.</span><span class="n">kind</span> <span class="o">==</span> <span class="s">&#39;glazed&#39;</span><span class="p">:</span>
<span class="k">if</span> <span class="bp">self</span><span class="o">.</span><span class="n">age</span> <span class="o">&gt;</span> <span class="mi">2</span><span class="p">:</span>
<span class="k">return</span> <span class="bp">False</span>
<span class="k">return</span> <span class="bp">True</span>
</pre></div>
<p>When passing <tt class="docutils literal">test_dumpster</tt> we could likewise have assumed that it should
be for 'glazed' doughnuts. Doing so, we could end up with this version:</p>
<div class="highlight"><pre><span class="c"># flour.py snippet</span>
<span class="k">def</span> <span class="nf">tasty</span><span class="p">(</span><span class="bp">self</span><span class="p">):</span>
<span class="k">if</span> <span class="bp">self</span><span class="o">.</span><span class="n">kind</span> <span class="o">==</span> <span class="s">&#39;glazed&#39;</span><span class="p">:</span>
<span class="k">if</span> <span class="bp">self</span><span class="o">.</span><span class="n">age</span> <span class="o">&gt;</span> <span class="mi">2</span><span class="p">:</span>
<span class="k">return</span> <span class="bp">False</span>
<span class="k">if</span> <span class="bp">self</span><span class="o">.</span><span class="n">location</span> <span class="o">==</span> <span class="s">&#39;dumpster&#39;</span><span class="p">:</span>
<span class="k">if</span> <span class="ow">not</span> <span class="bp">self</span><span class="o">.</span><span class="n">free</span><span class="p">:</span>
<span class="k">return</span> <span class="bp">False</span>
<span class="k">elif</span> <span class="bp">self</span><span class="o">.</span><span class="n">kind</span> <span class="o">==</span> <span class="s">&#39;apple fritter&#39;</span><span class="p">:</span>
<span class="k">raise</span> <span class="n">Questionable</span><span class="p">()</span>
<span class="k">elif</span> <span class="bp">self</span><span class="o">.</span><span class="n">age</span> <span class="o">&gt;</span> <span class="mi">5</span><span class="p">:</span>
<span class="k">return</span> <span class="bp">False</span>
<span class="k">return</span> <span class="bp">True</span>
</pre></div>
<p>The tests pass, but the code is wrong according to requirements 1, 3 and 4.
Make the smallest change possible when writing code to pass the tests.</p>
</div>
<div class="section" id="test-writing-test-driving">
<h1>Test writing != test driving</h1>
<p>Tests are like a car. You can either use the car to drive you where you want
to go, or you can tie a rope to the car and pull it with you.</p>
<img alt="http://www.acraftphoto.com/blogger/2009/2009.09.30.truckpull.049.jpg" src="http://www.acraftphoto.com/blogger/2009/2009.09.30.truckpull.049.jpg" />
<p>If you pull the car around long enough, you'll eventually abandon it because
it's too burdensome.</p>
<p>&quot;Having tests&quot; is not the same as test-driven development. Don't say, &quot;I have
tests.&quot; Instead say, &quot;I test-drove.&quot;</p>
</div>
<div class="section" id="coverage">
<h1>Coverage</h1>
<p>In general, TDD leads to significantly better code coverage, and code coverage
provides an indication as to whether something was test driven. For
simple code, coverage can usually be 100%.</p>
<p>See <a class="reference external" href="managing.html">Managing</a> for how to run coverage.</p>
</div>
<div class="section" id="what-other-people-say">
<h1>What other people say</h1>
<ul class="simple">
<li><a class="reference external" href="http://www.agiledata.org/essays/tdd.html">http://www.agiledata.org/essays/tdd.html</a></li>
<li><a class="reference external" href="http://en.wikipedia.org/wiki/Test-driven_development">http://en.wikipedia.org/wiki/Test-driven_development</a></li>
<li><a class="reference external" href="http://bazaar.launchpad.net/~exarkun/+junk/training/view/head:/testing/tdd_outline.txt">http://bazaar.launchpad.net/~exarkun/+junk/training/view/head:/testing/tdd_outline.txt</a></li>
</ul>
</div>
</div>
</body>
</html>

An Example of Test Driven Development

Back to index

Test Driven Development (TDD) is a method of code development. You lead with tests, and follow with code.

Steps

  1. Red - write a failing test
  2. Green - make the test pass
  3. Refactor - make it DRY instead of WET; optimize; document

Doughnut

In the following example, we'll test-drive the production of a Doughnut. There are different kinds of doughnuts, but most are glazed.

Red

The first step is to write a failing test for a Doughtnut class that is 'glazed'.

# test_doughnut.py
from unittest import TestCase

from flour import Doughnut

class DoughnutTest(TestCase):

    def test_glazed(self):
        """
        Doughtnuts are glazed by default
        """
        d = Doughnut()
        self.assertEqual(d.kind, 'glazed')

Run the test to see it fail. I like using Twisted's trial to run the tests.

$ trial test_doughnut
test_doughnut ...                                                       [ERROR]

===============================================================================
[ERROR]
Traceback (most recent call last):
  ...
  File "/Volumes/FatMan/home/matt/tmp/dnut/test_doughnut.py", line 4, in <module>
    from flour import Doughnut
exceptions.ImportError: No module named flour

test_doughnut
-------------------------------------------------------------------------------
Ran 1 tests in 0.026s

FAILED (errors=1)

Look at the last line:

exceptions.ImportError: No module named flour

The test fails because there is no module named flour. You might be tempted to make a class named Doughnut with an attribute kind set to 'glazed' in a file named flour.py -- DON'T! Resolve only the error at hand by creating a file named flour.py.

# flour.py

Run the tests again to see that we solved the "No module named flour" error:

===============================================================================
[ERROR]
Traceback (most recent call last):
  ...
  File "/Volumes/FatMan/home/matt/tmp/dnut/test_doughnut.py", line 4, in <module>
    from flour import Doughnut
exceptions.ImportError: cannot import name Doughnut

test_doughnut
-------------------------------------------------------------------------------

It complains that we there's nothing named Doughnut. Let's make a Doughnut class:

# flour.py

class Doughnut:
    """
    I am a doughnut
    """

On to the next failure:

[ERROR]
Traceback (most recent call last):
  ...
  File "/Volumes/FatMan/home/matt/tmp/dnut/test_doughnut.py", line 13, in test_glazed
    self.assertEqual(d.kind, 'glazed')
exceptions.AttributeError: Doughnut instance has no attribute 'kind'

test_doughnut.DoughnutTest.test_glazed

To make the AttributeError go away add a kind attribute to Doughnut:

# flour.py

class Doughnut:
    """
    I am a doughnut
    """

    kind = None
[FAIL]
Traceback (most recent call last):
  ...
  File "/Volumes/FatMan/home/matt/tmp/dnut/test_doughnut.py", line 13, in test_glazed
    self.assertEqual(d.kind, 'glazed')
exceptions.AssertionError: None != 'glazed'

test_doughnut.DoughnutTest.test_glazed

It wants a glazed doughnut. Glaze the Doughnut:

# flour.py

class Doughnut:
    """
    I am a doughnut
    """

    kind = 'glazed'
test_doughnut
  DoughnutTest
    test_glazed ...                                                        [OK]

-------------------------------------------------------------------------------
Ran 1 tests in 0.026s

PASSED (successes=1)

Green

Our first test passed! Now we would refactor, except this code is so simple there's nothing to refactor. I usually update the documentation in this step.

# flour.py

class Doughnut:
    """
    I am a doughnut.

    @type kind: C{str}
    @ivar kind: The kind of doughnut this is.
    """

    kind = 'glazed'

Adding features

Some doughnuts are better than others: new doughnuts are better than old ones; maple bars are better than glazed; etc... Let's enhance our Doughnut by providing a method for testing tastiness. This will involve writing several more tests. I've found it helpful to write the most common case first followed by a test for every exception to the common case. Glazed doughtnuts are tasty by default:

# test_doughnut.py snippet        

    def test_tasty(self):
        """
        Doughtnuts are tasty by default
        """
        d = Doughnut()
        self.assertEqual(d.tasty(), True)

Running the tests fails with an AttributeError:

exceptions.AttributeError: Doughnut instance has no attribute 'tasty'

For brevity, I won't show all the steps to get this test to pass. Here's the passing, documented flour.py:

# flour.py snippet

    def tasty(self):
        """
        Tests doughnut tastiness.

        @rtype: C{bool}
        @return: C{True} if this doughnut is tasty.        
        """
        return True

Various factors contribute to or detract from a doughnut's tastiness including the age of the doughnut, the location of the doughnut and the cost of the doughnut. Let's give the Doughnut some attributes so that we can later change tasty to consider them. Do this by modifying and renaming the first test test_glazed to test_default:

# test_doughnut.py snippet

    def test_default(self):
        """
        Doughtnuts are glazed, new, not free and in a box.
        """
        d = Doughnut()
        self.assertEqual(d.kind, 'glazed')
        self.assertEqual(d.age, 0)
        self.assertEqual(d.free, False)
        self.assertEqual(d.location, 'box')

The tests fail until, line by line, we've change flour.py into this (I've removed the docstrings for brevity):

# flour.py

class Doughnut:

    kind = 'glazed'
    age = 0
    free = False
    location = 'box'

    def tasty(self):
        return True

General case then exceptions

Let's now change tasty() to use these new attributes in the following way:

  1. Most doughnuts older than 2 days are not tasty
  2. Maple bars are tasty for 5 days
  3. Doughnuts found in dumpsters are not tasty if you have to pay for them.
  4. Free doughnuts in dumpsters are very tasty
  5. Apple fritters are questionable

Notice that requirement 2 is an exception to 1, which is the more general case. So let's write a test for 1 first, make it pass, then add a test for 2.

Test and passing code to implement requirement 1:

# test_doughnut.py snippet

    def test_old_doughnuts(self):
        d = Doughnut()
        d.age = 3
        self.assertEqual(d.tasty(), False)
# flour.py snippet

    def tasty(self):
        if self.age > 2:
            return False
        return True

Then the case for maple bars (requirement 2):

# test_doughnut.py snippet        

    def test_old_maplebars(self):
        d = Doughnut()
        d.kind = 'maple'
        d.age = 3
        self.assertEqual(d.tasty(), True)

        d.age = 6
        self.assertEqual(d.tasty(), False)
# flour.py snippet

    def tasty(self):
        if self.age > 2:
            if self.kind == 'maple':
                if self.age > 5:
                    return False
                return True
            return False
        return True

Refactor

Ugly code! Let's refactor the tests by removing duplicate code first. We can reuse the concept of an old doughnut:

# test_doughnut.py snippet

    def getOld(self):
        d = Doughnut()
        d.age = 3
        return d

    def test_old_doughnuts(self):
        d = self.getOld()
        self.assertEqual(d.tasty(), False)

    def test_old_maplebars(self):
        d = self.getOld()
        d.kind = 'maple'
        self.assertEqual(d.tasty(), True)

        d.age = 6
        self.assertEqual(d.tasty(), False)

Make sure the tests still pass:

trial test_doughnut
test_doughnut
  DoughnutTest
    test_default ...                                                       [OK]
    test_old_doughnuts ...                                                 [OK]
    test_old_maplebars ...                                                 [OK]
    test_tasty ...                                                         [OK]

-------------------------------------------------------------------------------
Ran 4 tests in 0.026s

PASSED (successes=4)

Now let's clean up tasty():

# flour.py snippet

    def tasty(self):
        if self.kind == 'maple':
            if self.age > 5:
                return False
        elif self.age > 2:
            return False
        return True

The tests still pass:

test_doughnut
  DoughnutTest
    test_default ...                                                       [OK]
    test_old_doughnuts ...                                                 [OK]
    test_old_maplebars ...                                                 [OK]
    test_tasty ...                                                         [OK]

-------------------------------------------------------------------------------
Ran 4 tests in 0.026s

PASSED (successes=4)

Requirements 1 and 2 are now implemented. After test-driving requirements 3, 4 and 5, we end up with a fully armed and operational doughnut:

# test_doughnut.py
from unittest import TestCase

from flour import Doughnut, Questionable

class DoughnutTest(TestCase):


    def test_default(self):
        d = Doughnut()
        self.assertEqual(d.kind, 'glazed')
        self.assertEqual(d.age, 0)
        self.assertEqual(d.free, False)
        self.assertEqual(d.location, 'box')


    def test_tasty(self):
        d = Doughnut()
        self.assertEqual(d.tasty(), True)


    def getOld(self):
        d = Doughnut()
        d.age = 3
        return d


    def test_old_doughnuts(self):
        d = self.getOld()
        self.assertEqual(d.tasty(), False)


    def test_old_maplebars(self):
        d = self.getOld()
        d.kind = 'maple'
        self.assertEqual(d.tasty(), True)

        d.age = 6
        self.assertEqual(d.tasty(), False)


    def getDumpster(self):
        d = Doughnut()
        d.location = 'dumpster'
        return d


    def test_dumpster(self):
        d = self.getDumpster()
        self.assertEqual(d.tasty(), False)


    def test_free_dumpster(self):
        d = self.getDumpster()
        d.free = True
        self.assertEqual(d.tasty(), True)


    def test_apple_fritters(self):
        d = Doughnut()
        d.kind = 'apple fritter'
        self.assertRaises(Questionable, d.tasty)
# flour.py

class Questionable(Exception): pass

class Doughnut:

    kind = 'glazed'
    age = 0
    free = False
    location = 'box'


    def tasty(self):
        if self.kind == 'apple fritter':
            raise Questionable('Apple fritters are of questionable tastiness')
        elif self.kind == 'maple':
            if self.age > 5:
                return False
        elif self.age > 2:
            return False
        if self.location == 'dumpster':
            if not self.free:
                return False
        return True

Make the least change possible

When trying to first make test_old_doughnuts pass, we could have explicitly limited oldness to 'glazed' doughnuts:

# flour.py snippet

    def tasty(self):
        if self.kind == 'glazed':
            if self.age > 2:
                return False
        return True

When passing test_dumpster we could likewise have assumed that it should be for 'glazed' doughnuts. Doing so, we could end up with this version:

# flour.py snippet

    def tasty(self):
        if self.kind == 'glazed':
            if self.age > 2:
                return False
            if self.location == 'dumpster':
                if not self.free:
                    return False
        elif self.kind == 'apple fritter':
            raise Questionable()
        elif self.age > 5:
                return False
        return True

The tests pass, but the code is wrong according to requirements 1, 3 and 4. Make the smallest change possible when writing code to pass the tests.

Test writing != test driving

Tests are like a car. You can either use the car to drive you where you want to go, or you can tie a rope to the car and pull it with you.

image

If you pull the car around long enough, you'll eventually abandon it because it's too burdensome.

"Having tests" is not the same as test-driven development. Don't say, "I have tests." Instead say, "I test-drove."

Coverage

In general, TDD leads to significantly better code coverage, and code coverage provides an indication as to whether something was test driven. For simple code, coverage can usually be 100%.

See Managing for how to run coverage.

What other people say

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment