테스트 사용 설명서

Dayzen
Vingle Tech Blog
Published in
8 min readJan 14, 2019

안녕하세요, 빙글에서 백엔드 개발자로 일하고 있는 이동현 입니다.

여러분은 테스트 코드를 작성하시나요?

개발을 “제대로" 하다보면, 테스트라는 단어를 반드시 접하게 됩니다.

검색창에서 테스트 코드라고만 검색해도 엄청난 수의 글이 나오네요…

요즘은 제대로된 라이브러리나 어플리케이션이라면, 테스트가 없는게 비상식적일 정도죠. (테스트 coverage의 차이는 있지만)

그렇다면,

테스트는 왜 중요하고, 테스트를 함으로써 얻는 이득은 뭘까요?

오늘은 테스트를 작성해야 하는 이유와 방법을 제가 일하면서 배운것을 토대로 설명해보려고 합니다.

먼저 한가지 중요한 점에 대해서 짚어 갈려합니다.

테스트 코드는 테스트 하려는 기능의 사용설명서여야 합니다.

테스트와 기능의 사용설명서에 대해서 왜 비교를 했는지는 좀 더 뒤에서 설명하도록 하겠습니다!! https://blog.naver.com/p-cher/220910376941

간단한 예시로, 이메일을 보내는 기능의 순서를 생각 해봅시다.

1. 자신의 이메일로 로그인2. 받을 대상의 이메일을 입력3. 메일의 내용을 작성4. 상대방에게 전송

이 내용을 간단한 코드로 옮긴다면 다음과 같습니다

또한 sendMail 함수를 테스트하기위해 다음과 같이 테스트를 만들었습니다

describe("sendMail", () => {
it("should set sender and receiver write payload and send mail") {
...
}
})

좋습니다 이제 테스트를 돌려보죠!!

초록색 성공이 아주 만족스럽네요!!

이대로 테스트 성공을 확인하고 만족하며 다음 개발을 진행해야 할까요?

아닙니다.

앞에서 “테스트는 사용설명서와 같다" 고 이야기 했었죠.

그렇다면 테스트를 다시 번역하여 사용설명서로 만들어보죠

- sendMail 사용설명서1. 송신자 메일 설정 후 수신자 메일 설정 후 메일 내용 작성 후 전송2. 끝

그렇습니다. 사용설명서라기에는 구조가 너무 부족하죠

그렇다면, 설명서처럼 구조를 분리한뒤 테스트를 변경 해볼까요?

1. 송신자의 메일 설정2. 수신자의 메일 설정3. 메일의 내용 작성4. 전송
짜잔! 바뀐 테스트 코드입니다

바뀐 코드를 보면 sendMail은 각 함수들을 옳게 호출 하는지를 테스트 하며

각각의 함수 테스트 코드는 아래와 같이 분리되어 작성됩니다

변경된 테스트 코드에 따라 sendMail의 흐름도 같이 변경되었습니다.

처음 구조와 비교를 했을때 지금이 훨씬 쉽게 읽히지 않나요?

테스트를 이용한 라이프 사이클

이렇게, 테스트를 이용해서 수정-> 통과 -> 리팩토링의 사이클이 작동하게 함으로서 코드 퀄리티를 높이고, 동시에 코드의 안정도를 높일수 있게 되는 것입니다.

다시 말해, 이러한 사이클의 일부로서 작동할수 없는 “테스트"는
“좋은 테스트" 라고 부르기 힘듭니다.

그럼 어떻게 해야 좋은 테스트가 되는건가요?

제가 생각하는 기준은 다음과 같습니다.

1. 다른 사람이 봐도 읽기 좋은 테스트2. 기능의 역할에 충실한 테스트

1. 다른 사람이 봐도 읽기 좋은 테스트?

흔히 볼수있는 설명서의 구성 요소는 다음과 같습니다.

1. 준비물2. 조립 순서3. 결과물

좋은 테스트라면, 내용에서 바로 설명서의 구성을 떠올릴수있어야 합니다.

테스트의 내용물1. 테스트에 필요한 인풋이 있으며(준비물)2. 테스트의 조건과 순서에 따라 진행되며(호출 순서)3. 테스트의 결과값이 예상 값과 동일 해야 합니다(결과물)

setSenderMail 테스트를 구성요소에 빗대어 설명해보겠습니다

테스트가 어떤 과정을 통해 역할을 수행하며 결과를 만드는지 잘 읽히시나요?

조금 더 나아가서, 주어진 상황 (Context) 에 대한 처리를 찾아보겠습니다.

만약 sender mail이 존재하지 않을때의 테스트 케이스를 아래와 같이 작성했다면 어떨까요?

context("when send mail invalid parameter", () => {
it("should failed send mail", () => {
...
})
})

테스트가 실패하면, “어떤" 상황인지를 이해하는것이 불가능합니다.
성공하더라도, 테스트를 통해 의미하는 점이 무엇인지 이해하기 어렵습니다.

Invalid 라니, 잘못된 형식의 이메일을 넣은건지, 이메일을 안넣은건지… etc

결국 테스트의 중요한 존재 목적중 하나는 봤을때 어떤 상황인지 쉽게 이해할수가 있어야 합니다.

