Professional C__ - Marc Gregoire [445]
Once you have generated ideas for some of the tests you would like to use, consider how you might organize them into categories, and the breakdown of tests will fall into place. In the database class example, the tests could be split into the following categories:
Basic tests
Error tests
Localization tests
Bad input tests
Complicated tests
Splitting your tests into categories makes them easier to identify and augment. It might also make it easier to realize which aspects of the code are well tested and which could use a few more unit tests.
It’s easy to write a massive number of simple tests, but don’t forget about the more complicated cases!
Create Sample Data and Results
The most common trap to fall into when writing unit tests is to match the test to the behavior of the code instead of using the test to validate the code. If you write a unit test that performs a database select for a piece of data that is definitely in the database, and the test fails, is it a problem with the code or a problem with the test? It’s often easier to assume that the code is right and to modify the test to match. This approach is usually wrong.
To avoid this pitfall, you should understand the inputs to the test and the expected output before you try it out. This is sometimes easier said than done. For example, say you wrote some code to encrypt an arbitrary block of text using a particular key. A reasonable unit test would take a fixed string of text and pass it in to the encryption module. Then, it would examine the result to see if it was correctly encrypted.
When you go to write such a test, it is tempting to try out the behavior with the encryption module first and see the result. If it looks reasonable, you might write a test to look for that value. Doing so really doesn’t prove anything, however. You haven’t actually tested the code — you’ve just written a test that guarantees it will continue to return that same value. Often times, writing the test requires some real work — you would need to encrypt the text independently of your encryption module to get an accurate result.
Decide on the correct output for your test before you ever run the test.
Write the Tests
The exact code behind a test will vary depending on what type of test framework you have in place. One framework, cppunit, is discussed later in this chapter. Independent of the actual implementation, however, the following guidelines will help ensure effective tests:
Make sure that you’re testing only one thing in each test. That way, if a test fails, it will point to a specific piece of functionality.
Be specific inside the test. Did the test fail because an exception was thrown or because the wrong value was returned?
Use logging extensively inside of test code. If the test fails some day, you will have some insight into what happened.
Avoid tests that depend on earlier tests or are otherwise interrelated. Tests should be as atomic and isolated as possible.
If the test requires the use of other subsystems, consider writing stub versions of those subsystems that simulate the modules’ behavior so that changes in loosely related code don’t cause the test to fail.
Ask your code reviewers to look at your unit tests as well. When you do a code review, tell the other engineer where you think additional tests could be added.
As you will see later in this chapter, unit tests are usually very small and simple pieces of code. In most cases, writing a single unit test will take only a few minutes, making them one of the most productive uses of your time.
Run the Tests
When you’re done writing a test, you should run it right away before the anticipation of the results becomes too much to bear. The joy of a screen full of passing