Refactoring Testware after Extracting Single Responsibility Classes
Background
Legacy service class that breaks Java’s “Single Responsibility Principle”
No/few helper classes
All code in service class
Complicated test-ware
No mocking in test-ware
Public method tests need to cover many branches
Developer decides to refactoring production code and extract out a helper class with a single responsibility (towards best practices) 👌
Impacts on Testware of Refactoring
Common incorrect approach
The developer often tries to update existing tests for the service class using different approaches
Include (new) helper class in service class test setup (sometimes OK)
Mock the new helper class in test for the service class (correct)
Mock the behavior of the new helper class for different scenarios in the service class test (incorrect)
Recommended Approach
Create new test class for the extracted helper class
Ensure all scenarios (code paths) in the helper class are covered in the new test class
(it is important to to this first o you know very well which scenarios are covered there)Remove all scenarios from the service test class that are now covered in the helper test class
Ensure 1 scenario is left that uses the helper class and
Introduce a mock for the helper class and define it to return a fixed response e.g. ‘output from helper class’
ORInsert a real instance of the helper class (sometime this is easier to set-up). But do not rely on this test to cover multiple scenarios in the helper class!
Delegation Test
Often when the service class does very little but forwarding a request to one helper class. This is called ‘delegation’ (see also delegation pattern). In such cases we can use a simple delegation test pattern. Two variations exist depending on whether the method in the help class is void or returns something
Delegation Tests with Return Objects (pseudo code)
given: Define & Insert mock in the service test class
given: Define mock to return a known object for the expected method with expected parameters (exact matching if possible)
use a clearly defined test-related name for the Object/String e.g def resultFromHelper= … or ‘result from helper’
when: Call the service method
then: Assert the result from the service method is or contains resultFromHelper
Delegation Tests for Void Methods (pseudo code)
given: Define & Insert mock in the service test class
when: Call the service method
then: Assert the correct help class method is called with the correct parameters (exact matching where possible)
Common Mistake: Mock Return Behavior and Later Assert Invocation
Often developers both define the output of mocked method at the start of the test (given) and then check if it is called in a then/and block. This does NOT work since the check for a method being called also function as defines its behavior, typically (>> is omitted) it means the mocked method will return nothing i.e. null!
It is possible to check a method is called (1 *) and define its output (>> ‘output from helper’ ) but that should not be needed as the test depends on the mocked behavior it implicitly means it has been called if you get the expected result. Also this would (have to be) defined at the end of the test and would lead to an unnatural flow for reading the test:
Bad (won't work) | Working but not Recommended |
|---|---|
|
|
Note: Line 5 ‘overrides’ line 2. This is why it does not work | Note: no need to check calls as the test depends on the mocked behavior |
The Use of the Word ‘some’
Code Snippet | Explanation |
|---|---|
Since the DataValidationException constructor requires 2 parameters we need to add strings. Blank string would not be very clear. | |
Now this test has been extended to verify the error details are included in the HTTP response body. And now the word ‘my’ is used in the relevant strings and to indicate that the test does care about these values. |
Concrete Examples (Gerrit Views)
User Story that triggered refactoring: https://lf-onap.atlassian.net/browse/CPS-1872
| Gerrit View | Notes | |
|---|---|---|---|
| 1 | Service Class | Extract private method toRestOutputCmHandle into a new class | |
| 2 | Service Test Class | Affected Test
| |
| 3 | (extracted) Helper Class |
| |
| 4 | Helper Test Class |
|
Sample Test Class Update August 2025
During the knowledge sharing session for which this page was create a review of the test class show still a few bad practices. The following commit attempts to fix all of these but is not guaranteed to be 100% correct of course and certain things will always remain a matter of opinion or taste.
Knowledge Sharing: Groovy/Spock best practices refactoring | https://gerrit.onap.org/r/c/cps/+/141835