CS 372 Spring 2016 > Notes for February 10, 2016
CS 372 Spring 2016
Notes
for February 10, 2016
Testing
Unit Testing & TDD
Recall that a unit is a small testable piece of code—for example, a single function. When we do unit testing, we test the units in the software we are developing.
Unit testing is typically done using a unit-testing framework. Many good frameworks are freely available; we will look at a couple of these shortly.
We keep our testing code in the same repository as the project code. A good rule is that no code is checked in unless it makes all tests pass.
In the Test-Driven Development (TDD) methodology, unit tests are written before implementing software features. Specifically, when we do TDD, we follow a four-step procedure when adding a new feature to software.
- Add one or more tests for the feature to the project unit tests.
- Make sure that the new tests fail. Note: In order for tests to fail, the testing code must compile and execute. If we are testing a new function, then we need to write a dummy version of that function.
- Add the feature to the software.
- Make sure that all tests pass. If they do not, then keep working until they do.
Note that TDD involves only black-box testing. When you use TDD, you may wish to add white-box tests as well; but these would be in addition to the tests you write as part of TDD.
Our in-class development will be done using TDD. You are encouraged to use TDD outside of class as well.
Unit-Testing Framework Example #1: Catch (C++)
Introduction
Catch is a simple, easy to use,
high-quality unit-testing framework for C++ projects.
It is available as a single header file (catch.hpp
).
A link to the GitHub page for Catch is on the class webpage;
it includes downloads, a tutorial, and a complete reference.
Catch is distributed under a permissive FLOSS license
(“FLOSS” = Free/Libre Open-Source Software).
You should read this license;
it is not long.
But essentially,
it says that you may distribute catch.hpp
with your project, free of charge,
as long as copyright notices are left intact
and the license is included.
Thus,
if you include catch.hpp
in your project,
then it is a good idea to include the license file there, too.
File catch.hpp
requires no special installation;
it can simply be included in a project’s
source tree—the directory structure holding
the project source code.
Code Organization
When we include unit-testing code in a project, we need to be able to execute the tests, and we also need to be able to execute the project code without running the tests.
One way to do this in C++ is to put project code into header files.
These headers contain no tests.
A separate main-program file holds the unit-test suites;
it includes the project headers and the test-framework header(s)
(catch.hpp
in this case).
If the project itself is a complete program,
then the project main-program file includes
the project headers—again, these contain no tests.
We developed a function to compute Fibonacci numbers
using TDD.
See
fibo.h
for the Fibonacci-computation code.
See
fibo_main.cpp
for a simple main program that uses the above code.
See
fibo_test.cpp
for a test suite;
this requires
catch.hpp
.
Note
If you are doing C++ development in this class, then you may use any unit-testing framework you wish, as long as you are using it legally, and you are able to demonstrate execution of your test suites in class.
In particular, if you are using an IDE, then this may include its own unit-testing framework, and you may wish to use that.
However, if you are unsure what testing framework to use, then Catch is certainly a good one.
See the Catch website for complete information on using Catch.
Unit-Testing Framework Example #2: doctest (Python)
Introduction
The doctest module is a part of the Standard Library of the Python programming language. It allows unit tests to be included in the documentation portion of a Python source file, for optional execution.
Docstrings are a Python feature for including
documentation in a Python source file
in a way that allows for separate documentation files to be
generated automatically.
It the first item in a Python source file, function,
or class is a string, then that string is the docstring
for the file, function, or class.
Typically, these use the Python multiline string syntax,
in which strings begin and end with three double-quote marks
(“"""
”).
Here is a simple Python function to compute the cube of a number. Note the docstring.
[Python 3.x]
def cube(n): """cube(n) -> the cube of n.""" return n**3
Writing doctests
When we use the Python interactive environment,
we can type Python expressions and statements for immediate
execution.
Below, “>>>
”
is the prompt for the Python interactive environment.
[Interactive Python 3.x]
>>> cube(12) 1728 >>> cube(0) 0
The idea of a doctest is that an interactive Python session is included in a docstring essentially unchanged.
[Python 3.x]
def cube(n): """cube(n) -> the cube of n. >>> cube(12) 1728 >>> cube(0) 0 """ return n**3
Now we have two test cases:
when we call function cube
with parameter 12
,
then it should return 1728
.
And when we call it with parameter 0
,
then it should return 0
.
Executing doctests
If a Unix-line command-line interface is available,
then we can execute the doctests in a file
as follows.
Suppose our Python source file is called cube.py
.
Below, “$
” is the command-line prompt.
[*ix Command Line]
$ python3 -m doctest cube.py
The above will print the results of failing doctests, giving no output if all tests pass.
To print the results of all doctests,
switch to verbose mode
by adding “-v
” to the command line,
as shown below.
[*ix Command Line]
$ python3 -m doctest cube.py -v
If a file is meant to be imported as a library in other Python source files, then a common thing to do is to give the file its own main program, which does not execute if the file is imported. The main program executes the doctests. This is done by writing code like the following at file scope (not inside a function or class), usually a the end of the file.
[Python 3.x]
if __name__ == "__main__": # Executed as separate progam? import doctest # Get the doctest module print("Running doctests") doctest.testmod(verbose=True) # Execute all doctests, verbose mode
Above, eliminate “verbose=True
”
to print only the results of failing doctests.
See
euclid.py
for a Python 3.x source file that includes doctests,
along with a main program, as above, that executes them.
Writing Unit Tests
When we do unit testing, coverage refers to how much of our package’s functionality is checked by the tests we have written. Greater coverage means more effective testing.
Therefore, when writing unit tests for a feature, we probably do not want to write only a single test. Our tests should be comprehensive enough that, if they all pass, then we can have confidence that the feature is implemented correctly.
In particular, consider the following.
- Is every public (exported) function in the package called by some test?
- If there is something that can vary (a parameter, a parameter type, the size of a data structure, etc.), are there tests that involve different values of it?
- Are there tests for unusual situations and/or corner cases? (For example, what if a data structure is empty? What if a number is zero? What if a string is empty, consists entirely of blanks, or is extremely long?)
When a function takes an object whose type is in some inheritance hierarchy defined in our project, it is common to test using mock objects. A mock object is an object that exists only for testing purposes. It implements whatever interface it needs to, but it may do so in a minimalist manner. Some of its member functions may do logging: keeping track of how they are called.