Refactoring Testware after Extracting Single Responsibility Classes

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) 👌

20250811_095946.jpg

 

Impacts on Testware of Refactoring

Common incorrect approach

The developer often tries to update existing tests for the service class using different approaches

  1. Include (new) helper class in service class test setup (sometimes OK)

  2. Mock the new helper class in test for the service class (correct)

    1. Mock the behavior of the new helper class for different scenarios in the service class test (incorrect)

Recommended Approach

  1. Create new test class for the extracted helper class

  2. 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)

  3. Remove all scenarios from the service test class that are now covered in the helper test class

  4. Ensure 1 scenario is left that uses the helper class and

    1. Introduce a mock for the helper class and define it to return a fixed response e.g. ‘output from helper class’
      OR

    2. Insert 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)

  1. given: Define & Insert mock in the service test class

  2. given: Define mock to return a known object for the expected method with expected parameters (exact matching if possible)

    1. use a clearly defined test-related name for the Object/String e.g def resultFromHelper= … or ‘result from helper’

  3. when: Call the service method

  4. then: Assert the result from the service method is or contains resultFromHelper

Delegation Tests for Void Methods (pseudo code)

  1. given: Define & Insert mock in the service test class

  2. when: Call the service method

  3. 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

Bad (won't work)

Working but not Recommended

  1. given: def mockHelper

  2. and: mockHelper.doSomethingElse() >> ‘output from helper class’

  3. when: result = serviceClass.doSomething()

  4. then: assert result == ‘output from helper class’

  5. and: 1 * mockHelper.doSomethingElse()

  1. given: def mockHelper

  2. when: result = serviceClass.doSomething()

  3. then: assert result == ‘output from helper class’

  4. and: 1 * mockHelper.doSomethingElse() >> ‘output from helper class’ 😕

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

Code Snippet

Explanation

Screenshot 2025-08-14 141103.png

Since the DataValidationException constructor requires 2 parameters we need to add strings. Blank string would not be very clear.
Since the test never uses/refers/asserts these values we can use the word ‘some' with another keyword to explain what these are. ‘some’ indicates that the test does not care about these values otherwise.

image-20250814-135800.png

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

 

Gerrit View

Notes

1

Service Class

NetworkCmProxyController.java

Extract private method toRestOutputCmHandle into a new class
(this was also triggered by the need to re-use this method in another controller: NetworkCmProxyInventoryController.java

2

Service Test Class

NetworkCmProxyControllerSpec.groovy

Affected Test

  1. 'Execute cm handle search.' (R#269)

  2. 'Get complete Cm Handle details by Cm Handle Reference.' (R#292)

  3. 'Call execute cm handle searches with unrecognized condition name.' (R#339)

3

(extracted) Helper Class

RestOutputCmHandleMapper.java

 

4

Helper Test Class

RestOutputCmHandleMapperSpec.groovy

 

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.