0. 가이드에 들어가기 전에
pytest 에 대한 설명을 담은 페이지입니다.
여기에서는 전반적인 설명과 사용법을 다루며 세부사항을 깊게 보지는 않습니다. 추가적인 자세한 내용은 공식문서를 참고해 주시길 바랍니다.
pytest 공식문서
Full pytest documentation - pytest documentation
1. 테스트에 대해서
코딩을 하게 되면 해당 코드가 의도대로 작동하는지, 어떤 부분들을 확인해야 하는지 등 고려해야 할 부분들이 몇몇 있습니다. 따라서 코드가 정상적으로 작동하는지 확인을 하기 위해 추가로 코드를 작성해 확인하는 과정을 거칩니다.
이 때 코드를 추가로 작성한다는 것은 양이 추가되기도 하지만 또다른 코드가 등장하는 것입니다. 즉, 이 테스트 코드를 테스트할 코드를 작성하지 않는 한 해당 코드는 최대한 꼼꼼하게, 문제없이 작동하도록 해야 합니다.
코드를 테스트할 때에는 어떤 것을 확인하는지에 따라 3가지로 분류됩니다.
- Unit testing: 하나의 어플리케이션 내에 있는 개별 모듈들을 (별도의 의존성 없이) 독립적으로 확인합니다.
- Integration testing: 한번에 여러개의 모듈들을 실행할 때 정상적으로 작동이 되는지 확인합니다.
- Functional testing: 하나의 시스템에서 특정 기능이 작동하는지 확인합니다. 이 과정에서 의존성이 있는 dependencies 들과 상호작용할 수 있습니다.
2. 테스트와 개발
해당 부분은 다양한 소프트웨어 테스트에 대한 사이트를 참고하시면 도움이 됩니다.
테스트 주도 개발 (TDD)
가장 많이 접했을 용어 중 하나는 '테스트 주도 개발' 혹은 'Test Driven Development (TDD)' 입니다.
TDD 는 쉽게 말해 어플리케이션 코드를 작성하기 전에 수행해야 하는 작업들을 먼저 테스트로 작성하는 것입니다.
예를 들어 숫자가 주어지면 해당 숫자의 제곱을 리턴하는 함수를 작성하고 싶다고 하겠습니다. 그렇다면 다음과 같은 순서로 개발을 하게 됩니다.
- 제곱 함수에 대한 테스트를 작성합니다.
- 테스트를 실행합니다.
- 코드를 작성하거나 수정합니다.
- 1번에서 작성한 테스트가 통과할 때까지 2, 3번들을 반복합니다.
TDD 의 장단점
- 테스트를 먼저 작성하기 때문에 요구사항이 명확해집니다.
- 테스트를 작성하면서 코드의 방향성이 명확해집니다.
- 개발 속도가 느릴 수도 있습니다.
이외에도 BDD, 다른 개발 방식 등이 존재합니다.
테스트와 개발은 분리할 수가 없습니다. 테스트 코드가 없는 개발은 존재할 수 있지만 테스트가 없는 개발이란 존재하기 힘듭니다. 설령 코드가 아니더라도 직접 개발자가 확인하는 과정도 테스트에 포함된다고 볼 수 있습니다. 즉, 테스트 코드를 작성한다는 것은 개발자가 확인해야 하는 부담을 코드가 가져가는 행위로도 볼 수 있습니다.
3. 파이썬 테스트 프레임워크에 대해서
파이썬에서 많이들 사용하는 unittest 와 pytest 에 대해서 알아보겠습니다.
unittest
소개
파이썬에서 기본으로 제공하는 테스트 라이브러리입니다. 즉, 추가로 설치할 필요가 없고 파이썬을 사용하는 코드에서는 대부분 사용할 수 있다는 장점이 있습니다.
또한 기본적인 디자인은 파이썬의 객체지향프로그래밍을 따라 클래스 형태로 테스트를 구분하며 각 클래스에서는 사전에 지정된 메소드를 통해서 테스트의
setUp
과tearDown
등을 실행할 수 있습니다.설치
추가 설치가 없습니다.
코드
import unittest def multiplication(x, y): return x * y class TestMultiplication(unittest.TestCase): def setUp(self): print('Starting test') def tearDown(self): print('Ending test') def test_3_times_5(self): result = multiplication(3, 5) self.assertEqual(result, 15)
unittest
파이썬 문서:unittest - Unit testing framework - Python 3.9.2 documentation
pytest
소개
파이썬에서 가장 널리 사용되고 있는 테스트 프레임워크입니다. 유연성과 다양한 플러그인 그리고 무엇보다 파이썬의
unittest
라이브러리도 같이 사용할 수 있다는 큰 장점이 특징들입니다.pytest
는 함수를 중심으로 작동합니다. 즉, 같이 사용할 수는 있지만 클래스를 통해 구분하고 나뉘는unittest
와는 다르게 작동합니다. 따라서setup
과teardown
등은 따로 작성해야 하며 필수는 아니지만 보통[conftest.py](http://conftest.py)
라는 파일에 따로 명시를 하고 파이썬의yield
를 사용합니다.설치
$ pip install pytest
코드
import pytest def multiplication(x, y): return x * y @pytest.fixture(autouse=True) def setup_teardown(): print('Starting test') yield print('Ending test') def test_3_times_5(): result = multiplication(3, 5) assert result == 15
4. pytest
이제 본격적으로 pytest 가 어떻게 작동하는지 살펴보도록 하겠습니다.
Effective Python Testing With Pytest - Real Python
목차는 다음과 가
소개
pytest 실행하기
Fixtures
Marks
Plugins
소개
pytest
는 기본적으로 함수 위주로 동작합니다. 가끔가다 클래스로 테스트들을 묶어 사용하기도 하지만 기본적으로 함수와 데코레이터 등으로 대부분을 작성합니다.먼저 테스트를 어떻게 작성하는지 기본 함수부터 보겠습니다.
def test_uppercase(): assert "loud noises".upper() == "LOUD NOISES" def test_reversed(): assert list(reversed([1, 2, 3, 4])) == [4, 3, 2, 1] def test_some_primes(): assert 37 in { num for num in range(1, 50) if num != 1 and not any([num % div == 0 for div in range(2, num)]) }
functional 테스트는 주로 다음과 같은 패턴을 따릅니다:
Arrange: 테스트를 실행하기 위한 사전 조건들을 셋업합니다.
Act: 특정 함수나 메소드를 실행합니다.
Assert: 주어진 조건들이 참인지 확인합니다.
pytest
도 마찬가지로 이 패턴을 따릅니다. 즉, 먼저 테스트에 필요한 조건들을 설정하고, 테스트 함수들을 실행하며 각 함수 내부에 있는 조건들이 참인지 확인하게 됩니다.그렇다면 어떤 파일들과 어떤 함수들을 테스트할지 어떻게 구별할까요?
pytest
는 기본적으로 파이썬의 컨벤션에 따라 테스트를 찾게 됩니다.
파일은
test_
로 시작클래스는
Test
로 시작물론 이외에도 다양한 파일명들을 찾거나 다른 설정을 할 수도 있습니다.
pytest 실행하기
CLI 실행
pytest 는 어렵지 않게 파이썬의
pip
으로 설치하게 되면 CLI 도 제공하기 때문에 다음과 같이 실행할 수 있습니다.$ pytest
물론, 이렇게 실행을 하게 되면 현재 디렉토리 기준으로 실행을 하지만 경로 문제로 실행이 잘 안 됩니다.
따라서 보통은 파이썬의
-m
을 사용해 실행합니다.$ python -m pytest
실행 결과
실행하게 되면 테스트의 결과는 다음과 같이 3가지로 나뉩니다.
.
: 테스트가 통과했을 때 등장합니다.F
: 테스트가 실패했을 때 등장합니다.E
: 테스트를 실행했을 때 예상치 못한 에러를 마주친 상황에서 등장합니다.또한 pytest 가 종료되면서 해당 테스트 실행 결과에 대한 코드를 알려줍니다. 이를 Exit Code 라고 하며 종류는 다음과 같습니다:
Exit code 0: 모든 테스트가 정상적으로 수집되고 통과했습니다.
Exit code 1: 테스트가 수집되었지만 부분적으로 실패했습니다.
Exit code 2: 테스트 실행이 유저에 의해 방해되었습니다.
Exit code 3: 테스트를 실행하는 도중에 내부 에러가 발생했습니다.
Exit code 4: pytest CLI 에서 에러가 발생했습니다.
Exit code 5: 수집된 테스트가 없습니다.
Fixtures
강력한 이유 중 하나는 fixture 를 사용해서 입니다. 간단히 봐서는 테스트를 설정하는 사전 단계에서
pytest
는 미리 함수들을 만들고 테스트 도중에 해당 함수들을 접근하게 해줍니다.이렇게 사전에 만들어 놓은 함수들을 테스트 함수에서 사용할 수 있는 부분은 매우 유용합니다.
한번 예시를 들어보겠습니다.
먼저 다음과 같은
format_data_for_display
라는 함수를 테스트한다고 하겠습니다.def format_data_for_display(people): formatter = lambda p: f"{p['given_name']} {p['family_name']}: {p['title']}" return [formatter(p) for p in people] def test_format_data_for_display(): people = [ { "given_name": "Alfonsa", "family_name": "Ruiz", "title": "Senior Software Engineer", }, { "given_name": "Sayid", "family_name": "Khan", "title": "Project Manager", }, ] assert format_data_for_display(people) == [ "Alfonsa Ruiz: Senior Software Engineer", "Sayid Khan: Project Manager", ]
만약에 위 테스트 함수
test_format_data_for_display
에서 사용되는people
리스트가 다른 테스트 케이스에서도 반복되어서 사용이 된다면 다음과 같이 따로 빼서 사용할 수 있습니다.import pytest @pytest.fixture def example_people_data(): return [ { "given_name": "Alfonsa", "family_name": "Ruiz", "title": "Senior Software Engineer", }, { "given_name": "Sayid", "family_name": "Khan", "title": "Project Manager", }, ]
fixture
로 정의했다면 다음과 같이 테스트 함수에서 해당fixture
를 불러와 사용할 수 있습니다.def test_format_data_for_display(example_people_data): assert format_data_for_display(example_people_data) == [ "Alfonsa Ruiz: Senior Software Engineer", "Sayid Khan: Project Manager", ] def test_format_data_for_excel(example_people_data): assert format_data_for_excel(example_people_data) == """given,family,titl e Alfonsa,Ruiz,Senior Software Engineer Sayid,Khan,Project Manager """
setup 과 teardown 에 사용하기
pytest
는 기본적으로 테스트를 돌릴 때 함수 단위로 실행하기 때문에unittest
와 같은 클래스의 메소드를 사용하지 않습니다. 따라서 따로[conftest.py](http://conftest.py)
에서나 사전에 정의한 함수들의 도움을 받아 사용할 수 있습니다.예를 들어 데이터베이스를 연결하는 상황을 보겠습니다.
# conftest.py import os import sqlite3 TEST_DB_PATH = os.path.join(os.getcwd(), 'test_db.sqlite3') @pytest.fixture(scope="session") def get_conn(): _conn = sqlite3.connect(TEST_DB_PATH) yield _conn _conn.close() @pytest.fixture(scope="session") def get_cursor(get_conn): cursor = get_conn.cursor() yield cursor cursor.close()
그리고 실제로 사용하게 될 때는 다음과 같이 사용할 수 있습니다.
def test_table(get_cursor): query = """CREATE TABLE user ( id INTEGER PRIMARY KEY, username VARCHAR(32) )""" get_cursor.execute(query) assert True
즉, 위 코드처럼
yield
를 사용하게 되면 테스트 함수가 실행될 때 해당 함수를 불러오고 테스트 함수가 종료될 때yield
다음 줄이 실행되면서 자연스럽게 데이터베이스 연결을 종료해 줄 수 있습니다.또한
fixture
를 정의할 때 추가로 키워드 인수들을 넘길 수 있습니다. 이에 대한 부분은 공식문서를 확인해주세요.적당한 사용법*
동일한 값을 여러 테스트 함수에서 반복적으로 사용할 때
데이터베이스나 서버 등 특정 설정을 미리 해야 할 때
setup 과 teardown 등이 필요할 때
적당하지 않은 사용법*
값이 조금씩 다르게 필요한 테스트를 실행할 때
파이썬 딕셔너리 등 일반적인 객체를 그냥 저장하는 용도로는 무의미, 비효율
Fixtures at Scale*
테스트 실행 전에 필요한 사전 설정들이 더 많아질 수록 관리가 힘들어집니다. 다행히도
fixture
는 모듈로 동작할 수 있습니다. 즉, 파일 단위로 사용 가능하다는 것이죠.또한 서로 간에 의존을 할 수도 있습니다. 따라서 파일 단위로 분리를 해 필요한 함수들을 따로 모아놓을 수 있습니다.
conftest.py
pytest
는 테스트를 실행하게 되면[conftest.py](http://conftest.py)
라는 파일을 찾게 됩니다. 해당 파일에서 주로 사전 설정들을 담아놓습니다.폴더의 구조는 다음과 같을 수 있습니다.
tests ├── conftest.py ├── test_1 │ ├── conftest.py │ ├── __init__.py │ └── tester_mults.py └── test_2 ├── conftest.py ├── __init__.py └── testing_etc.py
위 구조처럼 각 테스트 폴더는 하나의
__init__.py
를 가지게 되면서 서로 다른 패키지로 인식을 하게 되고 이렇게 패키지가 다르다는 것을 통해서 테스트를 실행할 때 각 테스트 파일 간에 파이썬의import
를 사용할 수 있습니다.Marks
pytest
에서는 함수들을 마킹할 수 있습니다. 쉽게 말해 그룹화할 수 있고 테스트를 실행할 때 특정 그룹들만 실행할 수 있습니다.기본으로 설정되어 있는 옵션들은 다음과 같습니다:
skip
: 테스트를 실행하지 않고 스킵합니다.skipif
: 넘겨진 특정 조건이True
일 경우 스킵합니다.xfail
: 테스트가 실패할 거라고 명시합니다. 따라서 테스트가 실패하더라도 전체적인 테스트는 통과 상태로 인식됩니다.parametrize
: 여러 값들을 한 함수에 전달할 수 있는 방법입니다.예시를 보겠습니다.
@pytest.mark.skip def test_1_plus_2(): assert (1 + 2) == 3
parametrize
는 테스트 함수에서 사용되는 파라미터에 값들을 전달할 때 사용할 수도 있습니다.예를 들어 다음과 같은 함수들이 존재할 때
def test_is_palindrome_empty_string(): assert is_palindrome("") def test_is_palindrome_single_character(): assert is_palindrome("a") def test_is_palindrome_mixed_casing(): assert is_palindrome("Bob") def test_is_palindrome_with_spaces(): assert is_palindrome("Never odd or even") def test_is_palindrome_with_punctuation(): assert is_palindrome("Do geese see God?") def test_is_palindrome_not_palindrome(): assert not is_palindrome("abc") def test_is_palindrome_not_quite(): assert not is_palindrome("abab")
보시면 패턴이 존재합니다.
def test_is_palindrome_<in some situation>(): assert is_palindrome("<some string>")
그렇다면 다음과 같이
pytest.mark.parametrize
를 사용해서 정리할 수 있습니다.@pytest.mark.parametrize("palindrome", [ "", "a", "Bob", "Never odd or even", "Do geese see God?", ]) def test_is_palindrome(palindrome): assert is_palindrome(palindrome) @pytest.mark.parametrize("non_palindrome", [ "abc", "abab", ]) def test_is_palindrome_not_palindrome(non_palindrome): assert not is_palindrome(non_palindrome)
Plugins
pytest
는 다양한 플러그인을 제공합니다. 각 플러그인을 사용할 때에는 대부분pip
으로 설치가 가능하며 테스트를 작성하거나 실행될 때 자동으로 경로에 포함이 됩니다.
'Programming Languages > Python' 카테고리의 다른 글
Python Testing (0) | 2022.06.01 |
---|---|
파이썬 가상환경을 왜 써야할까...? (0) | 2022.05.31 |