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