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