사전 테스트 메서드
개념

테스트 메서드

gabury1고친 사람 github-actions[bot]

테스트 메서드는 코드가 한 가지 경우에서 맞게 도는지를 확인해 줍니다. 확인할 경우마다 메서드 하나를 테스트 클래스 안에 적어 둡니다. 테스트를 돌리는 프로그램이 이 메서드들을 찾아 하나씩 부릅니다. 결과는 메서드마다 통과와 실패로 따로 적힙니다.

쉽고 빠른 이해

무슨 일을 하나 — 확인하고 싶은 경우 하나를 메서드 하나로 적어 둡니다. 「2만 원을 넣으면 1만 8천 원이 나와야 한다」를 확인하는 메서드가 테스트 메서드 하나입니다.

왜 이렇게 하나 — 경우마다 메서드를 나누면 어느 경우가 깨졌는지 메서드 이름으로 바로 압니다. 한 메서드가 실패해도 다른 메서드는 계속 돕니다.

어떻게 도나

  1. 메서드에 테스트라는 꼬리표를 붙이거나, 이름을 정해진 모양으로 짓습니다
  2. 테스트를 돌리는 프로그램이 꼬리표나 이름을 보고 메서드를 찾아 부릅니다
  3. 메서드가 끝까지 돌면 통과입니다. 확인이 어긋나 도중에 멈추면 실패입니다

대가 — 메서드끼리 필드 값을 나눠 쓸 수 없습니다. 메서드마다 새 인스턴스에서 돌기 때문입니다. 여러 메서드가 같은 준비를 하면 그 코드를 준비 메서드에 모아 둡니다. 러너가 준비 메서드를 테스트 메서드와 같은 인스턴스에서 먼저 불러 주어, 준비한 값이 필드로 건너갑니다.

상세

이 절의 예는 결제 금액을 돌려주는 pay 함수 하나입니다. 장바구니 금액이 2만 원 이상이면 2천 원을 깎아 줍니다. 이 함수를 확인하는 테스트 메서드 몇 개를 가지고 생김새와 도는 순서를 차례로 봅니다.

자동차 정기 검사장의 검사 항목표에는 브레이크, 전조등, 배기가스가 한 줄씩 적혀 있습니다. 검사원은 한 줄씩 짚어 갑니다. 줄마다 합격인지 불합격인지 따로 적습니다. 전조등이 불합격이어도 브레이크 검사는 계속 이어집니다.

테스트 메서드는 이 항목표의 한 줄입니다. 항목표 한 장은 테스트 메서드를 모아 둔 클래스입니다. 줄을 차례로 짚어 가는 검사원은 테스트를 돌리는 프로그램입니다.

테스트 메서드는 테스트 코드 안에서 경우 하나를 확인하는 메서드입니다. 「2만 원을 넣으면 1만 8천 원이 나와야 한다」가 경우 하나입니다. 이 경우를 맡은 메서드는 pay(20000) 을 부른 뒤 나온 값이 18000 인지 봅니다. 이렇게 넣을 값과 나와야 할 값을 정해 둔 확인 하나를 테스트 케이스라고 부릅니다.

이 메서드는 클래스 안에 적습니다. 테스트 메서드를 담으려고 만든 클래스가 테스트 클래스입니다. 한 테스트 클래스에는 대개 확인 대상 하나를 두고 메서드를 모읍니다. pay 를 확인하는 메서드는 PayTest 한 클래스에 모으는 식입니다.

개발자는 테스트 메서드를 직접 부르지 않습니다. 테스트 러너는 테스트를 찾아 부르고 그 결과를 모아 보여 주는 프로그램입니다. 개발자는 메서드를 적어 두기만 합니다.

테스트 메서드의 생김새

PayTest 를 자바로 적으면 아래와 같습니다. 테스트 도구는 자바에서 널리 쓰는 JUnit 입니다. 메서드 둘이 경우를 하나씩 맡습니다.

Java
class PayTest {

    @Test
    void discountAt20000() {
        int p = pay(20000);     // 18000
        assertEquals(18000, p); // 통과
    }

