Testing
💯

Testing

Created
Jun 7, 2021 10:12 PM
Language
English
Summary
Research on Unit Tests.
Attention Status
Ignorable

Bibliography

FAQ

Should I use mockable methods or static methods?
If you use static methods, you’ll need to test its output whenever you use it. So more useless tests to maintain. If you keep a good set of test scenarios, you can safely rely on the method and not test it over and over again.

Testing with a Team

💡
Align the test strategy and vocabulary for each project
Before starting to test a project alongside your team, you should have in writing a standard for each kind of test required, where they will be created, how they will be called.

Unit Testing

What is the purpose of a unit test? Well, with a well-done unit test we can:
  • Understand what and when something should happen.
  • Realize when our design is bad.
  • ? Change our focus to solve a problem when a test fails.
📌
When a test fails, it should provide the same information that a high-quality bug report should.
A bug report should inform:
  • What failed. Which class? Which function or method? Which aspect of the subroutine?
  • What should have happened. What was the expected return?
  • What happened. What was the actual return? What is the difference from what was expected?

Best practices in modern Java unit testing

  • For a unit test, use block tests such as AAA or "Given, When, Then". Each block should be as small as possible. Use sub-functions to shrink these blocks.
  • Use the prefixes actual* and expected*. They make clear the role of these variables and avoid confusion about which value is which when making an assertion.
  • Avoid using random values. They can lead to tests that fail randomly, which can lead to a unit test error (difficult to debug) that only occurs when releasing to production or later when no one remembers the context of the method.
    • Use fixed values for everything. They create highly reproducible tests, easy to debug, and generate error messages that are easier to track. (To avoid rework, create helper functions, e.g. PersonHelper.getCpf(), CompanyHelper.getCnpj(), TimeHelper.getToday(), CustomerHelper.getLegacyCreationDate().)
  • Use many helper functions. Extracting details or repetition of a test scenario into private functions with a descriptive name increases the speed at which we can understand a test (example).
    • Move creation of data and complex assertions to private methods. Only receive as a parameter the values that are relevant to your test. Use default values in other fields. (Java sucks there so you have to use method override. If it were Kotlin, it would have default arguments.)
    • varargs is a good solution for this (e.g. potato(String... values)).
    • helper functions are also good for creating simple values more easily (example).
  • Do not abuse the use of variables. If we are using fixed values, when a test fails we can find where each value was used (example).
  • Do not add multiple test scenarios in a single one. A test that covers more than one scenario ends up being too large and complex. This makes debugging and changes difficult. (Example.)
    • Each test scenario must be isolated in a test method with a name that describes the expected behavior. This helps document the implementation.
  • Only make assertions of the scenario being tested. If there is already a test that guarantees expected behavior, do not make assertions in test methods of other scenarios. (Example.)
  • Tests should contain all relevant data for that scenario. Helper functions cannot hide important parameters. Do not force the test reader to enter a sub-function to understand the test (example).
  • Do not use @BeforeEach for data insertion. This forces the reader to keep jumping from method on method to understand a scenario. Prefer helper functions.
  • Prefer composition to inheritance (example).
  • Dumb tests are great; compare the output with hard-coded values.
    • Do not reuse production code. Using production code in the test can hide a bug. Instead, compare the output of the method with hard-coded values (example).
    • Do not rewrite production code. For example, in a mapper, compare the real value returned by the production method with an object with hard-coded values. If the object is too large, compare only important values.
    • Do not write too much logic. Tests should basically compare output with hard-coded data. Using conditions and iterations slows down test understanding.
  • Focus on integrated tests. Unit tests do not guarantee that an entire flow works. It may happen that a method part of the flow is refactored and causes an error, and this goes unnoticed.
  • Do not use in-memory databases in tests. Some features may be different in the real database and in memory. Instead, use Testcontainers.
  • Avoid using assertTrue() and assertFalse(). The error messages from these tests are not very enlightening. e.g. expected: <true> but was: <false>. Instead, use AssertJ's assertion methods (assertThat()).
  • Use JUnit 5's parameterized tests. With them, you can run a test with various values without writing another unit test (Example.)
  • Create test groups with @Nested. Groups can be created by test types (such as InputIsPersonCustomer) or a group for each method being tested (such as FindCustomer or CreateCustomer).
  • Give a more descriptive name to your test with @DisplayName (example).
  • Mock remote services. Some tools for this: OkHttp's WebMockServer, WireMock or Testcontainer's Mockserver.
 
 
Â