사전 pytest
구현체

pytest

gabury1고친 사람 github-actions[bot]

pytest 는 파이썬 코드를 검사하는 테스트를 짜고 돌리게 해 주는 도구입니다. 이름이 test 로 시작하는 함수를 찾아 하나씩 부릅니다. 어느 테스트가 깨졌는지도 알려 줍니다. 확인하는 줄은 파이썬의 assert 문 하나로 적습니다.

쉽고 빠른 이해

pytest 는 파이썬 코드가 기대한 대로 도는지 명령 한 번으로 확인하게 해 줍니다. 할인 함수에 2만 원을 넣으면 1만 8천 원이 나와야 한다는 사실을 테스트 함수로 적어 두면, 코드를 고칠 때마다 다시 돌려 볼 수 있습니다.

이 도구가 없으면 확인할 때마다 값을 찍어 보는 코드를 따로 짜야 합니다. 찍힌 출력은 눈으로 비교합니다. pytest 에서는 기대한 값을 assert 한 줄로 적어 두면 비교는 도구가 맡습니다.

어떻게 도는가:

  1. 이름이 test 로 시작하는 파일과 함수를 찾습니다
  2. 테스트 함수가 인자 이름으로 달라고 한 준비물을 먼저 만들어 넘깁니다
  3. 함수를 부르고, assert 가 어긋나면 그 테스트를 실패로 적습니다
  4. 끝나면 통과와 실패를 모아 보여 줍니다

파이썬 백엔드의 테스트는 대개 이 도구로 짭니다. 패키지를 더 설치할 수 없는 곳에서는 파이썬에 딸려 오는 unittest 를 씁니다.

대가도 있습니다. 준비물은 인자 이름과 같은 이름의 준비 함수에서 옵니다. 그 함수가 어느 파일에 있는지는 테스트만 읽어서는 안 보입니다.

상세

pytest 는 파이썬 테스트를 짜는 틀이면서, 짜 놓은 테스트를 돌리는 테스트 러너입니다. 테스트 러너는 테스트를 찾아 차례로 부르고 결과를 모으는 프로그램입니다. 파이썬으로 백엔드를 만들면 테스트는 대개 이 도구로 짭니다.

파이썬에는 설치 없이 딸려 오는 테스트 도구가 따로 있습니다. 표준 라이브러리의 unittest 입니다. pytest 는 이와 달리 패키지 설치 도구인 pip 로 받아서 씁니다. 그래도 unittest 로 짠 테스트를 손대지 않고 함께 돌려 줍니다.

둘 중 무엇을 쓸지는 패키지를 설치할 수 있느냐로 갈립니다. 패키지를 더 들일 수 없는 환경에서는 unittest 로 짭니다.

pytest 가 없으면 코드를 확인할 때마다 값을 찍어 보는 스크립트를 따로 짜야 합니다. 찍혀 나온 값은 사람이 눈으로 비교합니다. 확인할 것이 수백 개로 늘면 그렇게 해서는 끝이 안 납니다.

이 절은 테스트 하나의 모양에서 시작합니다. 이어서 pytest 가 테스트를 찾는 규칙, 실패를 알리는 방식, 테스트 앞뒤의 준비와 정리를 맡기는 방법을 봅니다. 끝으로 테스트를 돌리는 방법과 이 도구가 치르는 대가를 봅니다.

테스트 하나의 모양

검사할 함수는 2만 원 이상이면 2천 원을 깎아 주는 할인 함수입니다. 넣은 값과 나오는 값은 이렇습니다.

Python
discount(20000)  # 18000
discount(15000)  # 15000

이 함수를 검사하는 테스트는 평범한 파이썬 함수입니다. 파일 이름은 test_price.py 로 짓습니다.

Python
from price import discount

def test_discount_over_20000():
    assert discount(20000) == 18000

test_ 로 시작하는 함수 하나가 테스트 하나입니다. 함수 이름에는 무엇을 검사하는지 적습니다. 테스트가 깨지면 이 이름이 결과에 뜨기 때문입니다.

마지막 줄의 assert 는 파이썬에 원래 있는 문장입니다. 뒤에 적은 식이 참이 아니면 오류를 냅니다. 이렇게 결과를 확인하는 한 줄을 단언이라고 부릅니다.

테스트를 찾는 규칙

pytest 는 테스트를 목록에 등록받지 않습니다. 이름을 보고 스스로 찾습니다. 그래서 테스트를 새로 짜도 따로 적어 둘 곳이 없습니다.

찾는 규칙은 이름의 앞뒤에 붙는 글자입니다.