    @Test
    void noDiscountAt10000() {
        int p = pay(10000);     // 10000
        assertEquals(10000, p); // 통과
    }
}

메서드마다 첫 줄은 pay 를 부르는 줄입니다. 줄 오른쪽 주석은 그 줄에서 나오는 값입니다. 둘째 줄의 assertEquals 가 나온 값을 기대한 값과 맞춰 봅니다.

「이 값은 이래야 한다」를 코드로 적어 확인하는 문장을 단언이라고 합니다. assertEquals(18000, p) 는 p 가 18000 이어야 한다는 단언입니다. 두 값이 같으면 단언은 아무 일도 하지 않습니다. 다르면 예외를 던져 메서드를 거기서 멈춥니다.

러너는 메서드가 어떻게 끝났는지만 보고 결과를 적습니다. 끝까지 돌았으면 통과입니다. 단언이 예외를 던져 멈췄으면 실패입니다. 단언 말고 다른 예외로 멈춘 경우를 오류로 따로 세는 러너도 있습니다. unittest 는 결과에 실패 수와 오류 수를 나눠 적습니다.

인자도 반환값도 없는 모양

위의 두 메서드는 인자를 받지 않습니다. 돌려주는 값도 없어서 반환형이 void 입니다. 테스트 메서드는 대개 이 모양입니다.

부르는 쪽이 러너라서 인자를 받지 않습니다. 러너는 어느 메서드에 어떤 값을 넘겨야 할지 모릅니다. 그래서 필요한 값은 메서드가 스스로 만듭니다. pay 에 넣을 20000 도 메서드 안에 적혀 있습니다.

판정을 예외로 하므로 반환값도 쓸 데가 없습니다. 러너는 메서드가 무엇을 돌려주는지 보지 않습니다. 끝까지 돌았는지, 도중에 예외를 던졌는지만 봅니다. 돌려줄 값이 있어도 받아 줄 곳이 없습니다.

러너가 알아보는 표시

메서드 위의 @Test 는 이 메서드가 테스트라고 알리는 꼬리표입니다. 이렇게 코드에 붙이는 꼬리표를 애너테이션이라고 합니다. 러너는 클래스의 메서드를 훑어 @Test 애너테이션이 붙은 것만 테스트로 부릅니다.

애너테이션 대신 이름으로 알아보는 도구도 있습니다. 파이썬 표준 라이브러리의 테스트 도구 unittest 는 이름이 test 로 시작하는 메서드를 테스트로 칩니다. test_discount_at_20000 은 테스트 메서드입니다. make_cart 는 테스트 메서드가 아닙니다.

러너가 테스트 메서드를 알아보는 표시는 이렇게 애너테이션과 이름 규칙 둘입니다. 표시가 빠진 메서드는 러너가 못 찾습니다. 실패로 잡히지도 않고 아예 돌지 않습니다. 결과에 뜬 테스트 수가 적어 둔 메서드 수보다 적으면 표시가 빠졌는지 먼저 의심합니다.

표시가 없는 메서드도 테스트 클래스 안에 둘 수 있습니다. 러너는 이런 메서드를 부르지 않습니다. 장바구니를 채우는 코드처럼 여러 테스트 메서드가 함께 쓰는 코드를 이런 보통 메서드로 빼 둡니다. 이런 헬퍼 메서드는 테스트 메서드가 직접 부릅니다.

경우마다 메서드를 나누는 까닭

확인 셋을 메서드 하나에 몰아 적어도 코드는 돕니다. 아래 메서드는 금액 셋을 한꺼번에 확인합니다. pay 에 버그가 있어 딱 2만 원에서만 할인이 안 붙는다고 합시다.

Java
@Test
void discounts() {
  assertEquals(18000, pay(20000)); // 실패
  assertEquals(28000, pay(30000)); // 안 돎
  assertEquals(10000, pay(10000)); // 안 돎
}

