테스트 코드에 대한 이해를 위하여

J쭈디·2025년 2월 24일
0

테스트 코드를 짜면서 자꾸만 시간에 쫓겨서 코드만 짜고 내부에 대한 공부가 부족하다는 위기감이 생겨서 공부를 면밀히 다시 하기로 했다. 일단 내가 아는 거, 단통시인...

단위테스트, 통합테스트, 시스템 테스트, 인수테스트

여기서 나는 각기 테스트의 사용 목적과 차이도 너무 추상적으로 알고 있어서 공부를 제대로 해보려고 한다.

1. 테스트 코드의 작성 이유

현직 개발자가 아닌, 취준생이 공부하면서 수집한 자료이므로 정확도가 떨어질 수 있고, 저의 경험 기반의 내용도 있지만 제가 경험하지 못한 부분에 대한 설명들도 다수 포함되어 있습니다. 그러한 부분은 다른 개발자 분들의 글들을 보고 작성된 저의 '예측'에 불과하다는 점 참고 부탁드립니다.

1. 디버깅 비용 절감

개발에 있어서 디버깅 비용이란 단순히 금전적인 비용이 아닌 시간, 노력, 자원 등을 말한다. 개발할 때 버그가 발생하게 되면 버그를 해결하는 데 걸리는 시간보다도 그 버그가 어디서 터졌는지 찾는 시간이 더 걸리는 경우가 왕왕 있다. 개발자를 지망하는 나만 해도 디버그 모드로 버그를 찾는데만 40번 이상의 클릭을 해서 넘기고 넘기고 넘겨서 버그의 원인을 겨우 찾은 적도 있다.

하지만 테스트 코드를 사용한다면 이러한 디버깅 비용을 절감할 수 있게 될 수 있다고 한다. 결함이나 에러 발생률이 0%가 되는 것은 아니지만 테스트 코드를 단계별로 짜 둠으로서 어디에서 문제가 발생하는지 상대적으로 빠르게 찾을 수 있게 된다는 것이다.

만약에 내가 로직 중 A B C D를 개발했다고 치면, 단위테스트로 A, B, C, D를 따로따로 테스트해보는 것만으로도 4개 중 어떤 로직에서 문제가 발생했는 지 찾을 수 있게 되는 것이다.

이러한 특징으로 인해 개발자는 버그 파악하는 시간을 절감하고 그 시간동안 비즈니스 로직을 더 개발할 수 있게 되어 생산성까지 향상될 수 있다고 한다.

2. 코드 변경 시 버그확률 낮춤

개발을 하다보면 버그를 고쳤는데 고친 버그보다도 더 많은 양의 버그가 나타날 수도 있다. 이는 어플리케이션의 기능은 여러 요소들이 서로 상호작용을 하고 있기 때문이다. 나는 한 개를 고친 것이지만 그것과 관련된 기능은 10개가 되기도 하기 때문이다.

실제로 나 또한 내 코드를 불과 3~4줄 고쳤는데 그게 메인 로직이라서 다른 사람의 코드를 20번 넘게 고치게 되는 경우도 경험했었다.

만약 테스트 코드를 작성하게 된다면 이러한 버그의 확률을 낮춰줄 수 있다. 회귀테스트를 추가하게 되면 기능을 추가하고 버그를 수정했을 때 그로 인해 유입되는 또 다른 오류가 있는지 검증해준다.

회귀 테스트(Regression Test)란 이미 테스트코드가 작성된 프로그램의 테스팅을 반복하는 것으로, 변경된 모듈이나 컴포넌트에서 새로운 버그가 있는지 확인하는 테스트이다.

3. 문서의 역할을 해준다.

코드를 리팩토링하고 개선을 하다보면 처음에 기술했던 문서화에서 많은 게 변동되곤 한다. 그러나 개발하고 문서화하는 것과 달리, 코드를 변경하면서 함께 문서화까지 변경하는 일은 쉽지 않다.

이럴 때 테스트 코드가 문서의 역할을 대신할 수 있다. 테스트 코드는 작성할 때 실제 동작을 기술하기 때문에 코드의 역할을 직관적으로 이해하기 쉽기 때문이다.

4. 좋은 코드인지 테스트로 확인이 가능하다.

변경하기 쉬운, 결합도가 낮은 코드를 좋은 코드라고 한다. 결합도는 낮고, 응집도는 높여야 한다고 말은 알고 있어도 실제로 이러한 문제를 바로 발견하기가 그리 쉽지는 않다.

그런데 테스트 코드를 작성하다보면 강결합 코드일 경우 자연스럽게 코드의 결합도 문제가 있다는 걸 발견할 수 있게 된다. 왜냐하면 결합도가 높으면 높을 수록 테스트 코드 작성 자체가 어려워지기 때문이다.

나만해도 며칠 전까지 결합도가 너무 높은 코드를 테스트코드로 작성하려다 아예 코드 자체를 뜯어 고쳤었다. 사실 나의 현재 실력으로는 여기서 말하는 좋은 코드를 작성하긴 힘든 수준이기 때문에 이 부분은 다른 분들의 경험 기반으로 보이는 글들을 참고했다.

5. 테스트 자동화

만약 코드가 테스트 코드 없이 실제 운영환경에 배포가 된다면 개발자들은 그 코드가 잘 돌아갈지 의문을 가지고 배포를 해야한다 그런데 테스트 코드를 작성하게 된다면 CI에서 버그가 배포되는 걸 방지하기 때문에 비교적 안전한 배포가 가능해진다.

CI란 Continuous Integration의 약자로 커밋을 할 때마다 빌드와 테스트가 이루어져 동작을 확인하는 것을 말하며, 변경으로 인해 버그 등이 발생하는 걸 방지하는 역할을 한다.

<Github Action CI 예시>
1. 개발자가 코드 커밋, 풀리퀘 작성
2. Github Action이 자동으로 해당 풀리퀘의 테스트 코드를 실행
3. 테스트 결과가 통과될 때만 머지 가능
4. 실패 시 풀리퀘에 실패라고 떠서 빠른 수정이 가능

2. 테스트 코드의 종류 및 특징

1. 단위 테스트 (Unit Test)

1-1. 단위 테스트 관련 세부 테스트

  • 기능 테스트 (Functional Test)
    • 특정 메서드나 기능이 올바르게 작동하는지 확인.
  • 경계값 분석 테스트 (Boundary Test)
    • 입력 값의 최대/최소 경계를 검증.
  • 예외 테스트 (Exception Test)
    • 예외 상황에서의 메서드 동작 검증.
  • Mock 테스트
    • 외부 의존성을 Mocking하여 비즈니스 로직만 검증.
    • Mocking이란 실제 객체가 아닌 가짜(Mock) 객체를 이용하는 것으로 외부 DB나 API에 의존 없이 테스트에만 집중하는 것이 가능하다.
    • Mocking 사용 시 가짜 객체를 사용하므로 테스트 후에도 실제 DB에는 영향이 가지 않는다.
    • Mocking을 사용하면 임의로 가짜객체를 가볍게 만들 수 있기 때문에 테스트 속도도 개선할 수 있다.

1-2. 단위 테스트 도구 및 프레임워크

  • JUnit
    • 자바에서 가장 널리 사용되는 테스트 프레임워크
    • 어노테이션을 이용하여 테스트 메서드를 정의, 실행 가능
  • Mockito
    • 자바에서 사용되는 Mocking 프레임워크
    • 의존성 있는 객체를 가짜 객체(Mock객체)로 대체하여 테스트 가능
  • AssertJ
    • 유창한(fluid) API를 제공하는 어서션(assertion) 라이브러리
    • 유창하다는 건 "문장을 읽듯이 자연스러운 API 스타일"을 말한다.
    • 예를 들자면 아래와 같은 스타일이다.
      assertThat(user.getName()).isNotNull().isEqualTo("홍길동").startsWith("홍");
      유저 이름이 null이 아니며, 홍길동이고, 글자 시작점이 홍이냐고 하는 문장 느낌이 나긴 한다.
    • 가독성 높은 테스트 코드 작성 가능

2. 통합 테스트 (Integration Test)

2-1. 통합 테스트 관련 세부 테스트

  • Mock 통합 테스트
    • 일부 모듈만 실제 객체를 사용하고 나머지는 Mocking.
  • 데이터베이스 통합 테스트
    • 실제 데이터베이스 연결을 통해 CRUD 동작을 검증.
  • API 통합 테스트
    • 외부 API나 내부 마이크로서비스 간의 연동 검증.
  • 메시지 브로커 테스트
    • Kafka, RabbitMQ와 같은 메시징 시스템과의 통합 검증.

