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 ApplicationContextHolder
is 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.
Top comments (0)