6 분 소요

References


📌 테스트로부터 얻을 수 있는 것

안정감과 자신감

  • 테스트는 현재와 미래의 나, 현재와 미래의 동료에게 안정감을 부여
  • 테스트는 짧은 주기로 피드백을 주기 때문에 프로그래머가 안정감, 자신감을 얻고 구현을 이어나갈 수 있게 만들어준다


📌 테스트 하기 쉬운 것과 어려운 것 구분하기

모든 코드를 테스트하는 것은 어렵다. 어떤 코드가 실행되기 위해서 DB와 연결이 필요하고, 입력을 받기 위해 IO 로직이 포함되어 있다면 더욱더 테스트하기 어려울 것이다.

특정 기능을 수행하는 코드를 작성할 때, 같은 동작을 하더라도 테스트가 어려운 코드가 있는 반면, 테스트하기 쉬운 코드가 있다. 테스트하기 쉬운 코드를 만들기 위해서는 테스트 가능한 것과 어려운 것을 구분할 수 있어야 한다.

테스트 하기 쉽다는 건?

테스트하기 쉽다는 것은 테스트 코드가 짧고, 단순히 빨리 작성할 수 있는 코드를 의미하지 않는다. 테스트하기 좋은 코드는 몇번을 수행해도, 어떤 순서이든 항상 같은 결과를 반환하는 멱등성이 보장되는 코드이다.

어떤 상황에서라도 같은 결과를 반환하기 위해서는 피해야할 2가지 요소가 있다

  • 제어할 수 없는 값에 의존하는 경우
    • Random(), DateTime 클래스 등에 의존하게 되면 실행할 때마다 결과가 달라진다
    • IO를 통합 입력에 의존하게 되면 실행할 때마다 입력에 따라 결과가 달라진다
  • 외부에 영향을 주는 경우
    • 외부 API에 의존하는 경우
    • 데이터베이스에 의존하는 경우

보통 테스트 코드를 프로젝트에 적용하기 시작할 때 도메인 로직에 대한 단위 테스트로 시작할 것을 추천한다. 그 이유는 도메인 로직은 외부와 아무런 연관 관계를 맺지 않고, 순수하게 비즈니스적으로 매우 중요한 결정만 내리기 때문이다. 도메인 로직이 잘 동작하는지 테스트하는 것도 매우 중요하지만, 이들은 외부와 철저히 분리되어 항상 같은 결과를 반환하는 순수한 함수이기 때문이다.

테스트 하기 어렵다는 건?

테스트가 어렵다는 것은 테스트가 불가능하다란 뜻이 아니다. 테스트 하기 쉬운 코드보다 몇 배의 비용이 든다는 것을 의미한다. 만약 시간이 남아 돈다면 테스트가 어려운 코드도 수십 시간을 투자해 능히 테스트할 수 있다.

하지만 우리가 왜 테스트 코드를 작성하는지 기억하길 바란다. 테스트를 작성하는 이유 중 하나는 우리가 작성한 코드를 빠르게 피드백하기 위해서다. 이런 어려운 코드를 테스트하는 이유가 단지 코드가 잘 동작하는지 확인하기 위해서, 테스트 커버리지를 높여 만족감을 얻으려는 이유라면 테스트 코드 작성을 다시 고려해봐야 한다.

또 테스트 프레임워크 사용이 익숙하지 않아서 테스트가 어려운것만은 아니다. 테스트가 어려운 코드의 대부분은 테스트를 작성하기 어렵게 구현되었기 때문이다.

불확실성: 제어할 수 없는 값에 의존한다

우리가 제어할 수 없는 것을 제어하려는 것은 의미가 없는 일일 가능성이 높다.

테스트 대상 코드가 테스트가 가능하려면 같은 입력 값에 항상 같은 결과를 반환해야 한다. 예를 들어 현재 시간을 기준으로 오전인지 오후인지 알려주는 메소드가 있다 가정한다. 이 메소드는 호출되는 시각에 따라 결과가 결정되어 결과 값에 확신을 가질 수 없다. 즉 테스트가 불가능하다. 불확실성은 테스트를 어렵게 만드는 첫 번째 요인이다. 랜덤값, 임의시각은 테스트를 어렵게 만드는 불확실성의 대표적인 예이다.

public static String getMorningOrAfternoon() {
    LocalDateTime now = LocalDateTime.now();

    if (now.getHour() < 12) {
        return "오전";
    }
    return "오후";
}

이 메소드를 오후에 테스트한다 가정하면 결과값으로 “오후”를 넣어 성공적인 테스트를 진행할 수 있다. 그러나 다음날 자고 일어나 오전에 테스트를 실행하면 실패하는 테스트가 된다.

불확실성은 랜덤값, 임의의 시간 뿐만 아니라 더 다양한 경우에서도 존재한다

  • 전역변수
  • local 환경에 존재하는 파일
  • DB의 특정 레코드
  • 외부 API를 통해 응답받는 내용
    • HTTP 통신

