This exercise (video 8m 53s)
shows how to migrate a test from using
directly to the exploiting the
referencetest capabilities in
the TDDA library.
(If you use
pytest for writing tests,
you might prefer the
of this exercise.)
We will see how even simple use of
- makes it much easier to see how tests have failed when complex outputs are generated
- helps us to update reference outputs (the expected values) when we have verified that a new behaviour is correct
- allows us easily to write tests of code whose outputs are not identical from run to run. We do this by specifying exclusions from the comparisons used in assertions.
★ You need to have the TDDA Python library (version 1.0.31 or newer) installed see installation. Use
to check the version that you have.
Step 1: Copy the exercises
You need to change to some directory in which you're happy to create three
new directories with data. We are use
~/tmp for this. Then copy the
$ cd ~/tmp $ tdda examples # copy the example code
Step 2: Go the exercise files and examine them:
$ cd referencetest_examples/exercises-unittest/exercise1 # Go to exercise1
You should have at least the following three files:
$ ls expected.html generators.py test_all.py
generators.pycontains a function called
generate_stringthat, when called, returns HTML text suitable for viewing as a web page.
expected.htmlis the result of calling that function, saved to file
test_all.pycontains a single
unittest-based test of that file.
It's probably useful to look at the web page
expected.html in a browser,
either by navigating to it in a file browser and double clicking it,
or by using
if your OS supports this. As you can see, it's just some text and an image. The image is an inline SVG vector image, generated along with the text.
Also have a look at the test code. The core part of it is very short:
import unittest from generators import generate_string class TestFileGeneration(unittest.TestCase): def testExampleStringGeneration(self): actual = generate_string() with open('expected.html') as f: expected = f.read() self.assertEqual(actual, expected) if __name__ == '__main__': unittest.main()
generate_string()to create the content
- stores its output in the variable
- reads the expected content into the variable
- asserts that the two strings are the same.
Step 3. Run the test, which should fail
$ python test_all.py # This will work with Python 3 or Python2
You should get a failure, but it will probably be quite hard to see exactly what the differences are.
We'll convert the test to use the TDDA libraries referencetest and see how that helps.
Step 4. Change the code to use referencetest.
First we need our test to use
ReferenceTestCase is a subclass of
- Change the import statement to
from tdda.referencetest import ReferenceTestCase
ReferenceTestCasein the class declaration
The result is:
from tdda.referencetest import ReferenceTestCase from generators import generate_string class TestFileGeneration(ReferenceTestCase): def testExampleStringGeneration(self): actual = generate_string() with open('expected.html') as f: expected = f.read() self.assertEqual(actual, expected) if __name__ == '__main__': ReferenceTestCase.main()
If you run this, it's behaviour should be exactly the same, because
we haven't used any of the extra features of
Step 5. Change the assertion to use
ReferenceTestCase provides the
which expects as its first positional arguments an actual string
and the path to a file containing the expected result. So:
expected.htmlas the second argument to the assertion
- Delete the two lines reading the file and assigning to
expectedas we no longer need that.
def testExampleStringGeneration(self): actual = generate_string() self.assertStringCorrect(actual, 'expected.html')
Step 6. Run the modified test
$ python test_all.py
You should see very different output, that includes, near the end, something like this:
Expected file expected.html Compare raw with: diff /var/folders/zv/3xvhmvpj0216687_pk__2f5h0000gn/T/actual-raw-expected.html expected.html Compare post-processed with: diff /var/folders/zv/3xvhmvpj0216687_pk__2f5h0000gn/T/actual-expected.html /var/folders/zv/3xvhmvpj0216687_pk__2f5h0000gn/T/expected-expected.html
Because the test failed, the TDDA library has written a copy of the
actual ouput to file to make it easy for us to examine it and to use
commands to see how it actually differs from what we expected. (In fact,
it's written out two copies, a "raw" and a "post-precocessed" one, but we
haven't used any processing, so they will be the same in our case. So
we ignore the second diff command suggested for now.)
It's also given us the precise
diff command we need to see the differences
between our actual and expected output.
Step 6a. Copy the first
diff command and run it. You should see something similar to this:
$ diff /var/folders/zv/3xvhmvpj0216687_pk__2f5h0000gn/T/actual-raw-expected.html expected.html 5,6c5,6 < Copyright (c) Stochastic Solutions, 2016 < Version 1.0.0 — > Copyright (c) Stochastic Solutions Limited, 2016 > Version 0.0.0 35c35 < </html> \ No newline at end of file — > </html>
(If you have a visual diff tool, can also use that. For example,
on a Mac, if you have Xcode installed, you should have the
opendiff command available.)
The diff makes it clear that there are three differences:
- The copyright notice has changed slightly
- The version number has changed
- The string doesn't have a newline at the end, whereas the file does.
The Copyright and version numbers lines are both in comments in the HTML,
so don't affect the rendering at all. You might want to confirm that if
you look at the actual file it saved (
/var/folders/zv/3xvhmvpj0216687_pk__2f5h0000gn/T/actual-raw-expected.html, the first file in the diff command),
you should see that it looks identical.
In this case, therefore, we might now feel that we should simply
expected.html with what
generate_string() is now
producing. It would be (by design) extremely easy to change the
in the command it gave is to
cp to achieve that.
However, there's better thing we can do in this case.
Step 7. Specify exclusions
Standing back, it seems obvious likely that periodically the version number and Copyright line written to comments in the HTML will change. If the only difference between out expected output and what we actually generate are those, we'd probably prefer the test didn't fail.
assertStringCorrect method from
referencetest gives us several
mechanisms for specifying changes that can be ignored when checking whether
a string is correct. The simplest one, which will be enough for our example,
is just to specify strings which, if they occur on a line in the output,
case differences in those lines to be ignored, so that the assertion
Step 7a. Add the
ignore_substrings parameter to
assertStringCorrect as follows:
self.assertStringCorrect(actual, 'expected.html', ignore_substrings=['Copyright', 'Version'])
Step 7b. Run the test again. It should now pass:
$ python3 test_all.py . ---------------------------------------------------------------------- Ran 1 test in 0.002s OK
Recap: What we have seen
unittest-based tests to use
When we do that, we gain access to powerful new assert methods such as
assertStringCorrect. Among the immediate benefits:
- When there is failure, this method saves the failing output to a temporary file
- It tells you the exact
diffcommand you need to see be able to see differences
- This also makes it very easy to copy the new "known good" answer into place if you've verified that the new answer is now correct. (In fact, the library also has a more powerful way to do this, as we'll see in a later exercise).
assertStringCorrectmethod also has a number of mechanisms for allowing specific expected differences to occur without causing the test to fail. The simplest of these mechanisms is the
ignore_substringskeyword argument we used here.