테스트 코드 유지보수 해보기 1

2023. 5. 14. 21:48테스트

배경

저희 조직에서는 테스트 코드를 잘 작성하지 못하고 있었습니다.

저는 그러한 이유에는 크게 3가지가 있다고 생각했습니다.

  • 시간 부족
  • 테스트 코드 작성에 대한 이해
  • 테스트 코드의 관리 상태

수 차례의 회고를 진행하며 팀원들에게 테스트 코드에 대한 중요성을 어필하였고, 관련 내용에 대해 별도로 논의를 거쳐왔습니다.

그 과정에서 3가지 중 시간 부족, 테스트 코드 작성에 대한 이해 항목은 어느 정도 해결할 수 있었습니다.

사실 마지막 항목인 테스트 코드의 관리 상태가 가장 어려운 문제였습니다.

테스트 코드는 결국 우리가 개발한 코드가 정상적으로 동작하는지 확인하는 용도가 가장 큰데, 기존에 존재하는 테스트 코드들이 환경이나 요구 사항 변동 등에 의해 정상적으로 동작하고 있지 않았습니다.

개인적으로 이 문제를 해결하고 싶었고, 빠르게 테스트 코드를 도입하고 싶었기에 짬을 내서 조금씩 개선해보자 마음 먹었습니다.

 

진행

동작하지 않는 테스트

첫번째로 환경 차이가 존재했습니다.

test에 대한 DB 설정을 다음과 같이 설정했는데, 구성원의 로컬 환경 마다 동일한지 보장이 안되었습니다.

spring.datasource.url=jdbc:mariadb://localhost:3306/test

테스트 결과 전과 후

 

제 경우는 test 스키마가 사전에 생성되어 있지 않아서 문제가 있었습니다.

해당 프로젝트는 멀티 모듈로 구성되어 있는데, 우선 작은 모듈은 임시로 test 스키마를 로컬에 생성하여 해결할 수 있었습니다.

 

테스트가 가장 많은 모듈에서는 스키마를 생성해주었는데도 불구하고, 많은 테스트에 에러가 발생하고 있었습니다.

 

테스트 결과

 

이렇게나 많은 테스트에서 문제가 발생하는 것을 보면 어떤 이유인지 예상이 되시나요?

맞습니다. 바로 테이블이 생성되어 있지 않아서 문제가 발생했습니다.

spring.jpa.hibernate.ddl-auto=create 상태인데도 말이죠.

 

여기서 테스트 환경에 대한 중요성을 굉장히 크게 느꼈습니다. 조직에서 테스트 환경을 동일하게 구축하지 않는다면 위와 같은 상황이 벌어질 수 있으니까요.

확인해보니 구성원들이 로컬에서 테스트용으로 사용하는 스키마명도 제각기 달랐습니다.

팀 규모가 크지 않다면 팀 내부적으로 환경을 구축해도 되겠지만, 테스트 코드는 협업하는 팀에서도 실행시킬 수 있고 이후 다루는 내용이지만 파이프라인에서도 동작이 가능해야 한다고 생각합니다.

 

그리고, 테스트 간 격리도 매우 중요한 문제입니다.

테스트 격리를 위한 방법은 다양합니다.

  • @BeforeEach, @AfterEach 등 junit에서 제공하는 어노테이션 이용
    • 어플리케이션 코드로 인스턴스나 데이터를 핸들링 할 수 있습니다.
// example
private MarketRepository marketRepository;
        
@BeforeEach
void init() {
    Market market = new Market();
    marketRepository.save(market);
}

@AfterEach
void destroy() {
    marketRepository.deleteAll();
}

 

  • @Sql 어노테이션을 통해 .sql 실행
// example
@Test
@Sql("./clean.sql")
void test() {
    Long ids = marketRepository.findIdsByType(MarketType.ONLINE);
    marketRepository.updateByIds(ids);
}

 

 

  • @Transactional 어노테이션이 제공하는 자동 롤백 기능
    • 테스트 클래스에 @Transactional 어노테이션을 선언하면, 테스트 패키지에서는 자동으로 데이터는 롤백 됩니다.
@SpringBootTest
@Transactional
class marketServiceTest {

    @Autowired
    private OrderService orderService;

    @Autowired
    private MarketService marketService;

    @Test
    void should_marketUpdated_when_orderCreated() {
        Order order = Order.create();

        // ..
        // 종료 후 롤백
    }
}
  •  
  • TestExecutionListener 사용
    • AbstractTestExecutionListener 를 상속하여 구현한다면, 테스트 클래스/메서드의 동작 전/후에 원하는 동작을 추가할 수 있습니다.
  • data.sql, schema.sql 스크립트를 통한 초기화
    • 위에서 다룬 방법들은 대부분 후처리에 가까운데, 이 방법은 스프링 애플리케이션이 시작될 때 데이터베이스에 스키마와 데이터를 초기화하는 방법입니다.

 

 

데이터에 대한 정합성이 맞지 않을 수 있다면 테스트를 마음 놓고 작성하지 못할 뿐더러, 작성한 테스트 코드를 신뢰할 수도 없겠죠?

 

아까 보여드렸던 실패 케이스 대부분은 테이블이 존재하지 않아서 동작하지 않는 경우였습니다.

우선, 테스트 패키지에서 스프링 애플리케이션이 실행될 때 마다 데이터베이스를 스크립트 기반으로 초기화하는 방법을 택했습니다.

  • spring.sql.init.mode=always 설정
    • spring.jpa.hibernate.ddl-auto=none
  • data.sql, schema.sql 파일 resources 디렉토리에 생성

환경 마다 test 스키마가 없을 수 있기에 schema.sql 스크립트를 다음과 같이 작성하였습니다.

DROP DATABASE IF EXISTS `test`;
CREATE SCHEMA `test`;
USE `test`;

-- Create tables ..

이렇게 초기 스키마, 테이블, 데이터 설정만으로 많이 개선 되었음을 확인할 수 있습니다.

테스트 결과

 

아직 갈 길이 멀어보이는데, 초반에 보여드렸던 상태에 비해 많이 양호해졌습니다.

다음 포스팅에서 나머지 테스트들에 대한 개선 과정을 이어가 보도록 하겠습니다.