첫 단언이 예외를 던지면 메서드는 거기서 멈춥니다. 둘째와 셋째 단언은 돌지도 못합니다. 러너의 결과에는 discounts 실패 하나만 남습니다. 이 결과만 봐서는 3만 원과 1만 원 경우가 맞는지 모릅니다.

같은 확인을 메서드 셋으로 나누면 러너가 셋을 각각 부릅니다. 결과는 실패 하나와 통과 둘로 나옵니다. 실패한 메서드 이름만 보고도 2만 원 경우가 깨졌다는 것을 압니다.

그래서 메서드 이름은 확인하는 경우를 말하게 짓습니다. discountAt20000 처럼 짓기도 합니다. 「2만 원이면 2천 원을 깎는다」처럼 한글 문장으로 짓는 팀도 있습니다. 실패 보고에 이 이름이 뜨면 코드를 열기 전에 무엇이 깨졌는지 압니다.

메서드마다 새로 만드는 인스턴스

러너는 테스트 메서드를 부르기 전과 뒤에도 할 일이 있습니다. 부르기 전에는 테스트 클래스의 인스턴스를 하나 새로 만듭니다. 메서드가 끝나면 그 인스턴스는 버립니다.

다음 메서드는 또 새 인스턴스에서 돕니다. JUnit 과 unittest 가 기본으로 이렇게 합니다.

이렇게 하면 메서드끼리 필드를 거쳐 영향을 주지 못합니다. 앞 메서드가 필드 값을 바꿔 놓아도 뒤 메서드는 그 값을 못 봅니다. 뒤 메서드는 새 인스턴스에서 돌기 때문입니다.

이 성질을 모르면 흔히 걸리는 실수가 있습니다. 한 메서드에서 필드에 값을 넣고 다른 메서드에서 그 값을 읽으려 합니다. 두 메서드는 서로 다른 인스턴스에서 돕니다. 읽는 쪽 필드에는 초깃값만 들어 있습니다.

그래도 여러 메서드가 같은 준비를 해야 할 때가 있습니다. 메서드마다 빈 장바구니가 필요할 때가 그렇습니다. 테스트가 돌기 전에 갖춰 두어야 하는 값이나 환경을 테스트 픽스처라고 합니다.

픽스처를 메서드마다 손으로 만들면 같은 코드가 메서드 수만큼 되풀이됩니다. 그래서 준비 코드는 메서드 하나에 모아 둡니다. 이 메서드를 준비 메서드라고 부릅니다. 러너는 테스트 메서드를 부르기 직전에 준비 메서드를 불러 줍니다.

준비 메서드는 테스트 메서드와 같은 인스턴스에서 불립니다. 그래서 준비 메서드가 필드에 넣어 둔 빈 장바구니를 테스트 메서드가 그대로 읽습니다. 다음 테스트 메서드 앞에서는 새 인스턴스에서 준비 메서드가 다시 불려 새 장바구니를 만듭니다.

뒷정리 코드를 담은 메서드는 정리 메서드라고 부릅니다. 러너는 테스트 메서드가 끝난 뒤에 정리 메서드를 불러 줍니다.

준비 메서드와 정리 메서드도 표시로 알아봅니다. JUnit 에서는 @BeforeEach 와 @AfterEach 애너테이션이 그 표시입니다. unittest 에서는 이름이 setUp 과 tearDown 인 메서드가 그 일을 합니다.

아래 그림은 러너가 테스트 메서드 하나를 도는 순서입니다.

sequenceDiagram
    participant 러너 as 테스트 러너
    participant 인스턴스 as 테스트 클래스 인스턴스
    loop 테스트 메서드마다
        러너->>인스턴스: 새로 만든다
        러너->>인스턴스: 준비 메서드를 부른다
        러너->>인스턴스: 테스트 메서드를 부른다
        인스턴스-->>러너: 끝까지 돎 또는 예외
        러너->>인스턴스: 정리 메서드를 부른다
        러너->>러너: 결과를 적고 인스턴스를 버린다
    end

