Junit에서 테스트코드를 작성 중 계속 테스트실패가 일어났다. 아무리봐도 코드에는 이상이 없었고 해당 로직을 Junit이 아닌 API로 호출시에는 정상적으로 작동하였다.
디버깅을 통해 확인해보니 @OneToMany를 통해 자식값들을 가져오는 List 객체가 null로 넘어오는 것이다.
API를 호출할 때는 정상적으로 값이 넘오는데 Junit에서는 왜 null로 넘어오는 것일까?
해당 현상에 대해 파악하던 중 아래 글을 발견하게 되었고 원인과 조치방법에 대해서 알 수 있었다.
원인과 조치방법에 대해서 다시 정리해본다.
우리의 프로젝트의 Layer는 크게 Controller -> Service -> Repository로 구성되어 있다. Junit은 Service Layer를 호출하여 테스트코드를 작성하였다.
@OneToMany가 작동하지 않는 현상은 Service Layer에 @Transactional을 작성한 메소드를 호출할 때 발생하였다.
위의 공유한 URL에서는 @Transactional(propagation = Propagation.REQUIRED) 일 때 발생하며(옵션을 설정하지 않을 때는 Propagation.REQUIRED default이다.) 옵션을 @Transactional (propagation = Propagation.SUPPORTS) 로 했을 때는 적상적으로 작동한다는 것이다.
본인의 테스트코드도 위와 같이 변경해보았더니 정상적으로 작동하였다.
안타깝게도 해당 글에서도 왜 이러한 현상이 발생하는지 모르겠다고 한다. 이러한 현상이 발생하는 원인을 알고자 하는 문의글이었다.
아래 테스트 코드처럼 Layer를 구성했을 때는 @OneToMany @Transactional에 Propagation.SUPPORTS을 추가해야 한다.
샘플코드는 위 글에서 참조하였다.
TEST
@ExtendWith(SpringExtension.class)
@SpringBootTest(classes = DemoUsingModelMapperAndOrikaApplication.class)
@Transactional(propagation = Propagation.SUPPORTS)
@ActiveProfiles("test")
class DemoUsingModelMapperAndOrikaApplicationTests {
private SonController sonController;
private FamilyController familyController;
@Autowired
public DemoUsingModelMapperAndOrikaApplicationTests(SonController sonController, FamilyController familyController) {
this.sonController = sonController;
this.familyController = familyController;
}
@Test
public void testAddSonsToMother() {
//check to make sure mother now also contains son
MotherDTO retrievedMother = familyController.getMotherBySon(newSon.getSonId());
Assertions.assertTrue( retrievedMother.getSonsId() != null && retrievedMother.getSonsId().size() > 0);
Assertions.assertEquals(retrievedMother.getSonsId().get(0), retrievedSon.getSonId());
}
}
Mother Entity
@Table(name = "mother")
@Getter
@Setter
@Builder
@NoArgsConstructor
@AllArgsConstructor
@EqualsAndHashCode
@ToString
public
class Mother{
//...
@OneToMany(
mappedBy = "mother",
cascade = CascadeType.ALL,
fetch = FetchType.LAZY,
orphanRemoval = true
)
private List<Son> sons;
}
Son Entity
@Entity
@Table(name = "son")
@Getter
@Setter
@Builder
@NoArgsConstructor
@AllArgsConstructor
@EqualsAndHashCode
@ToString
public
class Son{
//...
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "mother_id")
private Mother mother;
}
MotherRepository
@Repository
public interface MotherRepository extends JpaRepository<Mother, Long> {
@Transactional(readOnly = true)
@Query("select distinct m from Mother m inner join m.sons s where s.sonId = :sonId")
Mother findMotherBySonId(@Param("sonId") long sonId);
}
FamilyService
@Service("familyServiceImpl")
public class FamilyServiceImpl implements FamilyService {
private MotherRepository motherRepository;
@Autowired
public FamilyServiceImpl(MotherRepository motherRepository) {
this.motherRepository = motherRepository;
}
@Override
@Transactional(readOnly = true)
public Mother getMotherBySonId(long sonId){
return motherRepository.findMotherBySonId(sonId);
}
}
FamilyFacade
public class FamilyFacade {
@Qualifier("familyServiceImpl")
@Autowired
private FamilyService familyService;
@Autowired
private ModelMapper modelMapper;
@Transactional(readOnly = true)
public MotherDTO getMotherBySonId(long sonId){
return convertToMotherDTOModelMapper(familyService.getMotherBySonId(sonId));
}
}
FamilyController
@RestController
@RequestMapping("/family")
public class FamilyController {
private FamilyFacade familyFacade;
/**
* Here we are using the principle of IoC and DI to instantiate the {@link FamilyFacade}
* @param familyFacade - {@link FamilyFacade}
*/
@Autowired
public void setFamilyFacade(FamilyFacade familyFacade) {
this.familyFacade = familyFacade;
}
@GetMapping("/mother/son/{sonId}")
public MotherDTO getMotherBySon(@PathVariable("sonId") long sonId) {
return familyFacade.getMotherBySonId(sonId);
}
}
LOG - Propagation.SUPPORTS
2020-04-05 17:57:08.102 INFO 8072 --- [ main] o.s.t.c.transaction.TransactionContext : Began transaction (1) for test context [DefaultTestContext@e15b7e8 testClass = DemoUsingModelMapperAndOrikaApplicationTests, testInstance = com.example.family.demo.DemoUsingModelMapperAndOrikaApplicationTests@43f7f48d, testMethod = testAddSonsToMother@DemoUsingModelMapperAndOrikaApplicationTests, testException = [null], mergedContextConfiguration = [WebMergedContextConfiguration@1b2abca6 testClass = DemoUsingModelMapperAndOrikaApplicationTests, locations = '{}', classes = '{class com.example.family.demo.DemoUsingModelMapperAndOrikaApplication, class com.example.family.demo.DemoUsingModelMapperAndOrikaApplication}', contextInitializerClasses = '[]', activeProfiles = '{test}', propertySourceLocations = '{}', propertySourceProperties = '{org.springframework.boot.test.context.SpringBootTestContextBootstrapper=true}', contextCustomizers = set[org.springframework.boot.test.context.filter.ExcludeFilterContextCustomizer@4c9f8c13, org.springframework.boot.test.json.DuplicateJsonObjectContextCustomizerFactory$DuplicateJsonObjectContextCustomizer@be64738, org.springframework.boot.test.mock.mockito.MockitoContextCustomizer@0, org.springframework.boot.test.web.client.TestRestTemplateContextCustomizer@2667f029, org.springframework.boot.test.autoconfigure.properties.PropertyMappingContextCustomizer@0, org.springframework.boot.test.autoconfigure.web.servlet.WebDriverContextCustomizerFactory$Customizer@4ac3c60d], resourceBasePath = 'src/main/webapp', contextLoader = 'org.springframework.boot.test.context.SpringBootContextLoader', parent = [null]], attributes = map['org.springframework.test.context.web.ServletTestExecutionListener.activateListener' -> true, 'org.springframework.test.context.web.ServletTestExecutionListener.populatedRequestContextHolder' -> true, 'org.springframework.test.context.web.ServletTestExecutionListener.resetRequestContextHolder' -> true]]; transaction manager [org.springframework.orm.jpa.JpaTransactionManager@6fe9c048]; rollback [true]
2020-04-05 17:57:08.181 TRACE 8072 --- [ main] o.s.t.i.TransactionInterceptor : Getting transaction for [com.example.family.demo.family.facade.FamilyFacade.saveMother]
2020-04-05 17:57:08.193 TRACE 8072 --- [ main] o.s.t.i.TransactionInterceptor : Getting transaction for [com.example.family.demo.family.service.FamilyServiceImpl.saveMother]
2020-04-05 17:57:08.204 TRACE 8072 --- [ main] o.s.t.i.TransactionInterceptor : Getting transaction for [org.springframework.data.jpa.repository.support.SimpleJpaRepository.save]
2020-04-05 16:21:35.110 TRACE 29840 --- [ main] o.s.t.i.TransactionInterceptor : Getting transaction for [com.example.family.demo.family.facade.FamilyFacade.getMotherBySonId]
2020-04-05 16:21:35.111 TRACE 29840 --- [ main] o.s.t.i.TransactionInterceptor : Getting transaction for [com.example.family.demo.family.service.FamilyServiceImpl.getMotherBySonId]
2020-04-05 16:21:35.112 TRACE 29840 --- [ main] o.s.t.i.TransactionInterceptor : Getting transaction for [org.springframework.data.jpa.repository.support.SimpleJpaRepository.findMotherBySonId]
Hibernate: select distinct mother0_.mother_id as mother_i1_0_, mother0_.mother_age as mother_a2_0_, mother0_.mother_name as mother_n3_0_ from mother mother0_ inner join son sons1_ on mother0_.mother_id=sons1_.mother_id where sons1_.son_id=?
2020-04-05 16:21:35.136 TRACE 29840 --- [ main] o.s.t.i.TransactionInterceptor : Completing transaction for [org.springframework.data.jpa.repository.support.SimpleJpaRepository.findMotherBySonId]
2020-04-05 16:21:35.143 TRACE 29840 --- [ main] o.s.t.i.TransactionInterceptor : Completing transaction for [com.example.family.demo.family.service.FamilyServiceImpl.getMotherBySonId]
Hibernate: select sons0_.mother_id as mother_i7_1_0_, sons0_.son_id as son_id1_1_0_, sons0_.son_id as son_id1_1_1_, sons0_.mother_id as mother_i7_1_1_, sons0_.son_age as son_age2_1_1_, sons0_.son_birth_date as son_birt3_1_1_, sons0_.son_name as son_name4_1_1_, sons0_.son_notes as son_note5_1_1_, sons0_.son_type as son_type6_1_1_ from son sons0_ where sons0_.mother_id=?
2020-04-05 16:21:35.168 TRACE 29840 --- [ main] o.s.t.i.TransactionInterceptor : Completing transaction for [com.example.family.demo.family.facade.FamilyFacade.getMotherBySonId]
2020-04-05 16:21:35.185 INFO 29840 --- [ main] o.s.t.c.transaction.TransactionContext : Rolled back transaction for test: [DefaultTestContext@342c38f8 testClass = DemoUsingModelMapperAndOrikaApplicationTests, testInstance = com.example.family.demo.DemoUsingModelMapperAndOrikaApplicationTests@3966c679, testMethod = testAddSonsToMother@DemoUsingModelMapperAndOrikaApplicationTests, testException = [null], mergedContextConfiguration = [WebMergedContextConfiguration@c88a337 testClass = DemoUsingModelMapperAndOrikaApplicationTests, locations = '{}', classes = '{class com.example.family.demo.DemoUsingModelMapperAndOrikaApplication, class com.example.family.demo.DemoUsingModelMapperAndOrikaApplication}', contextInitializerClasses = '[]', activeProfiles = '{test}', propertySourceLocations = '{}', propertySourceProperties = '{org.springframework.boot.test.context.SpringBootTestContextBootstrapper=true}', contextCustomizers = set[org.springframework.boot.test.context.filter.ExcludeFilterContextCustomizer@48e4374, org.springframework.boot.test.json.DuplicateJsonObjectContextCustomizerFactory$DuplicateJsonObjectContextCustomizer@4a22f9e2, org.springframework.boot.test.mock.mockito.MockitoContextCustomizer@0, org.springframework.boot.test.web.client.TestRestTemplateContextCustomizer@9225652, org.springframework.boot.test.autoconfigure.properties.PropertyMappingContextCustomizer@0, org.springframework.boot.test.autoconfigure.web.servlet.WebDriverContextCustomizerFactory$Customizer@4f209819], resourceBasePath = 'src/main/webapp', contextLoader = 'org.springframework.boot.test.context.SpringBootContextLoader', parent = [null]], attributes = map['org.springframework.test.context.web.ServletTestExecutionListener.activateListener' -> true, 'org.springframework.test.context.web.ServletTestExecutionListener.populatedRequestContextHolder' -> true, 'org.springframework.test.context.web.ServletTestExecutionListener.resetRequestContextHolder' -> true]]
LOG - Propagation.REQUIRED
2020-04-05 17:57:08.102 INFO 8072 --- [ main] o.s.t.c.transaction.TransactionContext : Began transaction (1) for test context [DefaultTestContext@e15b7e8 testClass = DemoUsingModelMapperAndOrikaApplicationTests, testInstance = com.example.family.demo.DemoUsingModelMapperAndOrikaApplicationTests@43f7f48d, testMethod = testAddSonsToMother@DemoUsingModelMapperAndOrikaApplicationTests, testException = [null], mergedContextConfiguration = [WebMergedContextConfiguration@1b2abca6 testClass = DemoUsingModelMapperAndOrikaApplicationTests, locations = '{}', classes = '{class com.example.family.demo.DemoUsingModelMapperAndOrikaApplication, class com.example.family.demo.DemoUsingModelMapperAndOrikaApplication}', contextInitializerClasses = '[]', activeProfiles = '{test}', propertySourceLocations = '{}', propertySourceProperties = '{org.springframework.boot.test.context.SpringBootTestContextBootstrapper=true}', contextCustomizers = set[org.springframework.boot.test.context.filter.ExcludeFilterContextCustomizer@4c9f8c13, org.springframework.boot.test.json.DuplicateJsonObjectContextCustomizerFactory$DuplicateJsonObjectContextCustomizer@be64738, org.springframework.boot.test.mock.mockito.MockitoContextCustomizer@0, org.springframework.boot.test.web.client.TestRestTemplateContextCustomizer@2667f029, org.springframework.boot.test.autoconfigure.properties.PropertyMappingContextCustomizer@0, org.springframework.boot.test.autoconfigure.web.servlet.WebDriverContextCustomizerFactory$Customizer@4ac3c60d], resourceBasePath = 'src/main/webapp', contextLoader = 'org.springframework.boot.test.context.SpringBootContextLoader', parent = [null]], attributes = map['org.springframework.test.context.web.ServletTestExecutionListener.activateListener' -> true, 'org.springframework.test.context.web.ServletTestExecutionListener.populatedRequestContextHolder' -> true, 'org.springframework.test.context.web.ServletTestExecutionListener.resetRequestContextHolder' -> true]]; transaction manager [org.springframework.orm.jpa.JpaTransactionManager@6fe9c048]; rollback [true]
2020-04-05 17:57:08.181 TRACE 8072 --- [ main] o.s.t.i.TransactionInterceptor : Getting transaction for [com.example.family.demo.family.facade.FamilyFacade.saveMother]
2020-04-05 17:57:08.193 TRACE 8072 --- [ main] o.s.t.i.TransactionInterceptor : Getting transaction for [com.example.family.demo.family.service.FamilyServiceImpl.saveMother]
2020-04-05 17:57:08.204 TRACE 8072 --- [ main] o.s.t.i.TransactionInterceptor : Getting transaction for [org.springframework.data.jpa.repository.support.SimpleJpaRepository.save]
2020-04-05 16:24:16.772 TRACE 29946 --- [ main] o.s.t.i.TransactionInterceptor : Getting transaction for [com.example.family.demo.family.facade.FamilyFacade.getMotherBySonId]
2020-04-05 16:24:16.773 TRACE 29946 --- [ main] o.s.t.i.TransactionInterceptor : Getting transaction for [com.example.family.demo.family.service.FamilyServiceImpl.getMotherBySonId]
2020-04-05 16:24:16.774 TRACE 29946 --- [ main] o.s.t.i.TransactionInterceptor : Getting transaction for [org.springframework.data.jpa.repository.support.SimpleJpaRepository.findMotherBySonId]
Hibernate: select distinct mother0_.mother_id as mother_i1_0_, mother0_.mother_age as mother_a2_0_, mother0_.mother_name as mother_n3_0_ from mother mother0_ inner join son sons1_ on mother0_.mother_id=sons1_.mother_id where sons1_.son_id=?
2020-04-05 16:24:16.781 TRACE 29946 --- [ main] o.s.t.i.TransactionInterceptor : Completing transaction for [org.springframework.data.jpa.repository.support.SimpleJpaRepository.findMotherBySonId]
2020-04-05 16:24:16.781 TRACE 29946 --- [ main] o.s.t.i.TransactionInterceptor : Completing transaction for [com.example.family.demo.family.service.FamilyServiceImpl.getMotherBySonId]
2020-04-05 16:24:16.781 TRACE 29946 --- [ main] o.s.t.i.TransactionInterceptor : Completing transaction for [com.example.family.demo.family.facade.FamilyFacade.getMotherBySonId]
2020-04-05 16:24:16.802 INFO 29946 --- [ main] o.s.t.c.transaction.TransactionContext : Rolled back transaction for test: [DefaultTestContext@342c38f8 testClass = DemoUsingModelMapperAndOrikaApplicationTests, testInstance = com.example.family.demo.DemoUsingModelMapperAndOrikaApplicationTests@447543ee, testMethod = testAddSonsToMother@DemoUsingModelMapperAndOrikaApplicationTests, testException = org.opentest4j.AssertionFailedError: expected: <true> but was: <false>, mergedContextConfiguration = [WebMergedContextConfiguration@c88a337 testClass = DemoUsingModelMapperAndOrikaApplicationTests, locations = '{}', classes = '{class com.example.family.demo.DemoUsingModelMapperAndOrikaApplication, class com.example.family.demo.DemoUsingModelMapperAndOrikaApplication}', contextInitializerClasses = '[]', activeProfiles = '{test}', propertySourceLocations = '{}', propertySourceProperties = '{org.springframework.boot.test.context.SpringBootTestContextBootstrapper=true}', contextCustomizers = set[org.springframework.boot.test.context.filter.ExcludeFilterContextCustomizer@48e4374, org.springframework.boot.test.json.DuplicateJsonObjectContextCustomizerFactory$DuplicateJsonObjectContextCustomizer@4a22f9e2, org.springframework.boot.test.mock.mockito.MockitoContextCustomizer@0, org.springframework.boot.test.web.client.TestRestTemplateContextCustomizer@9225652, org.springframework.boot.test.autoconfigure.properties.PropertyMappingContextCustomizer@0, org.springframework.boot.test.autoconfigure.web.servlet.WebDriverContextCustomizerFactory$Customizer@4f209819], resourceBasePath = 'src/main/webapp', contextLoader = 'org.springframework.boot.test.context.SpringBootContextLoader', parent = [null]], attributes = map['org.springframework.test.context.web.ServletTestExecutionListener.activateListener' -> true, 'org.springframework.test.context.web.ServletTestExecutionListener.populatedRequestContextHolder' -> true, 'org.springframework.test.context.web.ServletTestExecutionListener.resetRequestContextHolder' -> true]]