이 값들은 공통된 특징이 있다. 외부에서 제공하는 값에 의존한다는 것이다. 랜덤값, 임의의 시간도 자바에서 제공하는 API를 사용하더라도 값을 외부에서 주입받는 것이다.

부수효과(side effects)

테스트를 어렵게 만드는 두 번째 요인은 부수효과이다. 부수효과는 나비효과처럼 코드의 작은 행동이 외부에 영향을 미치는 것을 의미한다.

부수효과를 갖는 메소드는 외부에 영향을 가하지만 특정한 리턴 값을 가지지 않는 특징이 있다. 예를 들어 메일을 발송하는 메소드도 메일을 발송한 후 아무런 값을 반환하지 않는다. 여기에 메일 발송에 성공했다는 ok 사인을 반환해도 메일을 발송했다는 것만 확인이 되었지, 정확한 수신자에게 정확한 내용을 전달했는지는 확인할 수 없다. 이 때문에 부수효과를 갖는 메소드는 정확한 검증을 위한 테스트를 작성하는데 매운 큰 비용이 든다.


📌 테스트 하기 좋은 코드를 작성하는 방법

실무에서는 테스트하기 쉬운 코드만 존재하지 않는다. 물론 경험 많은 개발자가 도메인을 아름답게 설계했다면 도메인 로직에 대한 테스트는 Mock 객체에 의존하지 않아도 매우 쉽게 테스트할 수 있다. 하지만 결국 실제 서비스를 제공하기 위해서는 DB에 저장을하고, IO를 통해 값을 입력받고, 외부 API와 통신을 거쳐야 한다. 따라서 우리는 테스트하기 어려운 코드를 피할 수 없다. 그렇다면 우리는 이를 최대한 줄이려고 노력해야 한다. 즉, 테스트하기 쉬운 코드를 가능한 많이 작성해야 한다는 의미다.

테스트하기 쉬운 코드와 어려운 코드 분리

예를 들어 이름과 아이디, 비밀번호를 입력받아 회원가입을 하는 서비스를 제공한다 가정하자. 이 메소드 안에는 IO를 통해 값을 입력받아 이름, 아이디, 비밀번호를 검증하는 메소드도 들어있고 값들을 DB에 저장하는 로직도 존재한다.

여기서 우리는 이름, 아이디, 비밀번호를 검증하는 로직을 테스트하고 싶다. 하지만 테스트를 작성하려면 매우 비용이 높다.

이런 경우 테스트하기 쉬운 코드로 분리해 새로운 타입을 만들면 좋다. 이름, 아이디, 비밀번호를 단지 문자열로 받지 않고 포장해 일급값으로 사용하면 테스트하기 좋은 코드로 변한다.

테스트하기 어려운 코드는 가장 바깥 쪽에 위치

A, B, C 세 개의 메소드가 존재한다고 가정한다. C 메소드를 실행하면 C는 B 메소드를 호출하고, B 메소드는 A를 호출한다. 여기서 A 메소드가 입출력 장치를 이용해 값을 입력받는 메소드이면 어떻게 될까?

현재 호출 흐름을 보면 C → B → A 순으로 되어있다. 여기서 발생하는 영향은 호출 흐름과 반대로 A → B → C로 전파된다. 즉, 제일 안쪽에 테스트하기 어려운 코드가 존재하면 그 영향이 나머지 메소드들도 테스트하기 어렵게 만든다.

테스트 하기 좋은 코드로 만들려면 A 메소드를 분리시켜 다른 곳에 위치시켜야한다. A 메소드를 프로그램 진입점에 위치시키면 나머지 B, C 메소드는 테스트가 가능해진다. 비록 3개의 메소드를 모두 테스트하기 쉬운 코드로 만들지 못했지만, 나머지 2개의 유의미한 결과를 냈다는 것에 집중하자. 실무의 프로젝트는 항상 테스트하기 어려운 코드가 존재한다는 걸 잊지 않아야 한다.


📌 무엇을 테스트 할 것인가

테스트는 항상 성공할 수 있는 것, 항상 동일한 결과가 나올 수 있는 멱등한 것을 대상으로 삼는다. 테스트 할 수 없는 대상은 과감히 진행하지 않아야 한다.

구현이 아닌 설계를 테스트해야 한다

  • 구현은 언제든 변할 수 있음
    • 6개의 중복 없는 숫자를 만들기 위해 Set을 사용했다고 중복 검증에 대한 테스트를 진행하지 않아도 되나?
    • 추후 Set 대신 다른 로직으로 구현이 변경될 수 있음 → 이런 변경이 발생해도 오류 없이 비즈니스를 제공할 수 있어야 한다
  • 설계를 중점으로 테스트해야 한다
    • 6개의 숫자를 제공한다
    • 중복이 존재하면 안된다
      • Set을 이용해 구현해도 설계 상 중복이 존재하면 안된다고 정했기 때문에 테스트가 무조건 필요함