무엇 이름 규칙 예
파일 test_ 로 시작하거나 _test 로 끝난다 test_price.py · price_test.py
함수 test 로 시작한다 test_discount_over_20000
클래스 Test 로 시작하고 생성자 __init__ 이 없다 TestDiscount

클래스는 테스트 여러 개를 한 이름 아래 묶을 때 씁니다. 그 안의 메서드도 test 로 시작해야 테스트로 잡힙니다. unittest 처럼 정해진 부모 클래스를 물려받을 필요는 없습니다.

규칙에 안 맞는 이름은 조용히 건너뜁니다. 파일 이름을 price_tests.py 로 지으면 그 안의 테스트는 하나도 돌지 않습니다. 실패로도 뜨지 않으니 결과에 찍힌 테스트 개수로 알아채야 합니다.

assert 로 실패를 알리는 방식

assert 의 식이 거짓이면 파이썬은 AssertionError 라는 예외를 던집니다. 예외는 무언가 잘못됐다고 알리는 신호입니다. 예외가 나면 함수는 거기서 빠져나옵니다. pytest 는 테스트 함수 밖으로 예외가 빠져나오면 그 테스트를 실패로 적습니다.

한 테스트가 깨져도 pytest 는 거기서 멈추지 않습니다. 나머지 테스트는 끝까지 돕니다.

맨 assert 는 거짓이라는 것만 알려 줍니다. 무엇과 무엇이 달랐는지는 안 알려 줍니다. 그래서 pytest 는 테스트 파일을 불러올 때 assert 문을 고쳐 씁니다. 식 안의 값을 하나하나 기록해 두었다가 실패하면 보여 주도록 바꾸는 것입니다. 이 기능을 assertion rewriting 이라고 부릅니다.

할인 함수가 할인을 빠뜨려 2만 원을 그대로 돌려줬다고 해 봅시다. 결과에는 이런 줄이 남습니다.

>       assert discount(20000) == 18000
E       assert 20000 == 18000
E        +  where 20000 = discount(20000)

> 가 붙은 줄이 깨진 assert 입니다. E 가 붙은 줄은 그때 식 안의 값입니다. 기대한 값은 18000 인데 discount(20000) 이 20000 을 돌려줬다는 뜻입니다.

unittest 에서는 이 비교를 self.assertEqual(나온 값, 기대한 값) 같은 메서드로 적습니다. 비교 방식마다 메서드가 따로 있습니다. pytest 는 == · in · < 같은 파이썬 식을 assert 뒤에 적기만 하면 됩니다.

예외가 나야 맞는 테스트

거꾸로 예외가 나야 통과인 테스트도 있습니다. 음수 금액을 넣으면 할인 함수가 거절해야 한다면 이렇게 적습니다.

Python
import pytest

def test_negative_price():
    with pytest.raises(ValueError):
        discount(-1)

ValueError 는 값이 잘못됐을 때 쓰는 파이썬 기본 예외입니다. with pytest.raises(...) 아래에 들여 쓴 코드에서 그 예외가 나야 통과입니다. 예외가 안 나면 이 테스트가 실패합니다.

픽스처로 준비와 정리를 맡긴다

테스트마다 같은 준비가 필요할 때가 많습니다. 장바구니 객체를 만들거나 테스트용 데이터베이스에 연결하는 일입니다. pytest 는 이 준비를 픽스처라는 함수에 모읍니다. 픽스처는 테스트가 쓸 준비물을 만들어 건네는 함수입니다.

픽스처는 함수 위에 @pytest.fixture 를 붙여 만듭니다. @ 로 시작하는 이 표시를 데코레이터라고 부릅니다. 데코레이터는 함수에 붙여 그 함수의 쓰임을 바꾸는 파이썬 문법입니다.

Python
@pytest.fixture
def cart():
    c = Cart()
    c.add("책", 20000)
    return c

def test_total(cart):
    assert cart.total() == 20000

테스트 함수 test_total 은 cart 라는 인자를 받겠다고만 적었습니다. pytest 는 이 인자 이름과 같은 이름의 픽스처를 찾아 먼저 부릅니다. 그 픽스처가 돌려준 값을 인자로 넣어 테스트를 부릅니다.

필요한 것을 테스트가 직접 만들지 않고 밖에서 받아 쓰는 셈입니다. 이런 방식이 의존성 주입입니다.

정리는 yield 뒤에

준비한 것은 테스트가 끝나면 치워야 할 때가 있습니다. 연 연결은 닫고 만든 파일은 지웁니다. 이때 픽스처 안에서 return 대신 yield 를 씁니다. yield 는 함수를 거기서 잠시 멈추고 값을 내보내는 파이썬 문장입니다.

