This is not meant to be a comprehensive guide to testing, but rather a high-level introduction. It is meant to help you become conversant in unit testing, not a master. That will come later!
Case Study: rekapi-timeline's computeHighlightedKeyframe
and computeDescaledPixelPosition
functions:
computeHighlightedKeyframe
:computeDescaledPixelPosition
- Sanity: Code that is tested is code that you don't have to worry about (as much).
- Stability: Testing affords a level of automated quality assurance and functional verification. It also improves maintainabilty by enabling greater confidence in making a change.
- Simplicity: Code that is testable tends to be more clear, modular, and reusable.
- Business logic: This should be your primary focus. A test suite should comprehensively verify that the core of your project solves the problem that it is intended to solve.
- Rendering logic: This gets a lower priority, because rendering tends to change frequently and is often tangential to whether the code actually works or not. Clear pass/fail characteristics of rendering logic should be tested, such as whether something on the UI shows up or not.
- Test organization
- Separate tests logically by file, component, and function: The test output should tell a sort of narrative around what the code is proven to do (see
2-test-output.txt
).
- Separate tests logically by file, component, and function: The test output should tell a sort of narrative around what the code is proven to do (see
- Test anatomy
- Setup: Perform any work that is necessary to enable a valuable assertion to be directly made.
- Assertion: A clear, isolated pass/fail expectation of the result of running a piece of code.
- Cleanup: Any work that is needed to revert the environment back to what it was before the test ran.
- Philosophy
- Isolation: A test should run the same way regardless of where in the test suite it is, and regardless of what has or has not happened before it is ran.
- Minimalism: A given test should focus on as little as possible in terms of setup and assertions. If a test isn't covering everything you want to verify, add more tests. Preferring to test smaller, isolated pieces of behavior helps ensure that this happens. You tend to end up with simpler tests by focusing on lower-level chunks of behavior (assuming your code is already decoupled and small to begin with). (Thanks to daveschinkel for help with this!)
- Things to assert:
- What should happen, does: All of your expected use cases should be accounted for.
- What shouldn't happen, doesn't: Expected negative cases, such as sane default return values and early returns.
- Error handling: Realistic failure cases are handled gracefully.
- Think not to assert:
- Unexpected parameter types: Your documentation should define an implicit contract of valid data types so your tests don't have to. It's expected that if the data types are invalid then your program will crash, so you don't have to explicitly test for that. (Thanks to @hillelogram for help with this!)
- External libraries: Assuming you are using robust external libraries, your tests should only focus on the code you are authoring.
- Unrealistic scenarios: There is infinite number of potential scenarios your code may run under, but a finite set of realistic ones. Focus on the latter.