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 c…


This content originally appeared on DEV Community and was authored by Clarence Dimitri CHARLES

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

  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.


This content originally appeared on DEV Community and was authored by Clarence Dimitri CHARLES


Print Share Comment Cite Upload Translate Updates
APA

Clarence Dimitri CHARLES | Sciencx (2024-07-03T21:59:26+00:00) How to mock a hidden dependency. Retrieved from https://www.scien.cx/2024/07/03/how-to-mock-a-hidden-dependency/

MLA
" » How to mock a hidden dependency." Clarence Dimitri CHARLES | Sciencx - Wednesday July 3, 2024, https://www.scien.cx/2024/07/03/how-to-mock-a-hidden-dependency/
HARVARD
Clarence Dimitri CHARLES | Sciencx Wednesday July 3, 2024 » How to mock a hidden dependency., viewed ,<https://www.scien.cx/2024/07/03/how-to-mock-a-hidden-dependency/>
VANCOUVER
Clarence Dimitri CHARLES | Sciencx - » How to mock a hidden dependency. [Internet]. [Accessed ]. Available from: https://www.scien.cx/2024/07/03/how-to-mock-a-hidden-dependency/
CHICAGO
" » How to mock a hidden dependency." Clarence Dimitri CHARLES | Sciencx - Accessed . https://www.scien.cx/2024/07/03/how-to-mock-a-hidden-dependency/
IEEE
" » How to mock a hidden dependency." Clarence Dimitri CHARLES | Sciencx [Online]. Available: https://www.scien.cx/2024/07/03/how-to-mock-a-hidden-dependency/. [Accessed: ]
rf:citation
» How to mock a hidden dependency | Clarence Dimitri CHARLES | Sciencx | https://www.scien.cx/2024/07/03/how-to-mock-a-hidden-dependency/ |

Please log in to upload a file.




There are no updates yet.
Click the Upload button above to add an update.

You must be logged in to translate posts. Please log in or register.