How to mock a hidden dependency

RMAG news

Working with legacy code is difficult.

When working with legacy code, you can run into a number of challenges, like for instance : how to write a unit test for a method that contains a hidden, private dependency.
Let me show you an example of such code :

public class NotificationService {

private void sendSMSNotification(User user, Event event, boolean isUrgent) throws NotificationException {
try {

String messageContent = buildSMSMessageContent(user, event, isUrgent);
String phoneNumber = user.getPhoneNumber();

if (phoneNumber == null || phoneNumber.isEmpty()) {
throw new NotificationException(“User’s phone number is not available.”);
}

// Get SmsService bean from ApplicationContext
**SmsService smsService = ApplicationContextHolder.getBean(SmsService.class);**
boolean isSent = smsService.sendSMS(phoneNumber, messageContent);

if (!isSent) {
throw new NotificationException(“Failed to send SMS to “ + phoneNumber);
}

// Optionally log the SMS sending for auditing purposes
logSMSSending(user, phoneNumber, messageContent, isUrgent);
} catch (Exception e) {
throw new NotificationException(“Error occurred while sending SMS notification.”, e);
}
}
}

Here the hidden dependency is the SmsService. As you can see, it is instantiated with the Spring ApplicationContext.

This is a common pattern we can “encounter” when working with a legacy code. The idea behind this ApplicationContextHolderis that it serves as a “utility” class that has a reference to the Spring applicationContext and instead of injecting the bean, or the service with @Autowired we are directly injecting by calling the static method ApplicationContext.getBean .

This is problematic because SmsService is hidden, private and is making a real Api call to the the SmsProvider.

In my test, I want to have the possibility to mock the SmsService.

So, how to achieve that ?

Extract and override getter

There is a technique that Michael Feathers describes in his book Working effectively with Legacy Code to overcome this problem. It’s called Extract and Override getter .

To expose the SmsService, define a getter, getSmService and use that getter in all places where the SmService is used in the class. This getSmsService visibility is protected.

public class NotificationService {

private void sendSMSNotification(User user, Event event, boolean isUrgent) throws NotificationException {
try {

//same as before

SmsService smsService = getSmsService();
boolean isSent = smsService.sendSMS(phoneNumber, messageContent);

if (!isSent) {
throw new NotificationException(“Failed to send SMS to “ + phoneNumber);
}

// same as before
}

protected SmsService getSmsService(){
return ApplicationContextHolder.getBean(SmsService.class);
}
}

2nd step, create a TestNotificationService that will override the getSmsService and return a FakeSmsService.

class TestNotificationService extends NotificationService {

@Override
public SmsService getSmsService(){
return new FakeSmsService();
}
}

For the sake of simplicity, let’s imagine that SmsService is an interface, otherwise you would need to extract an interface from the SmsService that will contain sendSms as a method.

The FakeSmsService will return false for the sendSms method.

class FakeSmsService implements SmsService {

@Override
public boolean sendSms(phoneNumber, messageContent){
return false;
}
}

And then write your test.

@Test
void test_raise_an_exception_when_sms_is_not_sent(){
NotificationService notificationService = new TestNotificationService();
Exception exception = assertThrows(NotificationException.class, () -> {
notificationService.sendSMSNotification(user, event, false);
}

Assertions.assertEquals(“Failed to send SMS to 0606060606”, exception.getMessage());
}

To summarize

Create a getter to expose with protected visibility
Define a test class that extends the main and overrides the getter previously defined.
Use it your test.