Search code examples
javaspringmicroservicesspring-cloud-feignopenfeign

Mocking an OpenFeign client for Unit Testing in a spring library and NOT for a spring boot application


I've implemented a feign client that calls a get API based on this official repository. I have a rule class UserValidationRule that needs to call that get API call getUser() and validate some stuff. That works as expected but when I get to testing that rule class, mocking the feign client is not successful and it continues to call the actual API. I've simplified the situation so please ignore the simplicity lol. This is a follow up question I have after i found this stackoverflow question

The API returns this model:

@Data
public class userModel {
    private long id;
    private String name;
    private int age;

}

The interface with the rest client method:

public interface UserServiceClient {

    @RequestLine("GET /users/{id}")
    UserModel getUser(@Param("id") int id);
}

And in the rule class, i build the feign client and call the API:

@RequiredArgsConstructor
@Component
public class UserValidationRule {

    private static final String API_PATH = "http://localhost:8080";

    private UserServiceClient userServiceClient;


    public void validate(String userId, ...) {
            // some validations 
            validateUser(userId);

    }

    private void validateUser(String userId) {
            userServiceClient = getServiceClient();
            UserModel userModel = userServiceClient.gerUser(userId);
            
            // validate the user logic
        }
    }

    private UserServiceClient getServiceClient() {
           return Feign.builder()
                  .encoder(new GsonEncoder())
                  .decoder(new GsonDecoder())
                  .target(UserServiceClient.class, API_PATH);
    }
}

And here comes the test class:

public class UserValidationRuleTest {

    private UserServiceClient userServiceClient = mock(UserServiceClient.class);
    private UserValidationRule validationRule = new UserValidationRule();
    private UserModel userModel;

    @Before
    public void init() {
        userModel = generateUserModel();
    }


    @Test
    public void validateWhenAgeIsNotBlank() {

        doReturn(userModel).when(userServiceClient).getUser(any());
        validationRule.validate("123", ...);
        // some logic ...
        assertEquals(.....);
        verify(userServiceClient).getUser(any());
    }



    private UserModel generateUserModel() {
        UserModel userModel = new UserModel();
        userModel.setName("Cody");
        userModel.setAge("22");
        return accountModel;
    }
}

As I debug validateWhenAgeIsNotBlank(), i see that the userModel is not the one that's generated in the test class and the values are all null. If I pass in an actual userId, i get an actual UserModel that I have in my db.

I think the problem is that UserServiceClient is not being mocked. The verify is failing as it says the getUser() is not invoked. It might be something to do with how the feign client is declared in the UserValidationRule with the feign.builder()... Please correct me if I'm wrong and tell me what I'm missing or any suggestions on how to mock it correctly.


Solution

  • You are not using the spring managed UserServiceClient bean. Every time you call UserValidationRule.validate it calls validateUser which in turn calls the getServiceClient method. This getServiceClient creates a new instance of UserServiceClient for each invocation. This means when testing the mocked UserServiceClient is not in use at all.

    I would restructure the code as below;

    First either declare UserServiceClient as final with @RequiredArgsConstructor or replace @RequiredArgsConstructor with @AllArgsConstructor. The purpose of this change is to allow an instance of UserServiceClient be injected rather than creating internally in the service method.

    @Component
    @RequiredArgsConstructor
    public class UserValidationRule {
    
        private final UserServiceClient userServiceClient;
    
    .... // service methods
    
    }
    

    Then have a separate Configuration class that builds the feign client as spring bean;

    @Bean
    private UserServiceClient userServiceClient() {
           return Feign.builder()
                  .encoder(new GsonEncoder())
                  .decoder(new GsonDecoder())
                  .target(UserServiceClient.class, API_PATH);
    }
    

    At runtime this bean will now be injected into the UserValidationRule.

    As for the unit test changes you are creating the mock correctly but aren't setting/injecting that mock anywhere. You either need to use @Mock and @InjectMocks annotations or manually create instance of UserValidationRule in your @Before method.

    Here is how @Mock and @InjectMocks use should look like;

    @RunWith(MockitoJUnitRunner.class)
    public class UserValidationRuleTest {
    
        @Mock private UserServiceClient userServiceClient;
        @InjectMocks private UserValidationRule validationRule;
        ... rest of the code
    

    or continue using mock(...) method and manually create UserValidationRule.

    public class UserValidationRuleTest {
    
        private UserServiceClient userServiceClient = mock(UserServiceClient.class);
        private UserValidationRule validationRule;
        private UserModel userModel;
    
        @Before
        public void init() {
            validationRule = new UserValidationRule(userServiceClient);
            userModel = generateUserModel();
        }
        ... rest of the code
    

    This will now ensure you are using single instance of spring managed feign client bean at runtime and mocked instance for the testing.