Search code examples
spring-data-jpaspring-boot-test

inject a JPA repository in Spring Boot test, without session issue


I am adding some tests on my Spring Boot 2.4 application that works well in production.

In one of my SpringBootTest , I call the API (using mockMvc) and compare the result with what I have in the DB.

@SpringBootTest
@AutoConfigureMockMvc
@ActiveProfiles("test")
class TicketIT {


    @Autowired
    private MockMvc mockMvc;

    @Autowired
    private ObjectMapper objectMapper;

    @Autowired
    private TicketTypeRepository ticketTypeRepository;
    
    
    @Test
    void shouldReturnListOfTicketTypes() throws Exception {
    
    RequestBuilder request =
        MockMvcRequestBuilders.get(RESOURCE_BASE_URL + "/types").contentType(APPLICATION_JSON);

    String responseAsString =
        mockMvc
            .perform(request)
            .andExpect(status().isOk())
            .andReturn()
            .getResponse()
            .getContentAsString();

    List<TicketTypesRepresentation> ticketTypes =
        objectMapper.readValue(
            responseAsString, new TypeReference<List<TicketTypesRepresentation>>() {
            });

    assertThat(ticketTypes).hasSameSizeAs(ticketTypeRepository.findAll());

    }
}

I have the feeling I've written that type of tests hundreds of times, but on this one, I am facing a problem : my application is configured correctly, because I receive a list of items in the API response.

However, what I find strange is that I get an exception from the ticketTypeRepository.findAll() call :

failed to lazily initialize a collection of role ... could not initialize proxy - no Session

I understand the issue, and I can fix it either by making the relation eager (with @Fetch(FetchMode.JOIN) on the entity), or my making the test @Transactional but I am not sure I like any of the options..

I don't remember facing that issue in the past in other Spring Boot tests so I am a bit puzzled.

Am I missing something to make sure that all the calls made to ticketTypeRepository are made within a transaction ? TicketTypeRepository is a wrapper around a CrudRepository, is it the reason why it doesn't work directly ?

Here's the entity and repository code :


    public class JpaTicketTypeRepository implements TicketTypeRepository {

    public List<TicketType> findAll() {

        var allTicketTypesEntity= jpaTicketTypesEntityRepository.findAll();

        return StreamSupport.stream(allTicketTypesEntity.spliterator(), false)
                .map(TicketTypeEntity::toTicketTypeList)
                .collect(Collectors.toList())
                .stream().flatMap(List::stream)
                .collect(Collectors.toList());

    }
}

and the entity (simplified) :

    @Table(name = "TICKET_TYPES")
    @Entity
    @Slf4j
    public class TicketTypeEntity {

      @Id
      private Long id;

      @OneToMany
      @JoinTable(name = "TICKET_TYPES_GROUPS",
          joinColumns =
              {@JoinColumn(name = "TICKET_TYPE_ID", referencedColumnName = "ID")},
          inverseJoinColumns =
              {@JoinColumn(name = "TICKET_GROUP_ID", referencedColumnName = "ID")})
      @Nonnull
      private List<TicketGroupsEntity> ticketGroupsEntity;

      @Nonnull
      public List<TicketType> toTicketTypeList() {

        log.info("calling toTicketTypeList for id "+id);
        log.info("     with size : "+ticketGroupsEntity.size());

        return ticketGroupsEntity.stream().map(group -> TicketType.builder()
            .id(id)
            .build()).collect(Collectors.toList());
      }
    }

The exception happens the first time size() is called on the collection :

failed to lazily initialize a collection of role: my.service.database.entities.TicketTypeEntity.ticketGroupsEntity, could not initialize proxy - no Session org.hibernate.LazyInitializationException: failed to lazily initialize a collection of role: my.service.database.entities.TicketTypeEntity.ticketGroupsEntity, could not initialize proxy - no Session at org.hibernate.collection.internal.AbstractPersistentCollection.throwLazyInitializationException(AbstractPersistentCollection.java:606) at org.hibernate.collection.internal.AbstractPersistentCollection.withTemporarySessionIfNeeded(AbstractPersistentCollection.java:218) at org.hibernate.collection.internal.AbstractPersistentCollection.readSize(AbstractPersistentCollection.java:162) at org.hibernate.collection.internal.PersistentBag.size(PersistentBag.java:371) at my.service.database.entities.TicketTypeEntity.toTicketTypeList(TicketTypeEntity.java:78) at java.base/java.util.stream.ReferencePipeline$3$1.accept(ReferencePipeline.java:195) at java.base/java.util.ArrayList$ArrayListSpliterator.forEachRemaining(ArrayList.java:1655) at java.base/java.util.stream.AbstractPipeline.copyInto(AbstractPipeline.java:484) at java.base/java.util.stream.AbstractPipeline.wrapAndCopyInto(AbstractPipeline.java:474) at java.base/java.util.stream.ReduceOps$ReduceOp.evaluateSequential(ReduceOps.java:913) at java.base/java.util.stream.AbstractPipeline.evaluate(AbstractPipeline.java:234) at java.base/java.util.stream.ReferencePipeline.collect(ReferencePipeline.java:578) at my.service.database.JpaTicketTypeRepository.findAll(JpaTicketTypeRepository.java:29)


Solution

  • I believe you mis-interpret the stack trace. The problem is not in calling the method size() on the result of findAll(), but in the method findAll itself.

    In findAll you call TicketTypeEntity.toTicketTypeList, which converts DB entity to DTO. This method touches ticketGroupsEntity, which is a lazy collection.

    The code fails in unit test, but runs when accessed via springs controller. This is due to Open Session In View, which is enabled by default.

    See:

    You could solve it multiple ways:

    • @Transactional findAll (be aware of lazy loading issues)
    • explicit fetch in query
    • entityGraph

    But to my eyes your entity mapping looks suspicious, you seem to have all data needed to construct TicketType in TicketGroupsEntity. Maybe you could query that entity instead?