How to mock a hidden dependency

How to mock a hidden dependency


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);
        }
    }
}
Enter fullscreen mode

Exit fullscreen mode

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);
    }
}
Enter fullscreen mode

Exit fullscreen mode

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();
    }
  }
Enter fullscreen mode

Exit fullscreen mode

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;
 }
}
Enter fullscreen mode

Exit fullscreen mode

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());
}
Enter fullscreen mode

Exit fullscreen mode



To summarize

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



Source link
lol

By stp2y

Leave a Reply

Your email address will not be published. Required fields are marked *

No widgets found. Go to Widget page and add the widget in Offcanvas Sidebar Widget Area.