2-2. 통합 테스트 도구 및 프레임워크

  • SpringBootTest
    • 스프링 부트 기반 애플리케이션의 통합 테스트를 지원하는 어노테이션
    • 실제 구동 환경과 유사한 컨텍스트에서 테스트를 실행 가능
  • Testcontainers
    • 도커 컨테이너를 활용하여 테스트 환경을 구축할 수 있게 해주는 자바 라이브러리
    • 데이터베이스나 메시지 브로커 등의 실제 환경과 유사한 테스트를 가능케 한다.
  • MockMvc
    • 스프링 MVC 테스트를 위한 프레임워크
    • 서블릿 컨테이너를 구동하지 않고도 컨트롤러의 동작을 테스트 할 수 있다.

3. 시스템 테스트(System Test)

3-1. 시스템 테스트 관련 세부 테스트

테스트 대상: 애플리케이션의 전체 시스템이 요구사항을 충족하는지 검증.
세부 테스트:

  • E2E 테스트 (End-to-End Test)
    • 사용자의 관점에서 시스템의 전체 흐름을 검증.
  • 성능 테스트 (Performance Test)
    • 시스템의 속도, 응답 시간, 부하 처리 능력을 검증.
  • 회귀 테스트 (Regression Test)
    • 시스템 업데이트 후 기존 기능이 문제없이 동작하는지 확인.
  • 안정성 테스트 (Stability Test)
    • 장기간 구동 시 시스템이 안정적으로 작동하는지 검증.
  • 부하 테스트 (Load Test)
    • 동시 사용자 수 증가 시 시스템의 성능을 확인.
  • 스트레스 테스트 (Stress Test)
    • 시스템에 한계치를 초과하는 부하를 가해 최대 허용치를 평가.

3-2. 시스템 테스트 도구 및 프레임워크

  • Selenium
    • 웹 애플리케이션의 E2E 테스트를 위한 프레임워크
    • 다양한 브라우저에서 자동화된 테스트 수행 가능
  • Cypress
    • 모던 웹 애플리케이션을 위한 빠르고 신뢰성 높은 테스트 프레임워크로, E2E 테스트에 주로 사용됩니다.
  • Playwright
    • 오픈 소스 E2E(End-to-End) 테스트 프레임워크
    • 웹 애플리케이션의 E2E 테스트를 위해 사용됨
    • 다양한 브라우저에서 사용자 시나리오를 자동화 가능

3-3. 성능 테스트 도구 및 프레임워크

  • JMeter
    • 아파치에서 제공하는 성능 테스트 도구
    • 다양한 프로토콜에 대한 부하 테스트를 수행 가능
  • Gatling
    • 고성능의 부하 및 성능 테스트를 위한 도구 (스칼라 기반)
  • Locust
    • 파이썬(Python) 기반의 오픈 소스 부하 테스트(Load Testing) 도구
    • 웹사이트나 시스템에서 가상 사용자를 시뮬레이션하여 성능, 부하 테스트 수행 가능

3-4 부하/스트레스 테스트 도구 및 프레임워크

  • Apache Benchmark
    • Apache HTTP Server와 함께 제공되는 간단한 성능 및 부하 테스트 도구
    • 웹 서버의 응답 성능 측정 시 사용됨
    • HTTP 요청을 일정량 발송하여 응답 속도를 측정 가능
    • 동시 접속 수와 총 요청 수를 설정하여 테스트 가능
    • 응답시간, 처리량, 전송속도 등의 실시간 통계 제공
  • Artillery
    • JSON 또는 YAML 형식의 설정 파일을 통해 테스트 시나리오를 정의
    • JavaScript 스크립트를 활용하여 복잡한 테스트 시나리오 구현 가능
    • HTTP뿐만 아니라 WebSocket을 지원하여 실시간 통신 애플리케이션의 성능 테스트도 가능
    • HTML 형식의 시각적인 테스트 결과를 제공

4. 인수 테스트(Acceptance Test)

4-1. 인수 테스트 관련 세부 테스트

