pytest
고친 사람 github-actions[bot]
pytest 는 파이썬 코드를 검사하는 테스트를 짜고 돌리게 해 주는 도구입니다. 이름이 test 로 시작하는 함수를 찾아 하나씩 부릅니다. 어느 테스트가 깨졌는지도 알려 줍니다. 확인하는 줄은 파이썬의 assert 문 하나로 적습니다.
쉽고 빠른 이해
pytest 는 파이썬 코드가 기대한 대로 도는지 명령 한 번으로 확인하게 해 줍니다. 할인 함수에 2만 원을 넣으면 1만 8천 원이 나와야 한다는 사실을 테스트 함수로 적어 두면, 코드를 고칠 때마다 다시 돌려 볼 수 있습니다.
이 도구가 없으면 확인할 때마다 값을 찍어 보는 코드를 따로 짜야 합니다. 찍힌 출력은 눈으로 비교합니다. pytest 에서는 기대한 값을 assert 한 줄로 적어 두면 비교는 도구가 맡습니다.
어떻게 도는가:
- 이름이 test 로 시작하는 파일과 함수를 찾습니다
- 테스트 함수가 인자 이름으로 달라고 한 준비물을 먼저 만들어 넘깁니다
- 함수를 부르고, assert 가 어긋나면 그 테스트를 실패로 적습니다
- 끝나면 통과와 실패를 모아 보여 줍니다
파이썬 백엔드의 테스트는 대개 이 도구로 짭니다. 패키지를 더 설치할 수 없는 곳에서는 파이썬에 딸려 오는 unittest 를 씁니다.
대가도 있습니다. 준비물은 인자 이름과 같은 이름의 준비 함수에서 옵니다. 그 함수가 어느 파일에 있는지는 테스트만 읽어서는 안 보입니다.
상세
pytest 는 파이썬 테스트를 짜는 틀이면서, 짜 놓은 테스트를 돌리는 테스트 러너입니다. 테스트 러너는 테스트를 찾아 차례로 부르고 결과를 모으는 프로그램입니다. 파이썬으로 백엔드를 만들면 테스트는 대개 이 도구로 짭니다.
파이썬에는 설치 없이 딸려 오는 테스트 도구가 따로 있습니다. 표준 라이브러리의 unittest 입니다. pytest 는 이와 달리 패키지 설치 도구인 pip 로 받아서 씁니다. 그래도 unittest 로 짠 테스트를 손대지 않고 함께 돌려 줍니다.
둘 중 무엇을 쓸지는 패키지를 설치할 수 있느냐로 갈립니다. 패키지를 더 들일 수 없는 환경에서는 unittest 로 짭니다.
pytest 가 없으면 코드를 확인할 때마다 값을 찍어 보는 스크립트를 따로 짜야 합니다. 찍혀 나온 값은 사람이 눈으로 비교합니다. 확인할 것이 수백 개로 늘면 그렇게 해서는 끝이 안 납니다.
이 절은 테스트 하나의 모양에서 시작합니다. 이어서 pytest 가 테스트를 찾는 규칙, 실패를 알리는 방식, 테스트 앞뒤의 준비와 정리를 맡기는 방법을 봅니다. 끝으로 테스트를 돌리는 방법과 이 도구가 치르는 대가를 봅니다.
테스트 하나의 모양
검사할 함수는 2만 원 이상이면 2천 원을 깎아 주는 할인 함수입니다. 넣은 값과 나오는 값은 이렇습니다.
discount(20000) # 18000
discount(15000) # 15000
이 함수를 검사하는 테스트는 평범한 파이썬 함수입니다. 파일 이름은 test_price.py 로 짓습니다.
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 뒤에 적기만
하면 됩니다.
예외가 나야 맞는 테스트
거꾸로 예외가 나야 통과인 테스트도 있습니다. 음수 금액을 넣으면 할인 함수가 거절해야 한다면 이렇게 적습니다.
import pytest
def test_negative_price():
with pytest.raises(ValueError):
discount(-1)
ValueError 는 값이 잘못됐을 때 쓰는 파이썬 기본 예외입니다. with pytest.raises(...) 아래에
들여 쓴 코드에서 그 예외가 나야 통과입니다. 예외가 안 나면 이 테스트가 실패합니다.
픽스처로 준비와 정리를 맡긴다
테스트마다 같은 준비가 필요할 때가 많습니다. 장바구니 객체를 만들거나 테스트용 데이터베이스에 연결하는 일입니다. pytest 는 이 준비를 픽스처라는 함수에 모읍니다. 픽스처는 테스트가 쓸 준비물을 만들어 건네는 함수입니다.
픽스처는 함수 위에 @pytest.fixture 를 붙여 만듭니다. @ 로 시작하는 이 표시를 데코레이터라고
부릅니다. 데코레이터는 함수에 붙여 그 함수의 쓰임을 바꾸는 파이썬 문법입니다.
@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 는 함수를 거기서 잠시 멈추고 값을 내보내는
파이썬 문장입니다.
@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 에서는 테스트 하나에 입력 목록을 붙이면 됩니다. 이렇게 입력만 바꿔 같은 검사를 되풀이하는 테스트가 매개변수화 테스트입니다.
@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