Best practices, naming conventions, processes to keep in mind, development practices and tools to use for a complete tested/testable environment.
Naming conventions
• Separate implementation from test code
Benefits: avoids packaging tests together with the production binaries
• Keep packaging of test classes the same as the unit they’re testing
Benefits: Helps finding and running tests obviously
e.g.: src/main/java/com.newlinecode.swimper.SwaggerParse.java -> src/test/java/com.newlinecode.swimper.SwaggerParseTest.java
• Name test classes similarly to the units they’re testing
Benefits: Helps finding and running tests obviously
e.g.: src/main/java/com.newlinecode.swimper.SwaggerParse.java -> src/test/java/com.newlinecode.swimper.SwaggerParseTest.java
• Use descriptive names for test methods
Benefits: helps understanding the objective of tests, serves as documentation for the implementation being tested
One of the most used methods is naming them using the Given/When/Then syntax used in Behavioural Driven Development.
Processes
• Write the test before the implementation code (obvious)
Benefits: ensures that testable code is being written; ensures that every line of code gets tests written for it. By writing or modifying test first, developer is focused on requirements before starting to work on a code.
• Only write new code when test is failing
Benefits: confirms that the test does not work without the implementation
• Rerun all tests every time implementation code changes
Benefits: ensures that there is no unexpected side-effect caused by code changes. There are the so-called watchers that you turn on when starting with development and runs all tests every time an implementation is added.
• All tests should pass before a new test is written
Benefits: focus is maintained on a small unit of work; implementation code is (almost) always in working conditions.
• As the other rules above this requires refactoring to be done only after all tests are passing
Benefits: refactoring is safe
Development practices
• Write the simplest/smallest code to pass the test
Benefits: ensures cleaner and clearer design; avoids unnecessary features
• Write assertions first, act later
Benefits: tells exactly what the implementation should be about by clarifying the purpose of the requirement and test early
• Minimize assertions in each test because if it fails it might be hard to find which of the assertions failed in some environments
Benefits: avoids assertion roulette; allows execution of more asserts.
• Don’t introduce dependencies in your tests; the execution of your test shoud have no order
Benefits: tests work in any order independently whether all or only subset is run
• Use mocks
Benefits: reduced code dependency; faster tests execution.
• Use setup and tear-down methods
Benefits: allows setup and tear-down code to be executed before and after the class or each method.
• Do not use base classes. It shouldn’t be needed to go to a parent class of a parent class of a test to understand what a test does.
Benefits: test clarity.
Important and helpful tools
• Code coverage. Tools like JaCoCo, Clover and Cobertura that scan your project and check for untested methods and units.
Benefit: assurance that everything is tested.
• Continuous integration (CI). Huge must in a tested environment. At fixed intervals or at every push to the remote repository the CI application downloads your project builds it and runs all the test. If any of the tests fail, the CI notfies the team. e.g.: Jenkins
Benefits: Always know ahead if builds are broken
Bonus:
There are also tools that change conditional operators, return types, return values and more in your source code and then run all the tests. In this case, all of them are expected to fail, otherwise the test was not written correctly.
Eni Sinanaj
JIT Senior Software Engineer