2. 기능의 역할에 충실한 테스트

간단한 예시로, 유저에게 메세지를 보내는 함수를 만든다고 생각 해봅시다.

현재까지는 아무문제가 없습니다.

“메시지를 보내기 전에, 메시지의 내용이 스팸이 아닌지 검사해주세요”

라는 조건이 추가로 요구한다면 어떨까요?

메시지를 보내는 기능에서 추가되는 요구 사항에 따라 점점 변경될것입니다

기존의 sendMessage 함수의 역할- Message를 찾고 유저에게 보내는 함수
요구사항이 추가된후의 sendMessage 함수의 역할- Message를 찾고 Invalid검사, 여러 요구사항의 검사를 추가한 후 유저에게 보내는 함수

결국 처음의 역할은 Message를 찾고 보내는 역할인데 기능이 추가될수록 처음 추구한 기능의 역할을 점점 잃어갑니다 :(

여기서 다시한번 돌아서 생각해보죠:

그런데 만약 위의 코드를 단 한줄만 바꾼다면(?!)

위의 상황은 일어나지 않습니다!!

짠!! 변경된 코드입니다

messageId를 인자로 받는대신 message 자체를 받아오는 변경점입니다

아니 이걸 누가 몰라?

라고 생각 할수 있지만 이 부분은 중요한 점이라고 생각합니다.

이전 문제점은 함수에서 record를 가져오는 로직이 포함되어있기 때문에 테스트 하기 곤란하다는 단점이 있습니다.

만약 이전 코드로 테스트를 하려면 어떻게 해야할까요?

1. Test DB에서 해당 messageId로 미리 record 생성 후 쿼리
2. DB에서 Message를 가져오는 내용 자체를 mocking

음… 다시 이 함수가 어떤 역할을 맡았는지 확인 해봅시다.

결국 sendMessage 함수는 주어진 Message를 User에게 전송하는 역할인 함수인데 다른 기능에 의존하는 코드가 들어가도 될까요?

베스트는 다른 기능에 의존을 가지는 내용을 지우면됩니다!

그럼 결국 기존 테스트에서는 비즈니스 로직만 테스트 하면됩니다.

물론 테스트를 할때 1, 2번은 사용하면 안된다!!! 라는 말은 아닙니다.

그래도 2번 Mocking은 상당히 조심히 접근해야 하는 점인데

Mocking 한다는 말은 결국 설정한 값을 그대로 사용한다 라는 뜻입니다.

이게 왜 조심해야 한다는 점인가요?

mock(Message, "findById").expects("message")

위와 같이 Message.findById의 응닶을 “message”로 Mocking 하면 테스트는 통과합니다

왜냐하면 “message”를 응답한다고 설정했기 때문이죠

여기서 테스트는 통과한다는 의미에 중점 두어야하는데

만약 다음의 상황을 생각 해봅시다.

1. Mocking한 값을 사용하는 테스트가 성공적으로 통과합니다2. 문제가 없다고 생각한 뒤 실제 Production에 배포3. 실제 사용을 할때 Mocking한 함수에서 에러 발생4. 어디서 에러가 발생한건지 테스트를 살펴보지만 테스트는 통과5. 결국 로직을 일일이 찾아본뒤 에러 수정 후 다시 배포
개발자의 심정…. (제 이야기가 아닙니다 ㅠㅠ)

제일 베스트는 Mock 하는 코드를 줄여나가는것이 제일이며,

Mock을 하더라도 Mock한 함수의 테스트는 따로 작성하는것이 필요합니다.

Mock은 안좋은것 이라는 인식이 박히는것 같은데 이에 해명할까 합니다.

하나의 예시를 들어보면,

외부 사이트의 API를 이용해서 작업하는 기능이 있습니다.

이런 상황에서 외부 사이트의 API를 테스트에서 실제로 요청해도 되지만 이런 경우에는 Mock을 하는게 적합하다고 생각합니다.

또한 파일 입출력, 자원 상태 등 어려운 테스트 환경인 경우 Mock은 좋은 선택이라고 생각합니다.

결국 기능의 역할에 충실한 테스트란 테스트 목적에 따라 환경을 구축하고 테스트 하려는 기능의 역할을 실행 하는지에 대한 테스트가 좋다고 생각합니다.

여기서 테스트 목적이란 단위 테스트인지, 통합 테스트인지, E2E 테스트 등등을 말합니다

마치며

빙글에서 이제 막 5개월 차에 들어가며 여태까지 개발을 하며 테스트를 많이 작성하고 또 많이 느낀점들이 많았습니다.

사실 앞서 말한 두가지 조건을 지키는게 어려운점 저도 잘압니다.

실제로 개발 하면서도 수시로 변경하기 때문이죠…

또한 테스트에 정답은 없다고 생각하는데 여러가지 방법론이 있듯이 지금보다 더 좋은 방식이 있을수도 있습니다

그렇기에 지금의 방식이 맞는지 고민하고 팀원의 의견을 듣고 수용하며 변화하려는 노력이 있다면 본인도 모르게 어느새 성장하지 않을까 생각합니다.

빙글은 이런 문제를 함께 풀어갈 사람을 언제나 기다립니다!

--

--