One very common source for errors is improper handling of boundaries. The idea in boundary value analysis is to test boundaries on the edges, for example MIN, MIN-1, MAX, MAX+1. These values should be exercised both for input and output of methods (when applicable).
It is impossible to create all the possible input data for the methods, which may happen when the system is used in actual reality. However, the similar data passed for the method does not usually change the execution path, and thus data can be abstracted. Equivalence class partitioning is a behavioral test design method that divides all the similar input values into classes. For example, if the method accepts values between -5 and 15, there are three classes, and thus only three test cases are needed:
Valid: [-5 – 15]
Invalid [-n – -6]
Invalid [16 – n]
Special values are a good source of errors and require extra attention from the implementation. Some examples include
Zero and one in arithmetic operations and function usages
90 degree and multiples
Empty strings
NULL values
Experience and intuition can guide developers to predict possible sources of errors. Because programmers often make similar errors, creating test cases through error guessing can be practical and effective. This is a good practice especially when software and development teams evolve, and test suites are run continuously. Such tests can locate bugs that crawl into software during changes.
Error-guessing-related examples include:
Improper or missing error handling, for example writing RFile.Open() instead
of User::LeaveIfError(RFile.Open()).
Improper exception handling, for example, trapping an exception and ignoring the problem (when the exception happens in production, it will crash the software).
Leaking code (heap or other resources). Proper use of the cleanup stack is the solution for automatic variables and constructors and destructors for class variables. When deleting objects behind pointers, those pointers shall always be set to NULL before doing anything else (which may cause a leave) unless deletion happens in the destructor.
Improper transaction semantics. For example, data is written to the file in pieces in nested method calls rather than gathering all the needed data first and then writing it in one atomic operation. The first approach easily corrupts the file if the exception happens in the middle of execution.
Improper CleanUpStack and CActive usage.
Invalid event handling, for example, what happens if events come in a different order? A common approach is to design a state machine, which defines all the states and allowed transitions from state to state. Then implementation should simply implement the machine and nothing else.
Object life-cycle changes from a common scenario, for example, a referred object gets deleted when the referring object does not expect it (call backs together with active objects cause this kind of situations if not considered and documented properly).
Multithreading, parallel execution, and dead locks.