Python
@pytest.fixture
def db():
    conn = connect_test_db()
    yield conn
    conn.close()

yield 앞이 준비이고 뒤가 정리입니다. pytest 는 yield 로 내보낸 값을 테스트에 넘깁니다. 테스트가 끝나면 멈춰 있던 픽스처를 다시 이어 돌려 yield 뒤를 실행합니다. 테스트가 실패해도 정리는 돕니다.

픽스처를 얼마나 오래 살리나

픽스처는 기본으로 테스트 하나마다 새로 만듭니다. 앞 테스트가 준비물에 남긴 흔적이 뒤 테스트로 새지 않게 하려는 것입니다. 테스트끼리 서로 영향을 주지 않게 떼어 두는 것을 테스트 격리라고 부릅니다.

그런데 띄우는 데 오래 걸리는 준비물을 테스트마다 새로 만들면 전체가 느려집니다. 그래서 픽스처에 수명을 줄 수 있습니다. @pytest.fixture(scope="session") 처럼 적으면 됩니다.

scope 값 한 번 만들어 누구와 나눠 쓰나
function 테스트 함수 하나. 아무것도 안 적으면 이것이다
class 한 테스트 클래스 안의 테스트 전부
module 한 테스트 파일 안의 테스트 전부
session 한 번 돌리는 동안의 테스트 전부

수명을 늘리면 빨라지는 대신 격리가 약해집니다. session 픽스처로 연 데이터베이스에 한 테스트가 데이터를 남기면 다음 테스트가 그 데이터를 봅니다.

pytest 가 미리 챙겨 둔 픽스처

pytest 에는 따로 짜지 않아도 받아 쓸 수 있는 픽스처가 딸려 옵니다. 테스트 함수에 그 이름의 인자를 적기만 하면 됩니다.

픽스처 건네는 것
tmp_path 테스트 하나만 쓰는 빈 임시 폴더
monkeypatch 객체의 속성이나 환경 변수를 잠깐 바꿔 두는 도구. 테스트가 끝나면 원래대로 되돌린다
capsys 테스트 중에 화면으로 찍힌 출력을 붙잡아 읽는 도구

monkeypatch 는 진짜 대신 가짜를 끼울 때 자주 씁니다. 외부 결제 서버를 부르는 함수를 정해진 답만 돌려주는 함수로 잠깐 바꿔 끼우는 식입니다. 진짜 대신 끼우는 이런 가짜를 테스트 더블이라고 부릅니다.

여러 파일이 나눠 쓰는 conftest.py

픽스처를 테스트 파일 안에 두면 그 파일에서만 씁니다. 여러 파일이 같은 픽스처를 쓰려면 conftest.py 라는 이름의 파일에 둡니다. pytest 는 이 파일을 알아서 읽습니다. 그래서 테스트 파일에서 불러오는 줄을 적지 않아도 됩니다.

conftest.py 의 픽스처는 그 파일이 놓인 폴더와 그 아래 폴더의 테스트가 씁니다. 폴더마다 하나씩 둘 수 있습니다. 아래 구성에서 tests/api/ 의 테스트는 두 conftest.py 의 픽스처를 모두 씁니다. tests/ 바로 아래 테스트는 위쪽 것만 씁니다.

flowchart TD
    subgraph T["tests 폴더"]
        C1["conftest.py · db 픽스처"]
        F1["test_price.py"]
        subgraph A["tests/api 폴더"]
            C2["conftest.py · client 픽스처"]
            F2["test_users.py"]
        end
    end
    C1 --> F1
    C1 --> F2
    C2 --> F2

같은 테스트를 여러 입력으로

경계값 근처를 검사하려면 입력만 다른 테스트를 여럿 짜게 됩니다. pytest 에서는 테스트 하나에 입력 목록을 붙이면 됩니다. 이렇게 입력만 바꿔 같은 검사를 되풀이하는 테스트가 매개변수화 테스트입니다.

Python
@pytest.mark.parametrize("price, expected", [
    (20000, 18000),
    (19999, 19999),
    (0, 0),
])
def test_discount(price, expected):
    assert discount(price) == expected

목록의 한 줄이 테스트 하나가 됩니다. 위 코드는 테스트 세 개로 돕니다. 결과에는 파일 이름::함수 이름 뒤에 입력이 대괄호로 붙어 뜹니다.

test_price.py::test_discount[20000-18000] PASSED
test_price.py::test_discount[19999-19999] PASSED
test_price.py::test_discount[0-0] PASSED

