In case you missed it, read Part 1 here.
Qualities of Good Tests
Trustworthy
Put simply: a test fails when it should and the test passes when it should.
Easier said than done.
Actionable items:
- No false positives
- Example: Tests without asserts that do not validate the behavior
- Example: Tests that you have not seen failing at least once
- No false negatives
- Why: One of the purposes of unit testing is to provide possibility for safe and easy refactoring of production code
- Example: Tests that fail due to change in implementation but the expected behavior is not changed. Fragile / change-detecting tests
- Mutation testing (there are also tools for that)
- Why: Helps for the two items above
- Example: Modify the production code to introduce test-breaking changes and revert them later
- Avoid (business) logic / production code in tests
- Why: Production code carries production bugs. The test becomes meaningless
- Example: Private function copy-pasted from prod code to test code
- Example: Conditionals and loops in tests
Maintainable
Important note to architects and managers:
Usually, developers are resistant to writing tests. This comes from the fact that they are not familiar with and used to TDD. Often production code is written first and only later, if there is time – tests are added.
Mother nature designed us to value most the things that are done first, with the greatest priority. This results in tests being perceived as less valuable compared to production code.
The harsh reality is exactly the opposite most of the time. Tests are more important than production code (what a wild idea, huh?).
If maintaining tests becomes a burden, developers become even more resistant to writing them.
Actionable items:
- Avoid creating fragile tests
- Example: Validating method calls when method returns a result.
- Example: Creating really specific or rigid expectations of the results in the asserts. Checking values that we are not interested in for our scenario.
- Avoid testing private methods
- Why: Internal implementations change often.
- Rule: Test them through their calling public method instead. If a private method is so important that it needs to be tested – it is better to make it public or package-private.
- Extract and avoid duplication
- Example: Use @Before setup methods
- Enforce test isolation
- Examples: Test execution order not affecting results. Test execution amount (only a subset) not affecting results.
- Example: Test calling other tests. DON’T create such, please.
- Example: Shared state corruption – internal or external (integration). Start each test on a clean slate.
Readable
Actionable items:
- Naming
- Example: Keep the same test naming convention for all unit tests
- Test name should contain:
- What we are testing (method name, action/operation that is executed)
- The context in which we are testing/ covered scenario
- What we expect as a behavior in the result
- Why: Removing even one of these parts from a test name can cause the reader of the test to wonder what’s going on and to start reading the test code (and spending more time than needed)
Good Testable Code Design
- Write tests first (TDD)
- Why: Enforces testable design from the start. Causes smaller classes. Improves readability. Allows easier maintenance
- Hint: Go check out the Three Laws of TDD
- Use Java EE constructor injection instead of field injections
- Why: Easier mocking of dependencies, alternative to @InjectMocks
- Rule: Do NOT instantiate dependencies in constructor definition field declaration
- Avoid creating static methods, final methods and classes
- Why: This makes them hard to test. You will need PowerMock
- Rule: When you need to use PowerMock, re-think twice. Then consider why you should use it one more time. And at the end, do not use it.
-
■Hint: You can “hide” static and final methods into package-private methods and test them instead
Even More Best Practices
- Test boundary values (yeah, yeah, I know, you know that already)
- Why: It is very important. It will be YOUR part in fighting The Big Three at the beginning of the blog.
- Example: If a zip code needs to be in the range 1000-2000, create a test for 999, 1000, 2000 and 2001 as values.
- Example: If you have if condition that checks for a specific value, pass the value again with border values.
- Example: If you have array as input parameter, pass it also when it is empty, null, and/or the max allowed size.
- Ignore adding simple test scenarios like setters and getters
- Why: If there is no validation on the setters and not transformations on the getters, the test would provide very small to zero value.
- Test invalid input. Validate expected behavior on such input
- Why: This will act as documentation to other developers later.
- Do not test components only through their interaction
- Why: Implementation may change and leave out some component untested.
- Create regression tests for bugs that pop up in time
- Why: This is your defense and protection mechanism. Your safe net against yourself.
- Hint: Add tests before starting to debug the problem.
- Avoid generic matchers in the assert part (not the mock part, it is ok there). Verify that calls are with a concrete object instance
- Example: Use literal or variable instead of any() or anyInt()
- Keep the tests away from production code: Put them in src/test
- Why: Who.. who puts tests in src/main?!
- Avoid state in the test (e.g. static variables)
- Why: It will introduce side-effects and break test isolation.
- Be careful when loading data from files
- Why: Reading a file is an I/O (input/ output) process, and it is unpredictable and slow.
- Example: Be careful to close any closeable resources.
- Be careful when working with locales and dates
- Example: Use fixed dates. Do not create them like: LocalDateTime.now()
- Example: Set locale for each test and return to original one at the end.
- Write asserts as human readable as possible
- Example: assertJ is really useful for this purpose.
- Do not be too specific in test method names. Omit such details as ids / codes / names of objects that are being used
- Why: They become outdated quite fast. They also can reduce readability. Method names as a whole become outdated often too. Treat them like code comments. Update them on each and every change in the tests.
Giveaway
I would like to inspire you to strive for quality when writing code and unit tests with the following gifts:
- A little about numbers and impact:
- A little about responsibility:
- A little about professionalism:
Key takeaway:
Stay in control of what you are creating
Now What?
If you found this article valuable – please, feel free to share it.
If you want to write high quality code that is tested appropriately – please, feel free to drop us your CV.
If you want to get high quality code that is tested appropriately for your project – please, feel free to send us an inquiry.
About the author
Martin Patsov is a Java Expert at Dreamix, a custom software development company. He has rich experience in software development with Java & Spring, Angular, SQL, JUnit, communication with clients and management of small teams.
His personal motto is to “Think (before you act & about improvements), Create (your own world, your dreams & the world around you) and Share (the knowledge, feedback & motivation)”. One of his habits is to always go the extra mile. His motivation is to: “Be the person you needed when you were younger.”
vvvvvvvvvv
Think.
Create.
Share.
Your turn.
^^^^^^^^^^^
You can contact and follow Martin via: