Testable code best practices - msssk/intern GitHub Wiki
Many of the best practices for writing testable code conform to general code best practices. Code that is easily testable often also tends to be highly maintainable and resilient against changing business requirements.
Separation of concerns
Code for handling discrete areas of functionality, such as data retrieval, data processing, data display, and event handling should be separated into individual modules. Besides aiding in code maintenance, this simplifies swapping out individual components with mocks during testing.
API naming conventions
When writing unit tests, it is important to be able to determine which properties and methods of an object are public and which are private implementation details for the object itself. This is because unit tests should only test the publicly defined APIs, as these are the only APIs that are guaranteed to exist and produce stable results. As long as the implementation of a public method generates the same results, it should not matter to unit tests how that result is produced. An added bonus of not testing private APIs is that, in conjunction with code coverage analysis, dead code belonging to implementations that have been subsequently factored out of use can be easily identified and removed. In JavaScript, private/protected properties and methods are conventionally annotated using an underscore prefix.
Loose coupling / dependency injection
Tight coupling between modules increases testing difficulty by requiring the explicit dependencies of a module to be redefined using the loader’s module mapping configuration. It also makes it difficult to modify the behavior of components at runtime by making it impossible for alternative implementations of external dependencies to be provided to individual instances of a component. Pub/sub (provided by dojo/topic
) is a good approach for loosely-coupled communication between modules. Using pub/sub allows you to generate a global notification that can be handled by external modules without creating tight coupling - the publishers and subscribers have no direct dependency on eachother.
Elimination of globals
One of the primary goals of the AMD module system is to provide a mechanism that eliminates global variables in place of explicitly requested dependencies. This explicit dependency requirement becomes especially relevant when performing testing, for two reasons:
- Use of global variables encourages state to be shared across different components and tests. The order in which test suites are executed should not impact the correct functionality of any other test, but reusing objects that maintain any sort of state will cause brittle cross-dependencies between tests that should not exist.
- Components tested in isolation will not be able to rely on global variables that have been manually defined in the index.html entrypoint file, since when components are tested in isolation, they are no longer loaded along with the entrypoint file.
The 1.x series of Dojo releases maintain the dojo
, dijit
, and dojox
globals for backwards compatibility, but AMD code should use explicit references to individual modules rather than these globals. Currently, the AMD loader used by Intern has to be swapped in order to include the legacy loader code responsible for exposing these globals, which adds
complexity to the testing system and is otherwise unnecessary. The legacy loader also requires dependencies to be loaded serially, which slows page loads by orders of magnitude.
Clear code and documentation
Tests are not a substitute for a clear, well-maintained codebase. In fact, in order to write accurate tests, it is necessary that code is kept clean enough that the test author can quickly understand the purpose of each unit of code being tested and how it fits into the overall application.
One of the most effective ways to ensure this level of visibility for test authors is to ensure that general best practices for code authorship are followed. At a minimum:
- Public classes, properties, methods, and parameters should be documented using a standard code documentation format like jsdoc or dojodoc
- Variables and parameters should be clear and understandable, with no abbreviations, truncations, or non-standard terminology that will cause the meaning of the code to be lost or distorted
- Code comments should be provided for potentially confusing or odd-looking code, describing why (not what) the code is doing what it is doing
As touched upon in the API naming conventions section, using idioms that are common to the language and libraries being used will ensure that test authors are able to complete their work quickly and without confusion.