사전 단위 테스트
개념

단위 테스트

gabury1고친 사람 github-actions[bot]

단위 테스트는 코드의 작은 조각 하나를 떼어 내어 제대로 도는지 자동으로 확인합니다. 함수 하나에 정해 둔 입력을 넣고 나온 값이 기대한 값과 같은지 봅니다. 몇 초 안에 끝나서 코드를 고칠 때마다 돌려 볼 수 있습니다. 무엇이 깨졌는지를 고친 사람이 바로 알게 하려는 것입니다.

쉽고 빠른 이해

무슨 일을 하나 — 함수 하나를 따로 불러 답이 맞는지 확인하는 코드를 미리 써 둡니다. 「2만 원어치를 사면 2천 원을 깎아 1만 8천 원이 나와야 한다」를 코드로 적어 두고 명령 한 번으로 확인하는 식입니다.

왜 하나 — 코드를 한 줄 고치면 멀리 떨어진 기능이 깨지기도 합니다. 화면을 눌러 보며 찾으면 늦고 빠뜨립니다. 작은 조각마다 확인을 걸어 두면 깨진 조각이 테스트 이름과 함께 드러납니다.

어떻게 도나

  1. 조각이 쓸 값을 준비합니다
  2. 그 조각을 한 번 부릅니다
  3. 나온 값이 기대한 값과 같은지 확인합니다

대가 — 기능 코드와 별도로 테스트 코드를 쓰고 고쳐야 합니다. 조각이 하나씩 다 맞아도 조각끼리 붙였을 때 맞는다는 보장은 없습니다.

상세

이 절은 두 예를 가지고 단위 테스트를 봅니다. 2만 원 이상 사면 2천 원을 깎는 가격 계산기와, 데이터베이스에 다녀오는 주문 서비스입니다.

자동차 공장은 브레이크를 차에 달기 전에 브레이크만 따로 시험대에 올려 밟아 봅니다. 시험대에서 안 선다면 탓할 곳은 브레이크 하나뿐입니다. 다 조립한 뒤에 차가 안 서면 엔진부터 바퀴까지 전부 의심해야 합니다.

단위 테스트는 프로그램을 이루는 작은 조각을 따로 떼어 검사합니다. 이 조각을 단위라고 부릅니다. 보통 함수 하나나 클래스 하나가 한 단위입니다. 검사는 사람이 아니라 테스트 코드가 합니다.

단위 테스트가 없으면 코드를 고친 뒤의 확인을 사람이 합니다. 화면을 눌러 보고 로그를 읽습니다. 고친 곳에서 멀리 떨어진 기능은 확인 목록에서 빠지기 쉽습니다. 그러면 깨진 것을 사용자가 먼저 찾게 됩니다.

한 단위의 크기

단위를 얼마나 크게 잡을지는 팀마다 다릅니다. 한쪽은 클래스 하나를 단위로 잡습니다. 그 클래스가 기대는 다른 클래스는 전부 가짜로 바꿔 끼우고 검사합니다.

다른 쪽은 동작 하나를 단위로 잡습니다. 「장바구니 합계를 낸다」는 동작이 클래스 셋을 거친다면 셋을 진짜로 엮어서 검사합니다. 오래 걸리거나 프로그램 바깥에 있는 것만 가짜로 바꿉니다.

두 쪽이 같이 지키는 성질도 있습니다. 어느 크기로 잡든 단위 테스트는 금방 끝나야 합니다. 테스트끼리 서로 기대지 않아야 합니다. 이런 성질은 뒤의 「단위 테스트가 흔히 갖추는 네 성질」 소절에서 넷으로 모아 봅니다.

테스트 하나의 모양

단위 테스트 하나는 세 단계로 짭니다. 먼저 검사에 쓸 값과 객체를 준비합니다. 다음으로 검사할 조각을 한 번 부릅니다. 끝으로 나온 값이 기대한 값과 같은지 확인합니다.

