본문 바로가기
Programming Languages/Python

Python Pytest 관해서

by Calvin H. 2022. 6. 1.

0. 가이드에 들어가기 전에

pytest 에 대한 설명을 담은 페이지입니다.

여기에서는 전반적인 설명과 사용법을 다루며 세부사항을 깊게 보지는 않습니다. 추가적인 자세한 내용은 공식문서를 참고해 주시길 바랍니다.

pytest 공식문서

Full pytest documentation - pytest documentation


1. 테스트에 대해서

코딩을 하게 되면 해당 코드가 의도대로 작동하는지, 어떤 부분들을 확인해야 하는지 등 고려해야 할 부분들이 몇몇 있습니다. 따라서 코드가 정상적으로 작동하는지 확인을 하기 위해 추가로 코드를 작성해 확인하는 과정을 거칩니다.

이 때 코드를 추가로 작성한다는 것은 양이 추가되기도 하지만 또다른 코드가 등장하는 것입니다. 즉, 이 테스트 코드를 테스트할 코드를 작성하지 않는 한 해당 코드는 최대한 꼼꼼하게, 문제없이 작동하도록 해야 합니다.

코드를 테스트할 때에는 어떤 것을 확인하는지에 따라 3가지로 분류됩니다.

  1. Unit testing: 하나의 어플리케이션 내에 있는 개별 모듈들을 (별도의 의존성 없이) 독립적으로 확인합니다.
  2. Integration testing: 한번에 여러개의 모듈들을 실행할 때 정상적으로 작동이 되는지 확인합니다.
  3. Functional testing: 하나의 시스템에서 특정 기능이 작동하는지 확인합니다. 이 과정에서 의존성이 있는 dependencies 들과 상호작용할 수 있습니다.

2. 테스트와 개발

해당 부분은 다양한 소프트웨어 테스트에 대한 사이트를 참고하시면 도움이 됩니다.

Software Testing Dictionary

테스트 주도 개발 (TDD)

가장 많이 접했을 용어 중 하나는 '테스트 주도 개발' 혹은 'Test Driven Development (TDD)' 입니다.

TDD 는 쉽게 말해 어플리케이션 코드를 작성하기 전에 수행해야 하는 작업들을 먼저 테스트로 작성하는 것입니다.

예를 들어 숫자가 주어지면 해당 숫자의 제곱을 리턴하는 함수를 작성하고 싶다고 하겠습니다. 그렇다면 다음과 같은 순서로 개발을 하게 됩니다.

  1. 제곱 함수에 대한 테스트를 작성합니다.
  2. 테스트를 실행합니다.
  3. 코드를 작성하거나 수정합니다.
  4. 1번에서 작성한 테스트가 통과할 때까지 2, 3번들을 반복합니다.

TDD 의 장단점

  • 테스트를 먼저 작성하기 때문에 요구사항이 명확해집니다.
  • 테스트를 작성하면서 코드의 방향성이 명확해집니다.
  • 개발 속도가 느릴 수도 있습니다.

이외에도 BDD, 다른 개발 방식 등이 존재합니다.

테스트와 개발은 분리할 수가 없습니다. 테스트 코드가 없는 개발은 존재할 수 있지만 테스트가 없는 개발이란 존재하기 힘듭니다. 설령 코드가 아니더라도 직접 개발자가 확인하는 과정도 테스트에 포함된다고 볼 수 있습니다. 즉, 테스트 코드를 작성한다는 것은 개발자가 확인해야 하는 부담을 코드가 가져가는 행위로도 볼 수 있습니다.


3. 파이썬 테스트 프레임워크에 대해서

파이썬에서 많이들 사용하는 unittest 와 pytest 에 대해서 알아보겠습니다.

  1. unittest

    소개

    파이썬에서 기본으로 제공하는 테스트 라이브러리입니다. 즉, 추가로 설치할 필요가 없고 파이썬을 사용하는 코드에서는 대부분 사용할 수 있다는 장점이 있습니다.

    또한 기본적인 디자인은 파이썬의 객체지향프로그래밍을 따라 클래스 형태로 테스트를 구분하며 각 클래스에서는 사전에 지정된 메소드를 통해서 테스트의 setUptearDown 등을 실행할 수 있습니다.

    설치

    추가 설치가 없습니다.

    코드

     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

  2. pytest

    소개

    파이썬에서 가장 널리 사용되고 있는 테스트 프레임워크입니다. 유연성과 다양한 플러그인 그리고 무엇보다 파이썬의 unittest 라이브러리도 같이 사용할 수 있다는 큰 장점이 특징들입니다.

    pytest 는 함수를 중심으로 작동합니다. 즉, 같이 사용할 수는 있지만 클래스를 통해 구분하고 나뉘는 unittest 와는 다르게 작동합니다. 따라서 setupteardown 등은 따로 작성해야 하며 필수는 아니지만 보통 [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 테스트는 주로 다음과 같은 패턴을 따릅니다:

    1. Arrange: 테스트를 실행하기 위한 사전 조건들을 셋업합니다.

    2. Act: 특정 함수나 메소드를 실행합니다.

    3. 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 를 정의할 때 추가로 키워드 인수들을 넘길 수 있습니다. 이에 대한 부분은 공식문서를 확인해주세요.

      API Reference - pytest documentation

    • 적당한 사용법*

    • 동일한 값을 여러 테스트 함수에서 반복적으로 사용할 때

    • 데이터베이스나 서버 등 특정 설정을 미리 해야 할 때

    • 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