python test는 이제 pytest와 함께 - pytest tutorial (1)

2 minute read

python에는 JUnit에 영감을 받은 unittest가 있습니다. 그런데 unittest는 다소 장황한 문법으로 인해서 사용이 불편한 감이 있어요. 그래서 훨씬 간편한 pytest를 소개합니다.

  • 이런 분께 추천합니다.
    • python에서 test를 하고 싶은 분
    • unittest의 class test 방식, 많은 assert variation에 지치신 분

Install

(optional) python -m venv
(optional) source venv/bin/activate

pip install pytest

Quick Start

# test_first_step.py

def my_function(x):
    return x + 1


def test_my_function():
    assert my_function(3) == 4
    assert my_function(3) == 5  # 실패


class TestClass:
    def test_one(self):
        x = "this"
        assert "h" in x

    def test_two(self):
        x = "hello"
        assert hasattr(x, x)  # 실패

pytest [file_name] 으로 테스트 코드를 실행시킬 수 있습니다. 만약 파일명을 명시하지 않고 pytest만 실행하면 파일명에 test_ prefix 가 붙어있는 파일의 테스트 코드를 검사합니다.

테스트 함수에는 test_ prefix가 붙어있어야 합니다. TestClass 처럼 테스트 함수를 클래스로 묶어서 테스트도 가능한데, 테스트 클래스에는 Test prefix가 붙어있어야 합니다.

그럼 이제 pytest를 돌려봅니다.

$ pytest

=========================================================== test session starts ============================================================
platform darwin -- Python 3.8.5, pytest-6.2.4, py-1.10.0, pluggy-0.13.1
rootdir: /Users/mskim/Developer/pytest_tutorial
collected 3 items                                                                                                                          

tests/test_first_step.py F.F                                                                                                         [100%]

================================================================= FAILURES =================================================================
_____________________________________________________________ test_my_function _____________________________________________________________

    def test_my_function():
        assert my_function(3) == 4
>       assert my_function(3) == 5
E       assert 4 == 5
E        +  where 4 = my_function(3)

tests/test_first_step.py:9: AssertionError
____________________________________________________________ TestClass.test_two ____________________________________________________________

self = <tests.test_first_step.TestClass object at 0x1040aba30>

    def test_two(self):
        x = "hello"
>       assert hasattr(x, x)
E       AssertionError: assert False
E        +  where False = hasattr('hello', 'hello')

tests/test_first_step.py:18: AssertionError
========================================================= short test summary info ==========================================================
FAILED tests/test_first_step.py::test_my_function - assert 4 == 5
FAILED tests/test_first_step.py::TestClass::test_two - AssertionError: assert False
======================================================= 2 failed, 1 passed in 0.06s ========================================================
(.venv)  ✘ mskim  ~/Developer/pytest_tutorial  

이렇게 어디에서 오류가 발생했는지 보여줍니다.

Test Folder Structure

test 코드가 위 예시처럼 main코드와 섞여있으면 불편하겠죠. 그래서 보통은 아래처럼 tests 폴더 아래에 테스트 파일들을 몰아넣고, test 코드와 test할 코드명을 짝으로 만들어 놓습니다. module/first_step.pytests/module/test_first_step.py 처럼요.

$ tree
.
├── module
│   ├── __init__.py
│   └── first_step.py
└── tests
    ├── __init__.py
    ├── module
    │   ├── __init__.py
    │   └── test_first_step.py
    └── test_tempfile.py

3 directories, 7 files

Temporary directories and files

pytest는 fixture 라는 걸 제공하는데, 이걸 이용해서 여러 편리한 기능을 사용할 수 있습니다. 여기에서는 임시 폴더와 파일 경로를 받는 걸 정리합니다.

# test_tempfile.py

def create_file(file_path, content):
    with open(file_path, 'w') as f:
        f.write(content)


def test_create_file(tmp_path):
    print(type(tmp_path), tmp_path)

    path = tmp_path / "sub"
    path.mkdir()

    file_path = path / "temp.txt"
    CONTENT = "content"
    create_file(file_path, CONTENT)
    
    assert file_path.read_text() == CONTENT  # CONTENT가 제대로 들어갔는지 확인
    assert len(list(tmp_path.iterdir())) == 1  # 디렉토리가 생성되었는지 확인
    assert 0    

위에서는 테스트 함수 인자가 없었지만, 여기에서는 tmp_path 를 받는 test 함수를 선언해줍니다. 그러면 테스트에 사용할 임시 경로를 pathlib.Path object로 받게 됩니다. tmp_path를 이용해서 create_file 함수를 테스트했습니다.

위 파일을 테스트하면, 아래 결과가 나옵니다.

================================================================= FAILURES =================================================================
_____________________________________________________________ test_create_file _____________________________________________________________

tmp_path = PosixPath('/private/var/folders/5b/xqppj5qx5jdfn38_3nbgc9lm0000gn/T/pytest-of-mskim/pytest-46/test_create_file0')

    def test_create_file(tmp_path):
        print(type(tmp_path), tmp_path)
    
        path = tmp_path / "sub"
        path.mkdir()
    
        file_path = path / "temp.txt"
        CONTENT = "content"
        create_file(file_path, CONTENT)
    
        assert file_path.read_text() == CONTENT  # CONTENT가 제대로 들어갔는지 확인
        assert len(list(tmp_path.iterdir())) == 1  # 디렉토리가 생성되었는지 확인
>       assert 0
E       assert 0

tests/test_tempfile.py:25: AssertionError
----------------------------------------------------------- Captured stdout call -----------------------------------------------------------
<class 'pathlib.PosixPath'> /private/var/folders/5b/xqppj5qx5jdfn38_3nbgc9lm0000gn/T/pytest-of-mskim/pytest-46/test_create_file0
========================================================= short test summary info ==========================================================
FAILED tests/test_tempfile.py::test_create_file - assert 0

py.path vs. pathlib
pytest는 os.path 의 interface인 py.path도 지원합니다. 다만, py 라이브러리는 pathlib 이 도입되면서 유지보수 모드가 되었습니다. 새로 시작하는 프로젝트라면 pathlib을 사용합시다.


Leave a comment