이 세 단계를 준비·실행·확인이라 부릅니다. 영어로는 Arrange-Act-Assert 라고 적습니다. 세 단계를 빈 줄로 갈라 두면 무엇을 검사하는 테스트인지 한눈에 읽힙니다.

아래는 자바에서 흔히 쓰는 테스트 도구인 JUnit 으로 쓴 테스트입니다. 2만 원 이상 사면 2천 원을 깎아 주는 가격 계산기를 검사합니다.

Java
@Test
void 이만원_이상이면_이천원_깎는다() {
    // 준비
    var calc = new PriceCalculator();

    // 실행
    int price = calc.apply(20000); // 18000

    // 확인
    assertEquals(18000, price);
}

@Test 가 붙은 메서드 하나가 테스트 하나입니다. 도구는 이 표시가 붙은 메서드를 찾아 차례로 부릅니다. 메서드 이름은 무엇을 검사하는지를 문장처럼 적습니다. 테스트가 깨지면 이 이름이 결과 보고에 뜨기 때문입니다.

마지막 줄의 assertEquals 는 두 값을 견줍니다. 같으면 조용히 지나갑니다. 다르면 테스트를 실패로 적고 기대한 값과 나온 값을 함께 보여 줍니다. 이렇게 결과를 확인하는 한 줄이 단언입니다.

조각을 바깥에서 떼어 내는 방법

검사할 조각이 혼자 도는 경우는 드뭅니다. 주문 서비스는 회원을 읽을 때 회원 저장소를 거치고, 회원 저장소는 데이터베이스에 다녀옵니다. 주문 서비스는 결제 서버도 부르고 현재 시각도 봅니다. 이것들을 진짜로 쓰면 테스트가 오래 걸리고 돌릴 때마다 결과가 달라집니다.

그래서 바깥 것들을 가짜로 바꿔 끼웁니다. 테스트에서 진짜 대신 쓰는 이 가짜를 테스트 더블이라고 부릅니다. 영화에서 배우 대신 위험한 장면을 찍는 대역을 영어로 더블이라 부르는 데서 따온 이름입니다.

flowchart TD
    subgraph 진짜["진짜로 쓸 때"]
        A["주문 서비스"] --> B["회원 저장소"]
        B --> C[("데이터베이스")]
    end
    subgraph 테스트["단위 테스트에서"]
        D["주문 서비스"] --> E["가짜 회원 저장소"]
    end

주문 서비스 입장에서는 진짜 회원 저장소와 가짜 회원 저장소가 똑같이 생겼습니다. 둘 다 같은 인터페이스를 따르기 때문입니다. 가짜는 데이터베이스까지 가지 않고 미리 정해 둔 회원을 바로 돌려줍니다.

가짜를 끼우려면 조각이 기댈 것을 밖에서 넘겨받아야 합니다. 조각 안에서 new 로 직접 만들어 버리면 바꿔 끼울 틈이 없습니다. 기댈 것을 생성자로 넘겨받게 짜는 방식을 의존성 주입이라고 부릅니다. 단위 테스트를 쓰기 쉬운 코드가 대개 이 모양인 까닭입니다.

가짜는 하는 일에 따라 이름이 갈립니다. 무엇을 확인하려는지에 맞춰 고릅니다.

이름 하는 일 쓰는 때
스텁 부르면 정해 둔 값을 돌려준다 조각이 그 값을 받아 옳게 계산하는지 볼 때
목 객체 자기가 어떻게 불렸는지 기록한다 조각이 바깥을 옳게 불렀는지 볼 때
페이크 객체 메모리에 담는 저장소처럼 가볍게 진짜 흉내를 낸다 쓰고 읽기를 여러 번 오가는 흐름을 볼 때

