나도 테스트코드를 작성해보겠어 (1)

Gwonyeong·2023년 7월 19일
0

유키독

목록 보기
11/11

나는 개인적으로 예전부터 테스트코드가 상당히 난이도가 있는 파트라고 생각했다.
테스트코드를 작성하는 문법, 라이브러리와 프레임워크를 다루는 방법 등이 어려운게 아니다.

어떤 함수를 어떠한 케이스로, 어떻게 시나리오를 작성해서 테스트를 할지.
이 로직을 머릿속으로 생각하고 구현해내는 것을 나는 어렵게 생각하고 있었다.

그 이유로는 테스트코드가 일단 일회성이 되어서는 안된다고 생각했기 때문이다.
테스트코드를 작성하고 나면 한번 테스트를 해보고 버릴 것이 아니라 앞으로 작성할 새로운 api, 로직에도 적용이 되어야 하고, 내가 유지보수, 리팩토링을 하는 로직에도 적용이 되어야 하기 때문이다.

단위테스트는 하나의 함수가 특정 매개변수를 받아 실행을 했을 때, 일정한 값을 return하면 된다고 이해했다.
안의 로직이 어떻게 변경이 되던간에 여러 케이스가 같은 return값을 가진다면 함수는 문제없이 작동되는 것이니까.

그래서 수도없이 고민하고, 래퍼런스를 찾아본 결과 테스트코드로 유명한 향로님의 블로그, chatGPT의 도움(일반적으로 테스트코드를 어떻게 작성할지 이야기해봤다. 4.0과 이야기를 해보았는데 크게 도움이 된지는 모르겠네)
그리고 jest공식문서를 보며 테스트코드를 어떻게 작성할지 이론적으로는 고민을 마쳤다.

그 이론은 다음과 같은데

1. 일단 테스트용 데이터베이스는 필요하다.

다른 라이브러리는 크게 찾아보지 않아서 모르겠지만 일단 jest에는 모킹이라는 기능을 제공하기때문에 특정 모듈, 함수의 반환값을 임의로 설정할 수 있다.
근데 모킹이라는 기능을 들었을 때 나는 그런 생각을 했다.
의존하고 있는 다른 함수를 모킹해서 반환값을 임의로 만드는 것까지는 좋아. 그런데 의존하고 있는 함수가 반환값이 바뀌도록 수정이 된다면? 모킹하는 반환값을 바꿔줘야할까?

이건 귀찮음을 넘어서 관리하기가 너무 어렵지 않을까?

아무리 실제 데이터베이스와 통신하는게 코스트가 많이 들어간다고 해도 production환경과 같게 설정하고 테스트를 진행해야 의미있지 않나?

결국 나는 데이터베이스와 통신하는 로직은 테스트용 데이터베이스를 로컬에 만들어서 테스트가 실행될 때마다 스키마를 삭제하고 다시 생성하도록 만들어 테스트별로 데이터베이스를 분리시켰다.

TypeOrmModule.forRoot({
                    dropSchema: true,
                    type: 'mysql',
                    host: process.env.MYSQL_HOST,
                    port: 3306,
                    username: process.env.MYSQL_USERNAME,
                    password: process.env.MYSQL_PASSWORD,
                    database: process.env.MYSQL_DATABASE,
                    entities: [`src/**/*.entity.{js,ts}`],
                    synchronize: true,
                    // dropSchema 옵션은 어플리케이션 구동시 스키마들을 모두 삭제함.
                }),
  • dropSchema 옵션으로 매번 스키마를 삭제해준다.

테스트용 모듈을 따로 작성할 때 매번 데이터베이스와 연결하고, 스키마를 초기화한다.

2. 그럼 모킹을 사용하지 않아야하나(어떻게 틀을 잡아야 하지)?

모킹의 사용의도를 생각해봤다.
모킹은 내가 테스트하려는 함수에서 의존하고있는 모듈이 있다면 이 값의 반환값을 임의로 조정한다.
그런데 내가 생각한 문제는 이 의존하고 있는 모듈이 문제가 생기거나 변경이 된다면 반환값을 임의로 설정하는게 의미가 있나? 였다.

