테스트 코드를 왜 작성하는 것인가 ?
첫번째, 디버깅 비용 절감
개발자가 총 개발 소요 시간중 온전히 요구사항에 대한 기능 개발을 하는 시간이 전체의 비중에 어느정도를 차지할까?
애플리케이션은 항상 내결함성을 가지고 있고 좋은 코드와 설계를 갖추더라도 결함의 존재자체를 부정할 수 없다 그래서 기능에 대한 버그는 항시 발생하고 그것을 해결하기 위해 개발자는 디버깅을 꾸준히 할 수 밖에 없다
프로젝트를 진행하다보면 문제를 해결하는 시간보다 문제를 찾기 위한 시간이 더 많이 소비되는 것을 알 수 있을 것이다. 어느 코드 블록에서 어떤 케이스에서 문제가 발생하는지 알기 힘들기때문이다
그렇다면 테스트 코드가 있는 프로젝트라면 어떨까 ?
견고한 테스트를 작성하더라도 완벽하게 결함을 없앨 순 없다
하지만 테스트 코드를 통해 오류를 줄일 수 있고, 빠르게 대처할 수 있다
테스트의 분류
아래의 그림과 같이 범위와 비중에 따라 세분류로 나누어지고 각각의 역할을 가지고 있다
- Unit Test (단위 테스트)
- 도메인 모델과 비즈니스 로직을 테스트, 작은 단위의 코드 및 알고리즘 테스트
- 하나의 모듈을 기준으로 독립적으로 진행되는 가장 작은 단위 테스트로, 여기서의 모듈은 애플리케이션에서 작동하는 하나의 메서드 혹은 기능을 의미한다
- 해당 단위의 기능을 검증하고 예상되는 결과를 확인하는 목적으로 사용된다
- Integration Test(통합 테스트)
- 코드의 주요 흐름들을 통합적으로 테스트하며 주요 외부 의존성(ex. 데이터베이스)에 대해서 테스트
- 여러 개의 모듈이나 시스템 구성 요소가 함께 동작할 때, 이들 간의 상호작용을 테스트하여 전체적인 시스템의 동작이 예상대로 이루어지는지 검증하는 테스트 방법으로 DB에 접근하거나 전체 코드와 다양한 환경이 제대로 작동하는지 확인하는데 필요한 모든 작업을 수행할 수 있다
- e2e Test (인수 테스트, Acceptance Test)
- 최종 사용자의 흐름에 대한 테스트이며 외부로부터의 요청부터 응답까지 기능이 잘 동작하는지에 대한 테스트
- 즉, 사용자 시나리오를 기반으로 작성하여 소프트웨어가 제공해야 할 기능이나 사용자가 어떠한 목적으로 소프트웨어를 사용하는지에 대한 내용을 담고 있으며, 일반적으로 기능 테스트 이후 수행된다
이렇게 구분한 테스트 구간의 경계로 인해 문제가 되는 코드의 범위와 케이스를 빠르게 파악하고 수정할 수 있게 되었다.
테스트로 인해 결함을 최소화시켜 디버깅에 소모되는 시간을 줄어듬으로써, 서비스 개발자가 비즈니스 개발에 집중하면서 생산성 향상에 도움을 주게 된다.
두번째, 코드 변경에 대한 불안감 해소
애플리케이션에서 기능은 단일 하나의 요소(함수, 객체, 도메인)로 이루어지지 않고 여러 요소들이 서로 상호작용하고 협력하여 만들어지기때문에 하나의 기능을 수정할 때 구성된 요소가 다른 기능과 협력하고 있고 영향을 준다
그렇기때문에 회귀 버그를 겪게 되는 것이다
그렇다면 회귀 버그를 완전히 차단하고 예방할 수 있을까
회귀 버그는 예방하는 것이 아니라 관리하고 대처해야 한다
다시 말해 회귀 테스트를 해야 한다
회귀 테스트는 기능 추가나 오류 수정으로 인해 새롭게 유입되는 오류가 없는지 검증한다
테스트 코드는 그때 당시의 기능을 만들기 위해서만 필요한 코드가 아니다
그 이후에
- 요구사항이 변경되어 기존 코드를 수정하거나
- 더 나은 코드를 위해 리팩토링을 하거나
서비스가 지속 가능하게 발전하기 위해 필요한 코드이다
세번째, 더 나은 문서 자료
좋은 코드를 작성하기 위해 잘 읽히는 코드, 가독성 좋은 코드를 작성하기 위해 노력한다
하지만 서비스 복잡도가 늘어남에 따라 코드의 양이 많아지고 코드의 복잡성가 높아지게 된다
이러한 복잡한 요구사항이 중첩되어 있는 기능들은 신규 입사자나 투입자들이 이 코드를 이해하는데 많은 불편함을 겪게한다
보통은 편의를 위해 따로 문서화를 해두지만 쉽게 방치되는 문서화의 문제점이 있다
이 문제점이 발생하는 이유가 무엇일까?
두 대상을 분리하고 물리적인 거리감이라는 이유로 코드의 수정에 맞춰 문서의 최신화가 잘 이루어지지 않는 경우가 많기때문에 소스 코드외의 별도의 문서자료들은 관리 대상으로서 쉽게 제외되기때문에 신뢰하기 어려워진다
테스트를 작성할 때 가장 먼저 명세를 작성하면서 코드의 실제 동작을 기술함으로써 이 코드가 어떤 역할을 가졌는지 이해하는데 도와준다
기존에 중복된 메일이 있는 경우 회원가입을 할 수 없습니다.
문서화 테스트는 기능과 코드를 이해하는 데 도움을 준다
네번째, 좋은 코드는 테스트하기 쉽다
좋은 코드는 “변경하기 쉬운”이라는 형용사를 가지고 있다
이 의미는 약한 결합도를 가지고 있는 코드를 뜻하며 반대로 강결합이 되어있는 코드는 유지비용이 증감되어 저품질코드로 분류되어진다
외부의 영향을 받거나 내부적으로 의존성을 가지고 있는 코드는 변경에 유연하게 대응하지 못하고 재사용하기 어려운 코드들이다. 그렇기때문에 테스트를 작성하기 어려운 코드가 만들어지는 것이다
그렇다고 테스트하기 쉬운 코드가 모두 좋은 코드가 되는 것은 아니다. 하지만 테스트를 작성하면서 하나의 좋은 코드의 지표를 아래와 같이 세울 수 있게 된다
“만약 내가 작성한 코드가 테스트하기 어려운 코드라면 냄새나는 코드일 가능성이 높아”
다섯번째, 테스트 자동화
개발자의 기도메타는 본인이 작성한 코드가 실제 운영 환경에 배포됐을 때 불안감에 휩싸여 기도를 시작한다
하지만 자동화 테스트가 있는 순간부터 우리는 CI에서 우리가 작성한 테스트 코드로 버그가 배포되는 것을 방지할 수 있다.
변경에 대한 내성과 회귀 버그를 지속적인 코드 병합 단계에서 확인할 수 있으므로 소프트웨어의 품질 또한 높여준다
즉, 테스트 코드가 존재함으로써 안전감 있는 프로젝트, 개발자의 모습을 볼 수 있게 된다
코드를 잘 작성하는 방법 with Jest
지금까지는 “테스트 코드를 왜 작성하는 것인가?”에 대해서 5가지 이유를 알아보았다
테스트 코드를 작성해야 되는 이유를 알았다면 이제 테스트 코드를 어떻게 작성해야 하는지에 대해서 알아보겠다
첫번째, 테스트 코드는 DRY 보다는 DAMP 하게 작성하라
개발 원칙 중 DRY(Don’t Repeat Yourself) 원칙을 많이 들어봤을 것이다
개발자는 중복 코드를 싫어해 중복 코드가 보이면 그것을 어떻게든 없애려고 하는 경향이 있다
그렇다면 중복을 줄이기 전에 DAMP하게 테스트 코드를 작성하는 것을 고려해봐야 한다
DAMP 원칙은 Descriptive and Meaningful Phrases의 약자로 의미있고 설명적인 구문을 사용하라는 원칙이다
테스트 코드의 중복을 줄이면서 테스트 간의 결합도를 높일 수 있다 또한, 이전보다 가독성이 떨어져 테스트 코드를 작성하는 사람도 테스트 코드를 파악하는 사람도 어려운 테스트가 될 수 있다
이러한 문제를 해결하기 위해 테스트 코드의 리팩토링은 중복을 줄이는 것이 아니라 더 서술적이고 의미있게 작성하는 방향으로 이루어져야 한다
오히려 무작정 코드의 중복을 줄이면 테스트 간의 격리성이 떨어지고 테스트 본래의 의도가 점점 모호해져 파악을 제대로 할 수 없게 되어지기때문이다
Better :
- 테스트의 중복을 줄이는 것이 아니라 더 서술적이고 의미있게 작성하는 방향으로 리팩터링을 해야 한다
- 테스트는 서로 독립적이고 격리되어야 하기때문에 테스트 수정에 다른 테스트가 영향을 받지 않도록 해야 한다
- DAMP 원칙을 지키면서 중복을 줄이는 방안으로는 테스트 픽스쳐 함수나 클래스등을 사용할 수 있다
두번째, 테스트는 구현이 아닌 결과를 검증하도록 한다
테스트 코드를 작성할 때 빠른 테스트를 위해 모의 객체를 사용할 때가 있다
모의 객체로 mock, spy, stub등을 사용하는 경우 테스트 대상의 구현을 알아야만 테스트를 작성할 수 있다
그런데, 이 부분을 남용하거나 잘못 사용하여 테스트를 작성하는 경우가 종종 있다
내부 구현이나 비공개(private) 메소드들은 언제든지 바뀔 여지가 있는 코드이기때문에 정보의 은닉을 위해 숨기는데 다시 정보를 꺼내여 테스트하는 것은 좋지 않다
테스트 코드는 내부 구현보다는 실행 결과에 집중하는 것이 리팩토링 내성을 높일 수 있다
지나친 모의 객체 남용과 내부 구현 검증은 오히려 리팩터링의 내성을 떨어뜨리고 유지보수를 더 어렵게 만든다
우리는 어떻게 라는 내부 구현 검증보다는 무엇을 이라는 결과 검증에 집중하여 테스트 코드를 작성해야 한다
Better :
- 테스트 코드는 내부 구현이 아닌 실행 결과에 집중해야 한다
- 의미 있는 테스트, 검증을 하자
세번째, 읽기 좋은 테스트를 작성하라
테스트 코드는 메인, 제품 코드을 위한 코드라 할지라도 우리의 책임이고 관리 대상이다
테스트 코드도 가독성이 좋아야하고 불명확한 테스트 코드는 읽는 행위도 유지보수 하는 행위도 어렵게 만든다
좋은 테스트 코드는 읽는 사람 입장에서 이 테스트를 이해하는데 필요한 모든 정보를, 테스트 케이스 본문에 담고 있는 테스트를 말한다
그리고 이와 동시에 관련없는 정보를 담지 말아야 한다
또한 테스트 코드의 가독성을 높이는 방법 중 하나는 테스트의 구조를 잘 잡는 것이다
테스트의 구조는 준비, 실행, 검증 3개의 구절로 나뉘어질 수 있다
각각의 구절을 AAA패턴(Arrange-Act-Assert 주석으로 구분)이나 GWT 패턴(given-when-then 행위 주석으로 구분)으로 구간을 나누어 작성하는 것이 좋다
*물론 구조를 위한 주석을 사용하지 않고 각 영역에 맞게 테스트 함수 내 코드를 구분해서 작성할 수 있다면 그 방안을 사용하는 것이 좋다
그리고 만약 테스트 안에 너무 많은 양의 코드가 존재한다면 이 부분을 좀 더 코드 재사용의 목적으로 모듈화(테스트 팩토리, 빌더, 헬퍼 메소드)해두면 쉽게 읽힐 수 있게 된다
Better :
- 테스트 코드도 코드이므로 읽기 쉬운 코드를 작성하자
- 무엇이 잘못되어 실패했는지, 어떻게 고쳐야 하는지를 파악하기 어려운 테스트를 작성하지 말자
- 테스트 구조를 나누는 패턴과 테스트 코드를 모듈화하는 방법을 통해 테스트 코드를 읽기 쉽게 작성하자
네번째, 테스트 명세에 비즈니스 행위를 담도록 한다
마지막으로 테스트 명을 작성할 때도 유의할 점이 있다
테스트명은 퍼블릭 인터페이스와 마찬가지로 명확하게 의도가 들어나도록 작성이 되어야 한다
테스트 코드 작성 이유 중 좋은 테스트 코드는 우리가 작성한 코드의 문서이기에 명확한 의도가 담긴 명세가 되어야 한다
여기서 명확하게 의도가 드러나는 이름은 개발자의 용어가 아닌 비즈니스 행위를 담은 비개발자가 읽을 수 있게 설명되어야 한다
Bad:
if(’관리자를 생성한 후 관리자 정보를 확인한다.’)
Better:
if(’관리자 정보로 가입한다’)
이렇게 잘 작성된 이름은 다음과 같은 효과를 가지고 온다
- 테스트 코드와 실행 로그를 확인하지 않아도, 동작을 유추할 수 있으며, 요구사항이 빠르게 공유가 될 수 있다
- 영향 범위를 빠르게 파악할 수 있으며 유추를 통하여 빠른 복구를 할 수 있다
테스트 코드 작성이 어려운 이유
하지만 테스트 코드를 작성하는 것은 쉽지 않은 작업이며 시간과 비용이 많이 든다
- 개발자들은 주로 코드 작성에 집중한다
- 단위 테스트의 경우 테스트 케이스 수가 많아질 수 있으며, 작성하고 관리하는데 쉽지 않다
- 코드 수정 및 리팩토링이 발생할때마다 테스트 코드도 같이 변경해주어야 한다
- 테스트 코드를 작성하면서 발생하는 문제점도 해결해야 한다
마무리
테스트 코드는 시간과 비용이 들더라도 “지속 가능한 서비스, 프로젝트”를 위해 필수적인 요소 중 하나이다
간단한 프로젝트에서는 테스트 코드의 영향을 확인하긴 힘들겠지만 서비스를 제공하는 회사
즉, 실무에서는 서비스, 기능등을 올바르게 수행하여 서비스를 이용하는 사용자들에게 제공해주어야 하기때문에 더욱 더 실무에선 테스트 코드의 중요성을 알고 투자를 하고 있을 것이다.
뿐만아니라 문서의 역할도 하기에 혼자가 아닌 팀으로 개발 할 경우 테스트 코드는 코드의 생산성과 개발시간의 단축을 시킬 수 있는 중요한 작업 중 하나이다
테스트 코드를 왜 그리고 어떻게 작성해야 할까?
테스트 코드가 필요한 이유와 잘 작성하는 방법에 대해 공유합니다.
tech.inflab.com
'TIL' 카테고리의 다른 글
MSA (0) | 2023.07.03 |
---|---|
Redis ? (0) | 2023.05.26 |
FCM(Firebase Cloud Messaging) (0) | 2023.05.26 |
[GreenCherry/React, Spring boot, PWA, FCM] FCM을 활용한 백그라운드에서 알림 구현 (0) | 2023.05.25 |
[JPA] OSIV와 성능 최적화 (0) | 2023.03.22 |