표의 둘째 줄은 돌려받는 값으로는 확인할 수 없는 것을 봅니다. 「주문 하나에 결제 서버를 딱 한 번 부른다」가 그런 예입니다. 결제 서버 대신 목 객체를 끼우고, 테스트 끝에 목 객체가 몇 번 불렸는지를 확인합니다.

단위 테스트가 흔히 갖추는 네 성질

단위 테스트는 하루에도 여러 번 돌립니다. 몇 가지 성질을 갖추지 못하면 사람들이 돌리기를 꺼리게 됩니다. 아래 넷이 흔히 꼽히는 성질입니다.

성질 뜻 없으면 생기는 일
금방 끝난다 전부 돌려도 몇 분 안에 끝난다 개발자가 돌리기를 미룬다
서로 기대지 않는다 어느 순서로 돌려도 결과가 같다 앞 테스트가 남긴 값 탓에 뒤 테스트가 깨진다
매번 같은 결과를 낸다 코드를 안 고치면 몇 번을 돌려도 같다 깨져도 코드 탓인지 운 탓인지 모른다
스스로 판정한다 통과와 실패를 테스트 코드가 가른다 사람이 출력을 눈으로 읽어야 한다

셋째 성질이 깨진 테스트를 불안정한 테스트라고 부릅니다. 흔한 원인은 현재 시각, 난수, 네트워크처럼 돌릴 때마다 달라지는 것에 기대는 것입니다.

불안정한 테스트가 쌓이면 사람들은 실패를 보고도 한 번 더 돌려 보기만 합니다. 그러다 코드가 진짜로 깨져서 난 실패까지 넘기게 됩니다. 앞 소절에서 시각이나 네트워크를 테스트 더블로 바꾸는 까닭이 이것입니다.

다른 테스트와 나뉘는 선

테스트는 한 번에 얼마나 넓게 보느냐로 나뉩니다. 단위 테스트는 가장 좁게 봅니다.

종류 무엇을 보나 걸리는 시간 깨졌을 때
단위 테스트 조각 하나 가장 짧다 깨진 조각이 바로 나온다
통합 테스트 조각 여럿, 또는 조각과 데이터베이스 같은 바깥을 붙인 모양 중간 어느 이음매인지 찾아야 한다
종단 간 테스트 사용자가 쓰는 흐름 전체 가장 길다 원인이 어디든 있을 수 있다

단위 테스트가 다 통과해도 조각을 붙인 곳에서 깨질 수 있습니다. 테스트 더블이 진짜와 다르게 굴면 그렇습니다. 그래서 단위 테스트만으로 끝내지 않고 넓은 테스트를 몇 개 더 둡니다.

흔한 권장은 좁은 테스트일수록 많이 두는 것입니다. 금방 끝나는 단위 테스트를 가장 많이, 오래 걸리는 종단 간 테스트를 가장 적게 둡니다. 개수를 층으로 쌓으면 아래가 넓은 모양이 됩니다. 이 모양을 테스트 피라미드라고 부릅니다.

언제 돌리나

단위 테스트는 코드를 쓰는 사람이 가장 먼저 돌립니다. 함수 하나를 고칠 때마다 그 둘레의 테스트를 돌리는 개발자도 많습니다. 몇 초면 끝나기에 가능한 일입니다.

다음은 코드를 합칠 때입니다. 지속적 통합은 누가 코드를 올릴 때마다 서버가 빌드하고 테스트를 돌려 주는 방식입니다. 단위 테스트는 그 첫 관문을 맡습니다. 여기서 깨지면 그 변경은 뒤 단계로 못 넘어갑니다.

한 번 통과한 테스트는 버리지 않고 남겨 둡니다. 나중에 누가 코드를 고쳐 예전 기능이 깨지면 그 테스트가 잡아 줍니다. 이미 되던 기능이 다시 안 되는 일을 회귀라고 부릅니다. 회귀를 잡으려고 테스트를 남겨 돌리는 일이 회귀 테스트입니다.