그렇다면 의존하고있는 모듈이 정상적으로 동작한다고 가정을 할 수 있는 상태이면 되지 않을까? 라는 생각이 머리를 스쳐갔다.
의존하고 있는 모듈을 먼저 테스트를 해보고, 정상적으로 작동한다면 모킹을 해도 괜찮겠다. 라고 생각한 것이다.

나의 프로젝트는

  • 컨트롤러
  • 서비스(비즈니스 로직)
  • 레포지토리(데이터베이스와 직접 통신하는 로직)

이렇게 크게 3가지의 파트로 나누어진다.

그렇다면?
레포지토리는 테스트용 데이터베이스를 작성해서 직접 통신을 시켜보며 단위테스트를 진행하고,
서비스로직은 의존하고있는 레포지토리 로직을 모킹해서 단위테스트를 진행한다.

  • 테스트용 데이터베이스를 만들어서 레포지토리 로직을 테스트를 이미 해보았으니까!

마지막으로 컨트롤러는 직접 요청을 보내보는 e2e테스트로 대체할 수 있지 않을까 라고 생각했다.

그렇다면 api별로 서비스와 레포지토리의 테스트파일을 하나씩, e2e테스트파일은 따로 관리하는 방법으로 폴더구조를 작성해볼 수 있겠다.

3. 테스트는 모든 코드를 진행해야 할까?

향로님의 블로그와, 그 안에 들어있던 래퍼런스에서(https://jwchung.github.io/testing-oh-my)
인상깊게 보았던 문구가 있다.

  • 테스트 할 수 있는 코드와 테스트 할 수 없는 코드가 있다.
  • 테스트 할 수 없는 코드는 최대한 시도를 해보되 너무 연연하지는 말아라.

테스트 할 수 있는 코드와 테스트를 하기 좋은 코드를 비슷한 의미라고 생각했을 때 많은 분들이 이야기하는 테스트하기 좋은 코드는 다음과 같았다.

  • 일정한 값을 매개변수로 전달했을 때, 매번 값이 변하는게 아닌 일정한 값을 반환하는 모듈

나의 유키독 프로젝트에서는 어떻게 구분을 지을 수 있을까?

이 프로젝트에서는 유튜브 API와 구글 로그인 구현을 위한 구글서버와 통신하는 로직을 가지고 있다.
이 외부와 통신하는 로직들은 내가 전달하는 변수의 값에 따라 반환값이 달라진다. 예측을 할수가 없는거지.

그래서 나는 일단 특정 키워드를 이용해 유튜브에서 영상을 가져오는 로직이나 구글 소셜 로그인 기능은 테스트할 수 없다고 판단했다.


  • 이런 코드도 테스트해야할까?
async findAll(userId : number ,findAllKeywordDto: FindAllKeywordDto): Promise<ResponseKeywordDto[]> {
    return await this.findAllKeywordQueryBuilder.findAll(userId, findAllKeywordDto);
  }

테스트코드를 작성하는 과정에서 등장한 최대의 난제였다.

이 코드의 테스트코드를 작성할 때 나는 this.findAllKeywordQueryBuilder.findAll를 모킹해서 특정 값을 반환하도록 만들었다.
근데... 굳이?
어차피 레포지토리(쿼리빌더)와 연결만 시켜주는 로직이라면 이 반환값을 모킹하는게 의미가 있을까?

물론 이 findAll이라는 함수의 내부 내용이 바뀔 수 있다.
다른 모듈을 의존하는 로직이 생길 수도 있고 여러 변경사항이 생길 수 있겠지만...

내가 테스트코드를 작성하기 좋지 않은 방법으로 로직을 작성한 걸까? 이 부분은 고민이 필요하겠다.

profile
부동의 첫사랑

0개의 댓글

관련 채용 정보