그림의 묶음 안이 테스트 메서드마다 되풀이됩니다. 인스턴스를 만드는 일부터 버리는 일까지가 메서드 하나의 몫입니다. 테스트 메서드가 예외로 멈춰도 정리 메서드는 불립니다. 그래야 앞 메서드가 남긴 흔적이 뒤 메서드에 안 넘어갑니다.

메서드를 부르는 순서

러너가 메서드를 부르는 순서는 클래스에 적어 둔 순서와 다를 수 있습니다. unittest 는 메서드 이름의 글자 순서대로 부릅니다. JUnit 은 기본으로, 돌 때마다 같지만 짐작하기 어려운 순서로 부릅니다.

새 인스턴스는 필드만 새로 합니다. 데이터베이스처럼 인스턴스 밖에 남는 것은 정리 메서드가 지우지 않으면 그대로 남습니다. 순서가 결과를 바꾸는 것은 이런 흔적 때문입니다.

예를 들어 주문 생성 메서드가 데이터베이스에 남긴 주문을 주문 취소 메서드가 꺼내 쓴다고 합시다. 두 메서드는 그 순서로 돌 때만 통과합니다.

이렇게 다른 테스트가 먼저 돌았는지에 결과가 달린 상태를 테스트 순서 의존성이라고 합니다. 테스트 메서드는 이런 의존이 없게 짭니다. 취소 메서드가 취소할 주문을 스스로 만들면 이 의존이 사라집니다.

테스트 메서드 하나에 케이스 여럿

대개 테스트 메서드 하나가 테스트 케이스 하나입니다. 그런데 입력만 다르고 확인하는 방식은 같은 케이스가 여럿일 때가 있습니다. 앞의 discounts 의 세 줄이 그렇습니다.

이럴 때는 입력과 기대 값을 표로 적어 메서드 하나에 넘기기도 합니다. 러너는 표의 줄마다 메서드를 한 번씩 부릅니다. 이런 테스트를 매개변수화 테스트라고 합니다.

매개변수화 테스트의 메서드는 표의 한 줄을 인자로 받습니다. 앞에서 테스트 메서드는 대개 인자가 없다고 했습니다. 이 경우는 러너가 넘길 값을 표에서 알기 때문에 인자를 받습니다.

판정도 줄마다 합니다. 메서드는 하나여도 결과에는 표의 줄 수만큼 통과와 실패가 뜹니다. 한 줄이 실패해도 나머지 줄은 계속 돕니다.

관련 항목

테스트 메서드를 담고 돌리는 도구

테스트 클래스 · 테스트 스위트 · 테스트 러너 · 테스트 프레임워크 · 테스트 탐색

테스트 메서드 안을 이루는 구성 요소

단언 · 테스트 케이스 · Arrange-Act-Assert · Given-When-Then · 헬퍼 메서드

테스트 메서드 앞뒤에서 도는 준비와 정리

테스트 픽스처 · 테스트 격리 · 테스트 생명주기 · 공유 픽스처

러너가 테스트 메서드를 알아보는 수단

애너테이션 · 리플렉션 · 명명 규칙

테스트 메서드 하나로 케이스 여럿을 돌리는 방식

매개변수화 테스트 · 데이터 주도 테스트 · 속성 기반 테스트

테스트 메서드를 채택한 테스트 도구

JUnit · unittest · pytest · TestNG · NUnit · xUnit

테스트 메서드의 결과를 흐리는 문제

테스트 순서 의존성 · 불안정한 테스트 · 깨지기 쉬운 테스트 · 어서션 룰렛

테스트 메서드가 확인 대상을 떼어 놓는 수단

테스트 더블 · 목 객체 · 스텁 · 페이크 객체

테스트 메서드로 짜는 테스트 종류

단위 테스트 · 통합 테스트 · 회귀 테스트 · 자동 테스트

테스트 메서드를 먼저 적는 개발 방식

테스트 주도 개발 · 행위 주도 개발 · 인수 테스트 주도 개발

테스트 메서드가 속하는 상위 분류

QA와 테스트 · 소프트웨어 테스트 · 품질 보증

다른 이름: test method · 테스트 메소드