How to mock a hidden dependency
Tags: refactoring, java, cleancoding
Published: 2024-07-03 21:59:26

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 :

```java
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. 

```java
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`. 

```java
 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.

```java
class FakeSmsService implements SmsService {
 
 @Override
 public boolean sendSms(phoneNumber, messageContent){
   return false;
 }
}
```

And then write your test. 

```java
@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.