Typically, validating the test's behavior is considered rather straightforward. Unfortunately, however, lack of attention here often can lead to false positives/negatives or non-actionable bugs. When approaching each test case, make the initial assumption that it currently fails, and then prove through testing that it works or doesn't work correctly. To do so, you will want to verify (and then log) a good set of detailed conditions.
For starters, whenever you encounter a failure condition, be sure to log out everything that you require to investigate the failure and file a meaningful bug. For example, the following sample provides much more information than just logging out a basic "test failed" string:
LogFailure("Unexpected result. Received %lu, expected %lu", GetLastError(), result);
A good logging methodology will let you provide a rich collection of useful state information. Consider adding some of the following items to your log output:
-
Write out the file name and line number of the test's source code where the error occurred. This allows users to jump easily to the exact location in the test code where an error was first identified. The compiler's __FILE__ and __LINE__ macros make this very easy.
-
Dump out any resource-utilization metrics that make sense for your tests. Memory usage is a usual suspect, but CPU utilization, network bandwidth, and disk-drive IO might be useful in the context of your tests.
-
Use the logging infrastructure to record performance timing for key scenarios, which can later be analyzed over a period of time to track trends.
Most tests follow a typical "setup, test, cleanup" pattern. Every step in the setup phase should be validated with as much enthusiasm as the actual test itself. If a precondition fails, you should record the error (typically, as a "this test is blocked" message) and exit from the test case. Failing to pay attention to initial failures can lead to false negatives. For example, if you expect your API under test to fail, it might be failing for the wrong reason if a precondition is not set up correctly.
You should run your tests periodically in an environment/configuration in which you expect them to fail. This is a good way to identify tests that provide false positives. For example, if a test is attempting to access a know resource (file path, registry setting, memory location, and so forth), run the test in an environment in which the resource does not exist (or the test does not have permission to access it). If a test case "passes" in this type of setup, it might be making false assumptions and hiding valid bugs.
Testing for assertions and crashes in the API are something that should be done periodically. However, these test cases should be coded up and configured so that they do not run by default; you don't want to halt or cause downstream errors when you run a series of automated tests.
If an API has out parameters, set the initial variable's value to a hard-coded but nonstandard pattern. This will enable you to check easily whether or not the API modified the out parameter. Obviously, it will depend on the specified behavior; however, for every test case, you should check to see if the value was altered, and then verify if it was set to the correct value.
Be sure to check also for over- and underflows of buffers and memory locations that are modified by the API under test. You can create a larger buffer than the API will be modifying, and place known values before and after the expected modified region. Then, verify that these values are not altered. Alternately, you can use read-only memory pages to trap edits that extend beyond the expected range.
Read-only memory also is good for ensuring that APIs do not alter memory that they aren't supposed to alter. An input buffer that is marked as "const" in the C/C++ language is not guaranteed to preserve the memory; it is a compiler-time check, not a run-time enforcement. By marking memory as read-only before passing it into an API, you can quickly discover conditions in which the code is misbehaving.