테스트 대상: 시스템이 최종 사용자의 요구사항을 충족하는지 검증.
세부 테스트:

  • 사용자 시나리오 테스트 (User Scenario Test)
    • 실제 사용 사례(예: 로그인 → 장바구니 → 결제)를 테스트.
  • UI/UX 테스트
    • 화면의 일관성 검증
    • 사용자 경험(UX)을 검증.
  • 베타 테스트
    • 실제 사용자가 시스템을 사용해 보고 피드백을 받는 단계.

4-2. 인수 테스트 도구 및 프레임워크

  • Cucumber
    • 자연어로 작성된 시나리오를 기반으로 테스트를 실행할 수 있게 해주는 프레임워크로, BDD(Behavior Driven Development)에 사용됩니다.
  • Rest Assured
    • 자바에서 RESTful 웹 서비스의 테스트를 쉽게 작성할 수 있게 해주는 라이브러리입니다.
  • Postman
    • API 테스트를 위한 도구로, HTTP 요청을 손쉽게 구성하고 응답을 검증할 수 있습니다.

3. 테스트 코드 작성 시 확인할 점

1. 테스트 코드 작성 팁

  • Given(준비), When(실행), Then(검증) 3단계로 구성
  • 테스트 메서드 명은 축약하지 않고 구체적으로 작성
  • 테스트는 독립적으로 실행이 가능해야 함, 다른 테스트에게 영향을 받으면 안됨
  • 테스트 전/후 초기화 및 정리 작업 필수
  • Mocking과 Stubbing을 적절히 사용
    • Mocking은 객체의 행위를 가로채서 원하는 대로 동작하게 만든다.
    • Stubbing은 특정 메서드 호출에 대해 미리 정해진 값을 반환하도록 설정하는 것이다.
  • 테스트 코드의 리팩토링 및 유지보수 잘 하기
    • BeforeEach나 BeforeAll 등의 어노테이션으로 공통 로직을을 분리하기
    • 중복 코드를 줄이고 가독성을 높이기 위한 리팩토링 필수
    • 테스트 데이터는 고정 값이나 Mocking을 사용하여 데이터 제어가 가능해야 한다.
    • 테스트 코드가 조건에 부합하지 않으면 빠르게 Fail 되도록 설정하기

2. 테스트 코드 작성 시 주의사항

  • 비즈니스 로직을 코드에 구현하지 않는다. (only 검증을 위한 코드)
  • 외부 환경에 의존하지 않게 한다.
    • 의존성 주입이란 하나의 객체가 다른 객체에게 도움을 받기 위해 사용하는 방 생성자나 Setter 대신, 어노테이션으로 직접 의존성을 부여해주는 것이다.
    • 외부 의존성을 낮추고, Mocking이나 AutoWired 등으로 객체 내부에서 직접 의존성을 주입해주면 외부 환경의 영향 없이 테스트 코드의 독립성을 높일 수 있다.
    • 네트워크나 DB에 따라 결과가 바뀌지 않게 함
    • Moking을 활용하여 테스트 코드 내부에서 해결되도록 함
  • 불필요한 테스트 코드 작성 지양
    • Setter나 Getter마냥 단순 메서드는 테스트하는 것을 지양하기
    • 테스트 코드가 본 코드보다 길어지는 모순이 발생할 수 있기 때문

4. 결론

테스트 코드를 작성하는 요령은 다양하지만 일단은 나의 현재 통합테스트 로직은 독립적인 테스트 코드 환경이 아니기 때문에 이 부분을 개선해야할 지, 아니면 통합테스트라면 괜찮을 지 한 번 더 체크해야 한다.
그리고 단위 테스트에는 Mock 객체를 사용하여 독립적인 구성으로 테스트 코드를 작성해야 한다.

<출처>
https://tech.inflab.com/20230404-test-code/
https://velog.io/@dahunyoo/Regression-Test
https://doding.tistory.com/14
https://www.2e.co.kr/news/articleView.html?idxno=301002
https://velog.io/@ecvheo1/Test-Test-Code%EB%8A%94-%EC%99%9C-%EC%9E%91%EC%84%B1%ED%95%B4%EC%95%BC-%ED%95%98%EB%8A%94%EA%B0%80
https://yozm.wishket.com/magazine/detail/1964/
https://tech.kakaopay.com/post/mock-test-code/

profile
언제 어느 위치에 있더라도 그 자리의 최선을 다 하는 사람이 되고 싶습니다.

0개의 댓글