Search code examples
javaspring-bootjunitmockitospecifications

Cannot invoke "org.springframework.data.domain.Page.getContent()" for Junit Test with the usage of Specification in Spring Boot


I tried to use specification instead of using findAllByAnything in my Spring Boot. When I tried to implement a test method with the usage of Specification for JUnit, I got an error shown below.

How can I solve the issue?

This part which is shown below in getAdminUsersWithAdmin returns null

Page<AdminUserEntity> adminUserEntitiesByOrganization = adminUserRepository.findAll(specification, listRequest.toPageable()); 
    

Here is the relevant method named getAdminUsersWithAdmin

private Page<AdminUser> getAdminUsersWithAdmin(AdminUserListRequest listRequest) {
        String organizationId = identity.getOrganizationId();
        Specification<AdminUserEntity> specification = Specification
                .where(AdminUserSpecifications.hasOrganizationId(organizationId));
        Page<AdminUserEntity> adminUserEntitiesByOrganization = adminUserRepository.findAll(specification, listRequest.toPageable());
        List<AdminUser> adminUsersByOrganization = adminEntityToAdminMapper.map(adminUserEntitiesByOrganization.getContent());
        return Page.of(adminUserEntitiesByOrganization, adminUsersByOrganization);
    }

Here is the AdminUserSpecifications shown below

public class AdminUserSpecifications {

    
    public static Specification<AdminUserEntity> hasOrganizationId(String organizationId) {
        return (root, query, criteriaBuilder) ->
                SearchSpecificationBuilder.eq(criteriaBuilder, root.get("organizationId"), organizationId);
    }
}

Here is the eq method in SearchSpecificationBuilder

public static Predicate eq(CriteriaBuilder criteriaBuilder, Path<Object> path, Object value) {
        if (value == null) {
            return null;
        }
        return criteriaBuilder.equal(path, value);
    }

Here is the test method shown below

@Test
    void givenUserListRequest_whenAdminwithRoleIsAdmin_thenReturnAllAdminUsers() {
        // Given
        AdminUserListRequest mockAdminUserListRequest = new AdminUserListRequestBuilder().withValidValues().build();

        AdminUserEntity mockAdminUserEntity = new AdminUserEntityBuilder().withValidFields().build();

        List<AdminUserEntity> mockAdminUserEntities = Collections.singletonList(mockAdminUserEntity);
        Page<AdminUserEntity> mockPageAdminUserEntities = new PageImpl<>(mockAdminUserEntities);

        List<AdminUser> mockAdminUsers = ADMIN_ENTITY_TO_ADMIN_MAPPER.map(mockAdminUserEntities);
        Page<AdminUser> mockPageAdminUsers = Page.of(mockPageAdminUserEntities, mockAdminUsers);

        UserType userType = UserType.ADMIN;
        Specification<AdminUserEntity> specification = Specification.where(AdminUserSpecifications.hasOrganizationId(mockAdminUserEntity.getOrganizationId()));

        // When
        Mockito.when(identity.getUserType()).thenReturn(userType);
        Mockito.when(identity.getOrganizationId()).thenReturn(mockAdminUserEntity.getOrganizationId());
        Mockito.when(adminUserRepository.findAll(specification, mockAdminUserListRequest.toPageable()))
                .thenReturn(mockPageAdminUserEntities);

        Page<AdminUser> pageAdminUsers = adminUserService.getAdminUsers(mockAdminUserListRequest);

        // Then
        PageBuilder.assertEquals(mockPageAdminUsers, pageAdminUsers);

        Mockito.verify(adminUserRepository, Mockito.times(1))
                .findAll(specification, mockAdminUserListRequest.toPageable());
    }

Here is the error shown below when I run givenUserListRequest_whenAdminwithRoleIsAdmin_thenReturnAllAdminUsers method.

java.lang.NullPointerException: Cannot invoke "org.springframework.data.domain.Page.getContent()" because "adminUserEntitiesByOrganization" is null

Editted

I revised this line shown below

Mockito.when(adminUserRepository.findAll(Mockito.any(Specification.class), Mockito.eq(mockAdminUserListRequest.toPageable()))) .thenReturn(mockPageAdminUserEntities);

I got this issue shown below

Argument(s) are different! Wanted:
adminUserRepository.findAll(
    com.admin_user.repository.specification.AdminUserSpecifications$$Lambda$421/0x000000080121b220@7f02251,
    Page request [number: 0, size 10, sort: UNSORTED]
);

adminUserRepository.findAll(
    com.admin_user.repository.specification.AdminUserSpecifications$$Lambda$421/0x000000080121b220@dffa30b,
    Page request [number: 0, size 10, sort: UNSORTED]
);

Solution

  • The problem lies in this line:

    Mockito.when(adminUserRepository.findAll(specification, mockAdminUserListRequest.toPageable()))
                    .thenReturn(mockPageAdminUserEntities);
    

    Your specification is a lambda - Specification.where returns passed in specification if it is not null, and you pass in a lambda created by hasOrganizationId.

    As the arguments passed in Mockito.when and actual prod code are not equal, Mockito returns default value of Page<AdminUserEntity> adminUserEntitiesByOrganization - which is null for any non-primitive type.

    See the following test:

    class AdminUserEntity{}
    
    public class LambdaTest {
        public static Specification<AdminUserEntity> hasOrganizationId(String organizationId) {
            return (root, query, criteriaBuilder) -> criteriaBuilder.equal(root.get("organizationId"), organizationId);
        }
    
        @Test
        void lambdasAreNotEqual() {
            var spec1a = hasOrganizationId("org1");
            var spec1b = hasOrganizationId("org1");
    
            Assertions.assertThat(spec1a).isNotEqualTo(spec1b);
        }
    }
    

    There are 2 options to solve:

    Option 1: relax your expectations about the argument

    use Mockito.any(Specification.class)

    Mockito.when(adminUserRepository.findAll(Mockito.any(Specification.class), Mockito.eq(mockAdminUserListRequest.toPageable()))) .thenReturn(mockPageAdminUserEntities);
    
    • By default, Mockito uses equals to compare argument passed to Mockito.when and argument in method under test
    • If one ArgumentMatcher is specified explicitly, all must be specified explicitly (Mockito.eq)

    Option 2: use an object that which implements equals (and hashCode for completeness)

    A lambda can be modified to concrete subclass of Specification

    public class AdminUserSpecifications {
        public static class AdminUserEntityHasOrganizationIdSpecification implements Specification<AdminUserEntity> {
            private final String organizationId;
    
            public AdminUserEntityHasOrganizationIdSpecification(String organizationId) {
                this.organizationId = organizationId;
            }
    
            @Override
            public Predicate toPredicate(Root<AdminUserEntity> root, CriteriaQuery<?> query, CriteriaBuilder criteriaBuilder) {
                return criteriaBuilder.equal( root.get("organizationId"), organizationId);
            }
    
            @Override
            public boolean equals(Object o) {
                if (this == o) return true;
                if (o == null || getClass() != o.getClass()) return false;
                AdminUserEntityHasOrganizationIdSpecification that = (AdminUserEntityHasOrganizationIdSpecification) o;
                return Objects.equals(organizationId, that.organizationId);
            }
    
            @Override
            public int hashCode() {
                return Objects.hash(organizationId);
            }
        } 
    
        public static Specification<AdminUserEntity> hasOrganizationId(String organizationId) {
            return new AdminUserEntityHasOrganizationIdSpecification(organizationId);
        }
    }
    

    In both options, I assume that object returned by mockAdminUserListRequest.toPageable() correctly implements equals. If not, add equals or use option 1 or option 2.