제어할 수 없는 값 분리

  • 제어할 수 없는 값을 최상단 Boundary Layer로 옮겨야 한다
    • 컨트롤러 같은 커다란 한 서비스(기능)의 진입점(모듈의 진입점)으로 제어할 수 없는 값을 옮긴다

단위 테스트부터 시작

  • 단위 테스트는 소프트웨어 시스템의 작은 부분들에 집중하는 low level
  • 일반적인 테스트 툴(자바는 JUnit, AssertJ)을 사용해 프로그래머들이 직접 작성하는 테스트
  • 다른 테스트 방식들에 비해 확실하게 빠른 테스트
  • 객체지향 프로그래밍에서의 단위는 클래스를 하나의 유닛으로 삼는다
  • 단위를 정할 때 중요한 부분은 테스트를 진행할 때 단독으로 진행할지 또는 협동적으로 진행할지 정의하는데 있다
    • 만약 협동으로 진행한다면 테스트더블(TestDouble)을 사용해 대체한다
    • 테스트더블이란 Mock, Stub, Fake 등 가짜로 대체해 사용하는 것을 의미한다

테스트에 익숙하지 않다면 또는 경험이 부족하다면 클래스를 단위로 삼고 단독으로 진행하는 테스트를 진행한다.


📌 효과적인 TDD

TDD를 사용하는 이유

  • 변화에 대한 두려움을 줄여준다
    • 리팩토링시 빠른 성공 피드백을 받을 수 있다
  • 디버깅 시간을 줄여준다
  • 동작하는 문서 역할을 한다
  • 오버 엔지니어링을 방지한다
    • 현재 필요한 만큼만 구현할 수 있다

우리의 TDD가 실패하는 이유

  • 코드가 이루고자 하는 가치나 기능을 테스트하기보다 그 기능을 어떻게 구현하고 있는지를 테스트한다
  • 따라서 테스트 케이스들이 구현체와 결합도가 높아진다
  • 구현체들을 리팩토링하면 결합되어있는 테스트 케이스들이 모두 깨져버린다

테스트 범위

  • 통합 테스트: 여러 작업 단위가 연계된 워크플로우를 테스트하기 위한 수단
    • 객체 간 협력을 테스트
    • 서비스 간 협력을 테스트
    • 시스템 간 협력을 테스트
  • 기능 테스트: 공개된 API의 가장 바깥쪽에 해당하는 코드 검사
    • 컨트롤러 호출
  • 부하 테스트: 주어진 단위 시간 동안 애플리케이션이 얼마나 많은 요청을 처리할 수 있는지 검사
  • 인수 테스트: 정의되어진 모든 목적에 부합되는지 확인해보고자 하는 검사

단위 테스트(Unit Test)

  • 가장 작은 단위의 테스트
  • 일반적으로 메소드가 한 단위
  • 검증이 필요한 코드에 대해 테스트 케이스를 작성하는 절차 또는 프로세스
  • 단위 테스트는 테스트 코드가 목적 코드의 완전성을 입증 해주기 때문에, 테스트 코드 그 자체만으로 주요한 가치가 있다

F.I.R.S.T: 좋은 단위 테스트를 만드는 법칙

  • F(Fast): 단위 테스트는 빨라야 한다. 빠르게 결과를 내고 피드백을 받아야 한다
  • I(Independent): 각각의 단위 테스트는 독립적이여야 한다. 각 테스트가 서로 의존 하면 안된다
    • 시간과 상관없이 성공해야 한다
    • 순서와 상관없이 성공해야 한다
  • R(Repeatable): 단위 테스트는 어떤 환경에서든 반복가능해야 한다
    • 네트워크가 없어도 성공할 수 있어야 한다 → 외부 API, DB 등에 의존하지 않아야 한다
  • S(Self-Validating): 단위 테스트는 무조건 성공, 실패 중 하나의 결과를 반환해야 한다
  • T(Timely): 단위 테스트는 항상 적절하게(적시에) 구현되어야 한다
    • 프로덕션 코드가 먼저 구현이 완료되고 테스트를 하면 테스트가 어려울 수 있다


📌 ATDD(Acceptance TDD)?

  • 인수 테스트 주도 개발
  • 인수 테스트: 시나리오(사용자 스토리) 기반으로 기능 테스트

ATDD의 장점

  • 배포 없이 빠른 피드백이 가능
    • 사용자 시나리오 대로 동작하기 때문
  • 도메인 이해가 빨라짐

인수 조건 정의

  • 인수 조건: 인수 테스트가 충족해야하는 조건
    • 시나리오 기반의 표현 방식을 사용할 수 있음
    • ex: given-when-then 구조
  • 검증 하고자 하는 when 구문 작성 → 기대 결과를 의미하는 then 구문 작성 → when과 then에서 필요한 정보를 given을 통해 마련

댓글남기기