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)