Test cases are an important aspect of any production software project. They help us guard against regression errors and other bugs which may occur as a result of making changes to a code base. When test cases are well written, they may also serve as documentation for the code base. In this installment we will implement test cases for a function that schedules or books appointments for a patient. Our goal is to provide coverage for the various execution paths of our code in a well defined and structured manner.
The following examples are compatible with Python 2.7.x.:
Nose is a unit test framework that extends Python’s built-in unittest framework. Nose’s notable features include:
- Auto-detects tests using a common naming convention
- Handy testing tools to simplify assertion checks and verification
- Supports fixtures at the package, module, class, and test level.
Rednose is a “nose plugin” used to provide colors and formatting for nose test results. We do have to liven up that terminal somehow, don’t we? After all we’re not all front end developers!
Design the Test Case
Next up, let’s take a look at the function we need to test. We can review this information to determine the test fixtures, or sample records, used in our test case, and the number of tests we should implement to cover our base functionality.
Our function accepts two parameters,
patient, and returns a
booked appointment. The function raises a
ValueError if our patient is less than pristine and is missing one or more required fields.
From the code, we can ascertain that we have two fixtures:
patient. We will also need to implement two test cases. The first test case validates our positive use-case where no errors occur during the appointment booking transaction. The second use case validates that the ValueError is raised under appropriate conditions. It’s important to include test cases for each “access path” in a method or function to ensure complete test coverage.
The Test Suite
We will implement our
book_appointment test suite using a Python class. The code for our class is below:
Our preference is to utilize test case classes, rather than functions, as we can implement test fixtures as instance attributes. The alternate approach, implementing test cases as functions, requires using module variables for test fixtures and the global keyword to set values, where appropriate, which can be verbose and unwieldy in larger test suites.
Our test fixtures are defined within the
__init__() method and are initialized within the
setup() method. The
setup method is a test case fixture method which runs prior to each test case. Nose also supports a
teardown function which can be used to remove test fixtures, if necessary. For our example, the
setup method is sufficient.
The first test,
test_book_appointment, implements the positive test case where all goes well. The docstring states what is tested and the test expectations. The code within the test case is structured to clearly convey our expected result, actual result, and test assertions. Additionally, the
nose.tools.assert methods provide default error messaging when an assertion fails. The latter comes in quite handy when comparing dictionary values.
The second test
test_book_appointment_invalid_patient tests our error condition, where a ValueError is raised. The
raises decorator from nose.tools is used to assert that the ValueError exception is raised. If we did not use the
raises decorator we would have to resort to verbose assertions.
Executing test cases is a snap. Simply source your virtual environment,
cd into your directory, and execute the following command:
A well written test case provides coverage for positive and negative use cases in a manner that is readily apparent to any developer running or perusing the tests. Test cases are easier to maintain overtime if they adhere to a standard structure and utilize a single method per use case.