I'm writing unit tests for a Controller and mocking all calls to it's service. I'm using Spring with a JPA repository.
@RestController
@CrossOrigin
@RequestMapping("/somerequest")
public class MyController {
@Autowired
UserService userService;
....
@RequestMapping("/filter")
public List<UserDTO> getUsersByFilter(@RequestParam(value = "search") String search) {
UserSpecificationsBuilder builder = new UserSpecificationsBuilder();
Pattern pattern = Pattern.compile("(\\w+?)(:|<|>)(\\w+?),", Pattern.UNICODE_CHARACTER_CLASS);
Matcher matcher = pattern.matcher(search + ",");
while (matcher.find()) {
builder.with(matcher.group(1), SearchOperation.getSimpleOperation(matcher.group(2)), matcher.group(3));
}
Specification<User> spec = builder.build();
List<User> result = userService.getUserByFilter(spec);
List<UserDTO> userDtoList = new ArrayList<UserDTO>();
if (!result.isEmpty()) {
for (User a : result) {
userDtoList.add(convertToDTO(a));
}
}
return userDtoList;
}
....
}
How can I (Mockito)verify with which parameters is the method getUserByFilter being called? I used ArgumentCaptor to catch the Specification object that's being given to the method, but I don't know how to get the search criteria from it. Thanks a lot for your help
public class UserSpecificationsBuilder {
private final List<SearchCriteria> params;
public UserSpecificationsBuilder() {
params = new ArrayList<SearchCriteria>();
}
public UserSpecificationsBuilder with(String key, SearchOperation operation, Object value) {
params.add(new SearchCriteria(key, operation, value));
return this;
}
public Specification<User> build() {
if (params.size() == 0) {
return null;
}
List<Specification> specs = params.stream().map(UserSpecification::new).collect(Collectors.toList());
Specification<User> result = specs.get(0);
for (int i = 1; i < params.size(); i++) {
result = Specification.where(result).and(specs.get(i));
}
return result;
}
}
public class UserSpecification implements Specification<User> {
/**
*
*/
private static final long serialVersionUID = 1L;
private SearchCriteria criteria;
public UserSpecification(SearchCriteria criteria) {
this.criteria = criteria;
}
@Override
public Predicate toPredicate(Root<User> root, CriteriaQuery<?> query, CriteriaBuilder builder) {
switch (criteria.getOperation()) {
case LIKE:
return builder.like(root.get(criteria.getKey()), "%" + criteria.getValue().toString() + "%");
case GREATER_THAN:
return builder.greaterThan(root.get(criteria.getKey()), criteria.getValue().toString());
case LESS_THAN:
return builder.lessThan(root.get(criteria.getKey()), criteria.getValue().toString());
default:
return null;
}
}
@Override
public boolean equals(Object obj) {
UserSpecification source = (UserSpecification) obj;
if (source.criteria != null) {
return this.criteria.equals(source.criteria);
}
return false;
}
}
public class SearchCriteria {
private String key;
private SearchOperation operation;
private Object value;
public SearchCriteria(final String key, final SearchOperation operation, final Object value) {
super();
this.key = key;
this.operation = operation;
this.value = value;
}
@Override
public boolean equals(Object obj) {
SearchCriteria source = (SearchCriteria) obj;
return this.key.equals(source.getKey()) && this.value.toString().equals(source.getValue().toString())
&& this.operation.equals(source.getOperation());
}
// getters and setters
}
My Test:
@ContextConfiguration(classes = Application.class)
@WebMvcTest(UserController.class)
@DisplayName("UserController - Test")
class UserControllerTest {
@Autowired
private MockMvc mockMvc;
@MockBean
private UserService userService;
...
@Test
void testGetUsererByFilter() throws Exception {
ArgumentCaptor<Specification> argsCaptor = ArgumentCaptor.forClass(Specification.class);
String filterURL = "/filter?search=personalnummer:6517,ueberstd>5";
given(this.userService.getUserByFilter(any(Specification.class)))
.willReturn(new ArrayList<User>());
mockMvc.perform(get(requestURL + filterURL)).andExpect(status().isOk())
.andExpect(handler().handlerType(UserController.class))
.andExpect(handler().methodName("getUserByFilter"));
UserSpecificationsBuilder builder = new UserSpecificationsBuilder();
UserSpecification spec1 = new UserSpecification(new SearchCriteria("personalnummer", SearchOperation.LIKE, "6517"));
builder.with(spec1);
UserSpecification spec2 = new UserSpecification(new SearchCriteria("ueberstd", SearchOperation.GREATER_THAN, "5"));
builder.with(spec2);
Specification<User> spec = builder.build();
verify(this.userService, times(1)).getUserByFilter(spec);
// verify(this.UsernehmerService, times(1)).getUserByFilter(argsCaptor.capture());
// List<Specification> capturedArgs = argsCaptor.getAllValues();
// assertEquals(Specification.where(spec1).and(spec2), Specification.where(capturedArgs.get(0)));
}
}
and the UserService:
@Service
public class UserService {
@Autowired
UserRepository userRepository;
public List<User> getUserByFilter(Specification<User> spec) {
List<User> userList = userRepository.findAll(spec);
return userList;
}
}
The problem is that you are trying to compare lamdas returned by Specification.where(spec1).and(spec2)
public interface Specification<T> extends Serializable {
default Specification<T> and(Specification<T> other) {
return Specifications.composed(this, other, AND);
}
}
public class Specifications<T> implements Specification<T>, Serializable {
static <T> Specification<T> composed(@Nullable Specification<T> lhs, @Nullable Specification<T> rhs, CompositionType compositionType) {
return (root, query, builder) -> {
Predicate otherPredicate = rhs == null ? null : rhs.toPredicate(root, query, builder);
Predicate thisPredicate = lhs == null ? null : lhs.toPredicate(root, query, builder);
return thisPredicate == null ? otherPredicate
: otherPredicate == null ? thisPredicate : compositionType.combine(builder, thisPredicate, otherPredicate);
};
}
}
These lambdas are not comparable.
I encourage you to move some logic from the controller to the service:
in your service expose a method that accepts a list of UserSpecification
objects. You can control UserSpecification
and override equals in this class.