@pytest.mark 로 시작하는 표시를 마크라고 부릅니다. 입력 목록 말고도 테스트를 건너뛰게 하거나 이름표를 붙여 골라 돌리는 데 씁니다.

테스트를 돌리는 방법

프로젝트 폴더에서 pytest 라고 치면 그 아래의 테스트를 전부 찾아 돌립니다. 하나만 돌리려면 pytest test_price.py::test_discount 처럼 파일과 함수 이름을 붙입니다.

옵션 하는 일
-k discount 이름에 discount 가 든 테스트만 돌린다
-x 첫 실패에서 멈춘다
-v 테스트마다 이름과 결과를 한 줄씩 찍는다

pytest 는 끝날 때 종료 코드를 남깁니다. 종료 코드는 프로그램이 끝나며 돌려주는 숫자입니다. 다 통과하면 0 입니다. 하나라도 실패하면 0 이 아닌 값입니다.

이 숫자 덕분에 지속적 통합 서버가 결과를 읽을 수 있습니다. 지속적 통합은 코드가 올라올 때마다 서버가 빌드와 테스트를 자동으로 돌리는 관행입니다. pytest 가 0 이 아닌 값으로 끝나면 서버는 그 변경을 실패로 표시합니다.

플러그인으로 붙이는 기능

pytest 는 테스트를 찾아 돌리고 결과를 모으는 데까지 맡습니다. 그 밖의 기능은 플러그인으로 붙입니다. 플러그인은 pip 로 설치하면 pytest 가 알아서 불러옵니다.

플러그인 붙이는 기능
pytest-cov 테스트가 코드의 몇 줄을 지나갔는지 잰다
pytest-xdist 테스트를 여러 프로세스에 나눠 동시에 돌린다
pytest-mock 가짜 객체를 만드는 mocker 픽스처를 준다
pytest-asyncio async def 로 짠 비동기 테스트를 돌린다

첫 줄의 pytest-cov 가 재는 값이 코드 커버리지입니다. 테스트가 한 번도 지나가지 않은 코드를 찾을 때 봅니다.

이름으로 이어 붙이는 방식의 대가

pytest 는 적는 양을 줄이는 쪽을 골랐습니다. 테스트는 부모 클래스 없이 함수로 짭니다. 준비물은 인자 이름만 적으면 들어옵니다. 그 대신 무엇이 어디서 오는지가 코드에 드러나지 않습니다.

테스트가 db 라는 인자를 받는다고 해 봅시다. 그 db 는 같은 파일에 있을 수도 있고, 위쪽 폴더 어느 conftest.py 에 있을 수도 있습니다. 불러오는 줄이 없으니 편집기에서 따라가기도 어렵습니다. pytest --fixtures 를 돌리면 지금 쓸 수 있는 픽스처와 그 픽스처가 있는 파일을 보여 줍니다.

assert 를 고쳐 쓰는 기능에도 경계가 있습니다. pytest 는 테스트 파일과 conftest.py 만 고쳐 씁니다. 테스트가 부르는 도우미 모듈 안의 assert 는 고치지 않습니다. 그래서 그 assert 가 깨지면 값 없이 AssertionError 만 뜹니다.

관련 항목

pytest 로 짜는 테스트의 종류

단위 테스트 · 통합 테스트 · 회귀 테스트 · 매개변수화 테스트 · 속성 기반 테스트

pytest 테스트를 이루는 구성 요소

테스트 케이스 · 테스트 스위트 · 단언 · 테스트 픽스처 · AssertionError · 예외 · 데코레이터 · 제너레이터

pytest 테스트에 끼워 넣는 가짜 객체

테스트 더블 · 목 객체 · 스텁 · 페이크 객체 · 몽키 패칭 · unittest.mock

pytest 에 붙여 쓰는 플러그인과 도구

pytest-cov · pytest-xdist · pytest-mock · pytest-asyncio · coverage.py · Hypothesis · tox

pytest 를 돌리는 환경과 도구

pip · 가상 환경 · 지속적 통합 · 종료 코드 · GitHub Actions · 통합 개발 환경

pytest 와 같은 역할을 맡는 테스트 도구

unittest · JUnit · Jest · xUnit · nose · doctest

pytest 테스트를 흔드는 문제

불안정한 테스트 · 테스트 격리 · 테스트 순서 의존

pytest 가 속하는 상위 분류

테스트 러너 · 테스트 프레임워크 · 테스트 자동화 · QA와 테스트 · Python · 의존성 주입 · 플러그인 · 코드 커버리지

다른 이름: 파이테스트 · py.test