개발이군고구마

[냄새라도 TDD/OOP] 3. 통합테스트 (서비스) 본문

SERVER/Architecture

[냄새라도 TDD/OOP] 3. 통합테스트 (서비스)

김구황 2025. 6. 27. 08:39
728x90
[OOP 설계 프로세스]
1. 설계 원칙 확립 < 구현 1단계
2-1. TDD 구현 단위(도메인) < 구현 2단계
2-2. TDD 구현 단위(컨트롤러-검증) < 구현 2단계

3. TDD 구현 통합

 


1. 서비스 통합테스트 

🌐 환경설정 

 

1) 통합테스트 DB 연결 설정과 뼈대 잡기 

@TestConfiguration
public class MySqlTestContainerConfig {

	@Bean
	@ServiceConnection
	public MySQLContainer<?> mySQLContainer() {
		return new MySQLContainer<>("mysql:8.0");
	}
}

- DB 로컬 연결은 컨테이너를 사용

-> 일일이 로컬에 테스트 환경 구축할 필요 없이 모든 팀원들과 같은 테스트 환경을 맞출 수 있음 

 

@SpringBootTest(classes = {
	GreenApplication.class,
	MySqlTestContainerConfig.class,
	TestBeanConfig.class
}, webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
public class ServiceIntegrationTest {
}

 

class InfoServiceIntTest extends ServiceIntegrationTest {
	...
}

 


🌐 원칙

- 요청은 정상적으로 왔다는 가정 하에 (컨트롤러에서 검증함) 

- 응답값 정상적으로 반환되는지 확인함 

 

2) 테스트 생각해보기 무엇이 필요할까

[조회]
1) 순서에 맞게 데이터가 뽑히는지 확인 (정렬)
2) 삭제나 미전시 표시 데이터는 조회가 되지 않아야 함 
3) key 값은 제대로 들어왔으나 데이터로 조회가 되지 않는 경우 적절한 예외를 던지는지 확인 

[등록/수정/삭제]
1) 요청 값은 정상적으로 들어왔으므로 제대로 등록/수정 되었는지만 확인
* 특히 Enum 값이 데이터에 들어갈 땐 Enum의 Code 값으로 들어가게 되는지 
2) 삭제도 마찬가지 

 

✅ 등록 테스트시에 MySql 로컬 저장은 컨테이너를 사용한다. 

✅ 서비스단 파일의 bounded context 의 경우엔 @SpyBean 을 사용하여 호출시에 아무것도 하지 않게 stubbing 진행한다. 

실제 서비스와 repository는 실제 Spring bean을 이용하기 때문에 @Autowired 사용 

 

[조회]

🔵 테스트 코드  

  • Entity를 생성해서 delete 마킹하여 저장함 
  • 해당 정보는 제외하고 조회가 되어야 함 
@Test
void 정보_목록을_조회할_때에는_삭제된_정보는_포함되지_않는다() {
    // given
    infoEntity.markDeleted();
    infoRepository.save(infoEntity);

    InfoRequest infoRequest2 = createSaveInfoRequest();
    infoRepository.save(infoRequest2.toEntity());

    int page = 0;
    int size = 10;

    // when
    var response = infoService.getInfosForAdmin(page, size);

    // then
    assertThat(response.content()).size().isEqualTo(1);
}

 

🔴  실제 구현 코드

@Where(clause = "deleted = false AND is_display = 'Y'")
public class InfoEntity extends BaseEntity {
	...
}

 

 

[등록]

🔵 테스트 코드  

@MockitoSpyBean
private FileManager fileManager;
@Autowired
private JdbcTemplate jdbcTemplate;

@Nested
class 디폴트_Entity_생성하지_않음 {
    @BeforeEach
    void cleanUpBefore() {
       jdbcTemplate.execute("TRUNCATE TABLE INFO");
    }

    @Test
    void 정보를_등록한다() {
	   
       // given
       InfoRequest saveRequest = createSaveInfoRequest();
       Mockito.doNothing().when(fileManager).confirmUsingImage(Mockito.anyString());

       // when
       var response = infoService.saveInfo(saveRequest);

       //then
       assertThat(response.id()).isEqualTo("P000001");
       assertThat(response.title()).isEqualTo(saveRequest.title());
       assertThat(response.modifiedDate()).isNotNull(); // 수정일자 확인
    }
}

 

  • fileManager 에 있는 메서드를 접근할 떄 doNothing
  • Test 실행할 때마다 Auto Increment 정보가 계속해서 남아 있어서 info table truncate 진행하고 통합 테스트 

🔴  실제 구현 코드

@Override
public InfoDetailResponseByAdmin updateInfo(String infoId, InfoRequest updateRequest) {
    InfoEntity infoEntity = getInfoEntity(infoId);
    if (StringUtils.isNotEmpty(infoEntity.getImageUrl())) {
       fileManager.unUseImage(infoEntity.getImageUrl());
    }
    log.info("[InfoServiceImpl] 정보공유 수정합니다. 정보공유 번호: {}", infoEntity.getId());
    makeUpdateEntity(updateRequest, infoEntity);
    fileManager.confirmUsingImage(updateRequest.imageUrl());
    return InfoDetailResponseByAdmin.from(infoEntity);
}

 

 

2. 과연 통합테스가 꼭 필요한 것일까? 

처음 의도한 바에 따르면 JPA Hibernate에서 받아오는 응답값이 제대로 도출되나 였다.

  • 순서에 맞게 도출되나 
  • 도출된 값은 응답 Wrapper class에 제대로 매핑 되나 

그러나 생각해보면 응답값에 대한 검증은 Entity 클래스만 만들어주고 stubbing을 하게 되면 충분히 단위 테스트 로도 검증을 할 수 있다는 것이다. 

 

현재 구조상 서비스에 다양한 로직이 들어가는 것이 아니고 단순 CRUD 이기 때문에 통합테스트에서 걸리는 시간적인 비용을 생각하면 단위테스트로 변경도 생각할 수 있을 것 같다.