회귀를 잡는 테스트가 있으면 구조를 고치기가 편해집니다. 동작은 두고 코드의 짜임만 바꾸는 일을 리팩터링이라고 합니다. 바꾼 뒤 단위 테스트가 전부 통과하면 동작이 안 바뀌었다고 믿을 근거가 생깁니다.

테스트 주도 개발은 테스트를 코드보다 먼저 쓰는 세 단계를 짧게 되풀이하는 방식입니다. 먼저 실패하는 테스트를 씁니다. 다음으로 그 테스트를 통과할 만큼만 코드를 씁니다. 끝으로 코드를 다듬습니다.

테스트가 지나간 코드의 비율

테스트 커버리지는 테스트를 돌리는 동안 한 번이라도 실행된 코드가 전체에서 얼마만큼인지를 비율로 잽니다. 테스트가 안 닿은 코드를 찾는 데 씁니다.

커버리지가 높다고 코드가 맞다는 뜻은 아닙니다. 코드를 실행만 하고 결과를 확인하지 않는 테스트도 커버리지를 올립니다. 숫자를 목표로 걸면 이런 테스트가 늘어나기 쉽습니다.

잘 맞는 코드와 덜 맞는 코드

단위 테스트가 가장 값을 하는 곳은 갈림이 많은 계산입니다. 할인 규칙, 날짜 계산, 입력 검증처럼 조건에 따라 답이 갈리는 코드가 그렇습니다. 경우마다 테스트를 하나씩 두면 조건을 바꿀 때 무엇이 흔들리는지 바로 보입니다.

바깥을 부르기만 하는 코드에는 덜 맞습니다. 요청을 받아 데이터베이스에 넘기기만 하는 코드를 가짜 저장소로 검사하면, 가짜가 돌려준 값을 다시 확인하는 셈이 됩니다. 이런 코드는 진짜 데이터베이스를 붙인 통합 테스트가 더 많은 것을 알려 줍니다.

테스트가 조각의 속을 너무 많이 알 때도 탈이 납니다. 결과가 아니라 안에서 어떤 함수를 몇 번 불렀는지까지 확인하면, 동작은 같게 두고 짜임만 바꿔도 테스트가 깨집니다. 그러면 테스트를 고치는 손이 코드를 고치는 손보다 커집니다. 목 객체를 많이 쓸수록 이런 일이 잦습니다.

관련 항목

단위 테스트와 범위로 나뉘는 테스트

통합 테스트 · 종단 간 테스트 · 시스템 테스트 · 인수 테스트 · 스모크 테스트 · 회귀 테스트 · 성능 테스트

단위를 떼어 낼 때 끼우는 가짜

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

단위 테스트 한 벌을 이루는 구성 요소

단언 · 테스트 케이스 · 테스트 픽스처 · 테스트 스위트 · 테스트 러너 · Arrange-Act-Assert

단위 테스트를 쓰고 돌리는 도구

JUnit · pytest · Mockito · Jest · xUnit

단위 테스트를 중심에 둔 개발 방식

테스트 주도 개발 · 행위 주도 개발 · 리팩터링 · 의존성 주입 · 테스트 피라미드

단위 테스트가 첫 관문을 맡는 개발 단계

지속적 통합 · 지속적 전달 · 빌드 · 배포 파이프라인

단위 테스트의 빈 곳을 재는 지표

테스트 커버리지 · 분기 커버리지 · 구문 커버리지 · 뮤테이션 테스트

단위 테스트에서 자주 나는 문제

불안정한 테스트 · 깨지기 쉬운 테스트 · 테스트 순서 의존성 · 과도한 모킹

단위 테스트와 함께 결함을 잡는 수단

정적 분석 · 동적 분석 · 퍼징 · 코드 리뷰 · 속성 기반 테스트

단위 테스트가 속하는 상위 분류

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

다른 이름: unit test · unit testing · 유닛 테스트 · 단위 시험