Testing Legacy Code with new() Calls Using Mockito

Nov 22, 2025 · Programming · 24 views · 7.8

Keywords: Mockito | Unit Testing | Java Testing | Spy Objects | Constructor Mocking

Abstract: This article provides an in-depth exploration of testing legacy Java code containing new() operator calls using the Mockito framework. It analyzes three main solutions: partial mocking with spy objects, constructor mocking via PowerMock, and code refactoring with factory patterns. Through comprehensive code examples and technical analysis, the article demonstrates the applicability, advantages, and implementation details of each approach, helping developers effectively unit test legacy code without modifications.

Challenges in Legacy Code Testing

In unit testing practice, developers frequently encounter legacy code containing hard-coded new() operator calls. Such code is typically difficult to test because the instantiation process is tightly coupled with business logic, preventing replacement with mock objects through conventional dependency injection. This article examines this challenge through a typical login scenario and explores solutions using the Mockito framework.

Problem Scenario Analysis

Consider the following legacy code example:

public class TestedClass {
  public LoginContext login(String user, String password) {
    LoginContext lc = new LoginContext("login", callbackHandler);
    return lc;
  }
}

This code directly instantiates a LoginContext object, which typically requires complex JAAS security configuration for proper initialization. In unit testing environments, such external dependencies are often unavailable or difficult to configure.

Solution 1: Partial Mocking with Spy Objects

Mockito's spy functionality allows creating wrappers around real objects, enabling mocking of specific methods while preserving the real behavior of other methods. This approach is particularly suitable for legacy code that cannot be modified.

Implementation Principle

Spy objects implement the delegation pattern, where all method calls not explicitly mocked are forwarded to the real object. When specific methods need mocking, they can be configured using when().thenReturn() or doReturn().when().

Concrete Implementation

@Test
public void testLoginWithSpy() {
  // Create spy instance of the class under test
  TestedClass tc = spy(new TestedClass());
  
  // Create mock object for LoginContext
  LoginContext lcMock = mock(LoginContext.class);
  
  // Configure spy to return mock object when login method is called
  when(tc.login(anyString(), anyString())).thenReturn(lcMock);
  
  // Execute test
  LoginContext result = tc.login("testUser", "password");
  
  // Verify result
  assertSame(lcMock, result);
}

Important Considerations

When using spies, pay special attention to:

Solution 2: Constructor Mocking with Mockito Inline

Modern Mockito versions (3.4.0+) provide inline mocking capabilities that can directly mock constructor calls without additional dependencies.

Implementation Approach

@Test
public void testLoginWithMockedConstruction() {
  try (MockedConstruction<LoginContext> mockedConstruction = 
       Mockito.mockConstruction(LoginContext.class)) {
    
    TestedClass tc = new TestedClass();
    LoginContext result = tc.login("something", "something else");
    
    // Verify constructor was called
    assertEquals(1, mockedConstruction.constructed().size());
    
    // Can retrieve mock instance for further verification
    LoginContext mockInstance = mockedConstruction.constructed().get(0);
    assertNotNull(mockInstance);
  }
}

Dependency Configuration

Using this feature requires adding the mockito-inline dependency:

<dependency>
  <groupId>org.mockito</groupId>
  <artifactId>mockito-inline</artifactId>
  <version>5.2.0</version>
  <scope>test</scope>
</dependency>

Solution 3: Code Refactoring with Factory Pattern

Although not a direct Mockito solution, this approach represents a long-term best practice worth considering. By refactoring code to separate object creation logic, testability can be significantly improved.

Refactored Code Structure

public interface LoginContextFactory {
  LoginContext createLoginContext();
}

public class DefaultLoginContextFactory implements LoginContextFactory {
  @Override
  public LoginContext createLoginContext() {
    return new LoginContext("login", callbackHandler);
  }
}

public class TestedClass {
  private final LoginContextFactory loginContextFactory;
  
  public TestedClass(LoginContextFactory loginContextFactory) {
    this.loginContextFactory = loginContextFactory;
  }
  
  public LoginContext login(String user, String password) {
    return loginContextFactory.createLoginContext();
  }
}

Corresponding Test Code

@Test
public void testLoginWithFactory() {
  LoginContextFactory factoryMock = mock(LoginContextFactory.class);
  LoginContext lcMock = mock(LoginContext.class);
  
  when(factoryMock.createLoginContext()).thenReturn(lcMock);
  
  TestedClass tc = new TestedClass(factoryMock);
  LoginContext result = tc.login("user", "password");
  
  assertEquals(lcMock, result);
  verify(factoryMock).createLoginContext();
}

Solution Comparison and Selection Guidelines

Scenario Analysis

<table border="1"> <tr><th>Solution</th><th>Applicable Scenarios</th><th>Advantages</th><th>Disadvantages</th></tr> <tr><td>Spy Objects</td><td>Unmodifiable legacy code</td><td>No source code modification required</td><td>Higher test complexity</td></tr> <tr><td>Constructor Mocking</td><td>Modern Mockito environments</td><td>Powerful functionality, no extra configuration</td><td>Requires newer Mockito versions</td></tr> <tr><td>Factory Pattern Refactoring</td><td>Modifiable code</td><td>Clear code structure, easy testing</td><td>Requires source code modification</td></tr>

Performance Considerations

Both spy objects and constructor mocking introduce some runtime overhead. In performance-sensitive scenarios, the factory pattern is usually the better choice as it avoids runtime bytecode manipulation.

Best Practices Recommendations

Test Design Principles

Regardless of the chosen solution, follow these test design principles:

Error Handling Testing

A complete test suite should also include tests for exception scenarios:

@Test
public void testLoginWithException() {
  TestedClass tc = spy(new TestedClass());
  
  // Simulate exception during login process
  when(tc.login(anyString(), anyString()))
    .thenThrow(new RuntimeException("Login failed"));
  
  assertThrows(RuntimeException.class, () -> {
    tc.login("user", "password");
  });
}

Conclusion

Testing legacy code containing new() calls is a common challenge in unit testing. Mockito provides multiple solutions to address this challenge, ranging from simple spy objects to powerful constructor mocking capabilities. Choosing the appropriate method requires careful consideration of code modifiability, testing environment constraints, and long-term maintenance requirements. By properly applying these techniques, developers can effectively test various types of legacy code without compromising test quality.

Copyright Notice: All rights in this article are reserved by the operators of DevGex. Reasonable sharing and citation are welcome; any reproduction, excerpting, or re-publication without prior permission is prohibited.