pytest를 이용한 파이썬 테스트 코드 작성 - 개념편-


이번 포스팅에서는 python을 이용한 테스트 코드를 작성하는 방법 중 pytest를 이용해서 테스트 코드를 작성하는 방법에 대해 알아보고자 합니다. 더 나아가 Copilot와 ChatGPT API를 이용해서 테스트 코드 작성하는데 도움을 얻는 방법도 알아 보고자 합니다.
이런 방법을 알아보기 전에 DevOps를 위한 Pytest 챕터를 스터디하고 정리한 개념적인 내용에 대해 한번 알아보도록 하겠습니다.


1. 레이아웃 규약

■ 테스트 파일 명은 test_로 시작해야 한다.

  • 실행

    1
    $ pytest -q
  • 실패 케이스

    1
    no tests ran in 0.00s
  • 성공 케이스

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    .F                                                                                                           [100%]
    ===================================================== FAILURES =====================================================
    ____________________________________________________ test_fails ____________________________________________________

    def test_fails():
    > assert False
    E assert False

    test_basic.py:6: AssertionError
    ============================================= short test summary info ==============================================
    FAILED test_basic.py::test_fails - assert False
    1 failed, 1 passed in 0.03s

■ 테스트 파일은 test_basic.py와 같이 접두사로 test_를 붙이거나 test.py로 끝나야 한다.

■ 테스트 함수는 def test_simple()과 같이 접두사로 test_가 붙어야 한다.

■ 테스트 클래스는 class TestSimple와 같이 접두사로 Test가 붙어야 한다.

■ 테스트 메서드는 함수와 동일한 규약을 따라def test_method(self)와 같이 test_가 붙은 접두사로 붙는다.



2. unittest vs pytest 차이점

pytestunittest는 모두 Python에서 널리 사용되는 테스트 프레임워크입니다. 두 프레임워크는 모두 테스트를 작성하고 실행하는 데 사용되지만, 다음과 같은 차이점이 있습니다.

  • unittest는 클래스와 클래스 상속을 반드시 사용해야 합니다.

    1
    self.assertEqual(a, b)
  • pytest는 함수를 사용합니다.

    1
    assert a == b
  • pytest는 python의 내장 assert문을 사용하여 테스트 결과를 검증합니다. 이로 인해 pytest의 단언문이 더 간결하며, 읽기 쉽다는 장점이 있습니다.

    • unittest
      1
      self.assertEqual(a, b)
    • pytest
      1
      assert a == b

2-1 테스트 구조

  • unittest는 객체지향 스타일의 테스트를 지원하며, 테스트는 TestCase 서브클래스로 정의되고, 각 테스트 메소드는 test_로 시작하는 이름을 가져야 합니다.
  • setUptearDown 메서드를 통해 테스트 전/후에 실행할 코드를 정의할 수 있습니다. 반면에 pytest는 함수형 스타일의 테스트를 지원하며, 테스트 설정 및 정리는 픽스처(fixture)를 통해 수행합니다.

unittest

  • 예시
    1
    2
    3
    4
    5
    6
    7
    class TestExample(unittest.TestCase):
    def setUp(self):
    self.a = 1
    self.b = 2

    def test_add(self):
    self.assertEqual(self.a + self.b, 3)

pytest

  • 예시

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    import pytest

    @pytest.fixture
    def numbers():
    a = 1
    b = 2
    return a, b

    def test_add(numbers):
    a, b = numbers
    assert a + b == 3
  • 결과

    1
    pytest -v test_basic.py
  • 테스트 정상적으로 통과시

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    =============================================== test session starts ================================================
    platform darwin -- Python 3.8.0, pytest-7.3.1, pluggy-1.0.0 -- /Users/kojaeyeong/anaconda3/envs/pytorch/bin/python
    cachedir: .pytest_cache
    rootdir: /Users/kojaeyeong/Documents/Study/Python-for-DevOps/8장
    plugins: anyio-3.6.2
    collected 1 item

    test_basic.py::test_add PASSED [100%]

    ================================================ 1 passed in 0.02s =================================================
  • 테스트 통과 실패시

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    test_basic.py::test_add FAILED                                                                               [100%]

    ===================================================== FAILURES =====================================================
    _____________________________________________________ test_add _____________________________________________________

    numbers = (1, 2)

    def test_add(numbers):
    a, b = numbers
    > assert a + b == 5
    E assert (1 + 2) == 5

    test_basic.py:11: AssertionError
    ============================================= short test summary info ==============================================
    FAILED test_basic.py::test_add - assert (1 + 2) == 5
    ================================================ 1 failed in 0.05s =================================================

2-2 플러그인 지원

pytest는 풍부한 플러그인 생태계를 갖추고 있습니다. 이는 테스트 작성에 더 많은 유연성을 제공합니다. 예를 들어, pytest-xdist 플러그인을 사용하면 여러 CPU 코어를 사용하여 테스트를 병렬로 실행할 수 있습니다. pytest-cov 플러그인을 사용하면 테스트 커버리지를 측정할 수 있습니다.

2-3 테스트 발견

pytesttest_로 시작하는 함수를 자동으로 테스트로 인식합니다. 반면에 unittestunittest.TestCase 서브클래스의 test_로 시작하는 메소드만 테스트로 인식합니다. 따라서 unittest는 테스트를 작성할 때 클래스를 사용해야 하지만, pytest는 함수를 사용할 수 있습니다.



3. Pytest 특징

3-1. conftest.py을 이용한 테스트 설정 및 픽스처 관리

conftest.py 파일을 사용하여 중복을 줄이고 테스트 환경을 더욱 관리하기 쉽게 만들 수 있습니다. conftest.py는 테스트를 실행하기 전/후에 실행되는 코드를 정의하는데 사용되며, conftest.py에 정의된 픽스처는 해당 디렉토리의 모든 테스트에서 재사용할 수 있습니다.

  • 테스트 설정

    1
    2
    3
    4
    5
    6
    7
    8
    # conftest.py
    import pytest

    @pytest.fixture(scope="session", autouse=True)
    def setup():
    print("테스트 시작")
    yield
    print("테스트 종료")
    1
    2
    3
    # test_basic.py
    def test_add():
    assert 1 + 2 == 3
  • 실행

    1
    pytest -v test_basic.py
  • 테스트 정상적으로 통과시

    1
    2
    3
    4
    5
    6
    =============================================== test session starts ===============================================
    collected 1 item

    test_basic.py::test_add PASSED [100%]

    ================================================ 1 passed in 0.01s ================================================
  • 테스트 통과 실패시

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    test_basic.py::test_add FAILED                                                                               [100%]

    ===================================================== FAILURES =====================================================
    _____________________________________________________ test_add _____________________________________________________

    def test_add():
    > assert 1 + 2 == 6
    E assert (1 + 2) == 6

    test_basic.py:2: AssertionError
    ---------------------------------------------- Captured stdout setup -----------------------------------------------
    테스트 시작
    --------------------------------------------- Captured stdout teardown ---------------------------------------------
    테스트 종료
    ============================================= short test summary info ==============================================
    FAILED test_basic.py::test_add - assert (1 + 2) == 6
    ================================================ 1 failed in 0.04s =================================================

3-2. 간결한 테스트 작성

간단하게 내가 만든 함수를 테스트 해보고 싶은 경우가 있을 것 입니다. 이럴 경우 거창하게 할 필요 없이 간단하게 입력 인자와 리턴 결과가 옳바른지 확인하고 싶을때 pytest의 assert문을 사용해서 간단하게 테스트를 작성하고 검증해볼 수 있습니다. assert는 다음과 같은 역할을 할 수 있습니다.

  • assert python의 내장 키워드로써, 주어직 조건이 참(True)인지 검증하는데 사용됩니다.
  • assert문 뒤에 조건이 False라면 AssertionError 예외를 발생시킵니다.
    이처럼 assert문을 사용하면 코드의 특정 부분이 정상 동작하는지 또는 특정 조건을 만족하는지 쉽게 확인이 가능해집니다. 이 방법은 pytest의 가장 기본적인 테스트 케이스 작성 방법이며 단순하고 직관적으로 함수나 메소드의 기대되는 동작을 검증합니다. 다음은 테스트 코드의 간단한 예시를 알아보도록 하겠습니다.

assert문을 사용한 간단한 테스트

우선 테스트할 함수를 작성합니다. 이 함수는 두 개의 숫자를 입력받아 더한 값을 리턴하는 간단한 함수입니다.

1
2
3
# my_module.py
def add(a, b):
return a + b

이제 이 함수를 테스트하는 코드를 작성해보겠습니다. 테스트 코드는 test_로 시작하는 파일에 작성합니다. 그리고 assert문을 사용해 예상 결과를 확인할 수 있습니다. 테스트가 정상적이면 에러 메세지가 안뜨고 테스트 실패하면 에러 메세지가 뜹니다.

1
2
3
4
5
6
7
8
9
10
# test_my_module.py
import my_module

def test_add():
result = my_module.add(3, 4)
assert result == 7

def test_add_with_strings():
result = my_module.add('Hello, ', 'world!')
assert result == 'Hello, world!'

pytest 는 특정 패턴(test_*.py 또는 _test.py 파일 내의 test_ 로 시작하는 함수)을 따르는 테스트를 자동으로 찾아냅니다. 그래서 위 테스트 코드를 실행하기 위해서는 커멘드 창에 다음과 같이 입력해주면 됩니다.

1
pytest

테스트 성공

1
2
3
4
5
6
7
8
9
=============================================== test session starts ================================================
platform darwin -- Python 3.8.16, pytest-7.3.1, pluggy-1.0.0
rootdir: /Users/kojaeyeong/Documents/Study/Python-for-DevOps/8장/8-1
plugins: anyio-3.4.0
collected 2 items

test_my_module.py .. [100%]

================================================ 2 passed in 0.02s =================================================

테스트 실패

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
=============================================== test session starts ================================================
platform darwin -- Python 3.8.16, pytest-7.3.1, pluggy-1.0.0
rootdir: /Users/kojaeyeong/Documents/Study/Python-for-DevOps/8장/8-1
plugins: anyio-3.4.0
collected 2 items

test_my_module.py .F [100%]

===================================================== FAILURES =====================================================
______________________________________________ test_add_with_strings _______________________________________________

def test_add_with_strings():
result = my_module.add('Hello, ', 'world!')
> assert result == 'ello, world!'
E AssertionError: assert 'Hello, world!' == 'ello, world!'
E - ello, world!
E + Hello, world!
E ? +

test_my_module.py:9: AssertionError
--------------------------------------------- Captured stdout teardown ---------------------------------------------
테스트 종료
============================================= short test summary info ==============================================
FAILED test_my_module.py::test_add_with_strings - AssertionError: assert 'Hello, world!' == 'ello, world!'
=========================================== 1 failed, 1 passed in 0.09s ============================================

3-4. 픽스처 (Fixture)

  • **pytest**의 픽스처 시스템은 테스트 전후에 실행되는 코드를 정의하고 관리하는 데 유용하다. 이를 통해 테스트의 독립성을 보장하고 코드 중복을 줄일 수 있다.

  • Fixture 기능은 테스트 전후에 실행되어야 하는 코드를 정의하고, 이를 재사용 가능하게 만들어주는 기능.

  • 이는 DB 연결, 테스트 데이터, APP Instance 등 설정과 같은 여러 테스트에서 공통으로 사용되는 설정을 관리하는데 유용하다.

  • **Fixture**는

    1. import pytest 선언

    2. 함수를 정의

    3. @pytest.fixture 데코레이터를 사용하여 선언한다.

    4. 이후 Fixture 기능을 사용하려는 테스트 함수는 해당 Fixture 함수의 이름을 인자로받는다.

      ⇒ 이렇게 하면 pytest는 해당 테스트를 실행하기 전 Fixture 함수를 호출하고, 그 결과를 테스트 함수에전달한다.

1
2
3
4
5
6
7
8
9
import pytest

# Fixture 정의
@pytest.fixture
def example_data():
return [1, 2, 3, 4, 5]

def test_mean(example_data):
assert sum(example_data) / len(example_data) == 3
- pytest를 실행하면 test_mean 함수를 실행하기 전에 먼저 example_data Fixture가 실행되고 그 return 값이 test_mean 함수의 인자로 전달된다. 이런 방식으로 Fixture는 테스트간에 재사용이 될 수 있으며 테스트의 독립성을 유지하는데 도움을 준다.

3-5. 풍부한 플러그인 생태계

  • **pytest**는 많은 내장 및 서드파티 플러그인을 지원한다. 이는 테스트를 더욱 쉽게 작성하고 관리하며, 사용자가 필요로 하는 특정 기능을 추가할 수 있도록 한다.

  • 대표 pytest 플러그인 리스트

    1. pytest-cov: 이 플러그인은 테스트 커버리지 보고서를 생성한다. 즉, 어떤 코드가 테스트에 의해 실행되었는지, 어떤 코드가 실행되지 않았는지에 대한 정보를 제공한다. 이러한 정보는 테스트가 코드의 어떤 부분을 놓치고 있는지 파악하는 데 유용하다.
    2. pytest-mock: 이 플러그인은 테스트에서 mocking을 보다 쉽게 사용할 수 있도록 돕는다. Mocking은 테스트에서 실제 객체나 함수를 가짜 객체나 함수로 대체하는 것을 의미하며, 외부 시스템과의 상호작용이나 시간이 오래 걸리는 작업을 제어하는 데 유용하다.
    3. pytest-xdist: 이 플러그인은 여러 CPU나 머신에서 테스트를 동시에 실행하여 테스트 실행 시간을 줄여준다. 이는 대규모 테스트 스위트에서 특히 유용하다.
    4. pytest-django: 이 플러그인은 Django 프로젝트의 테스팅을 위해 특화된 기능을 제공한다. 예를 들어, Django 설정을 **pytest**에 통합하거나, Django 데이터베이스를 위한 픽스처를 제공하는 등의 기능을 가지고 있다.

3-6. 유연한 테스트 구성

  • **pytest**는 다양한 옵션과 설정을 통해 테스트를 매우 유연하게 구성할 수 있다. 예를 들어, 특정 테스트만 선택하여 실행하거나, 테스트 출력의 형식을 변경하는 것이 가능.

  • 마커를 이용한 테스트 분류와 선택

    • pytest**에서는 **@pytest.mark 데코레이터를 사용하여 테스트를 마킹하고, 커맨드라인 옵션을 통해 특정 마커를 가진 테스트만 실행하거나 제외할 수 있다. 예를 들어, **@pytest.mark.slow**로 느린 테스트를 마킹하고, 빠른 피드백이 필요할 때는 이러한 테스트를 제외하고 실행할 수 있다.

      1
      2
      3
      @pytest.mark.slow
      def test_large_computation():
      ...
      1
      2
      3
      '''위와 같이 테스트를 마킹한 후, -m 옵션으로 특정 마커를 가진 테스트만 실행하거나 제외할 수 있다 '''

      pytest -m "not slow" # 'slow' 마커가 없는 테스트만 실행

3-7. 파라미터화된 테스트

  • pytest**의 **@pytest.mark.parametrize 데코레이터를 사용하면, 동일한 테스트를 다양한 입력값으로 반복 실행할 수 있다. 이를 통해 코드의 다양한 상황에 대한 처리를 간결하고 효율적으로 테스트할 수 있다.
    1
    2
    3
    @pytest.mark.parametrize("num1, num2, expected", [(1, 2, 3), (2, 3, 5), (3, 5, 8)])
    def test_addition(num1, num2, expected):
    assert num1 + num2 == expected
    • test_addition 테스트를 세 번 실행하는데, 각각 다른 입력값을 사용한다

3-8. Hooks

  • **pytest**의 Hook 시스템은 테스트 실행의 다양한 단계에서 사용자 코드를 실행하도록 하는 강력한 기능이다. 이를 통해 테스트 환경을 세부적으로 제어하고, 테스트 보고의 형식을 사용자 정의하며, 테스트 실패 시 추가 동작을 정의하는 등 다양한 확장이 가능하다.

  • pytest_runtest_protocol

    • 테스트 항목이 실행되는 방식을 변경할 수 있다. 사용자는 이 hook을 재정의하여 테스트 항목이 실행되는 과정을 완전히 제어할 수 있다.
      1
      2
      3
      4
      5
      6
      def pytest_runtest_protocol(item, nextitem):
      print(f"\n\nStarting test: {item.nodeid}\n\n")
      # 상위 클래스의 runtest_protocol 메소드를 호출하여 테스트 실행을 위임
      outcome = item.ihook.pytest_runtest_protocol(item=item, nextitem=nextitem)
      print(f"\n\nFinished test: {item.nodeid}\n\n")
      return outcome
      위 예제는 pytest_runtest_protocol을 재정의하여 각 테스트가 실행되기 전과 후에 메시지를 출력한다.
  • pytest_configure

    • 이 hook은 pytest 설정이 완료된 후에 실행된다. 사용자는 이 hook을 재정의하여 테스트 세션 전반에 걸쳐 사용할 설정이나 상태를 추가할 수 있다.

    • 사용자 정의 마커 **custom_marker**를 설정 파일에 추가

      1
      2
      3
      4
      5
      def pytest_configure(config):
      # pytest 실행시 사용자 정의 설정을 추가
      config.addinivalue_line(
      "markers", "custom_marker: Mark test as a custom_marker test."
      )
      • pytest가 인식하는 마커를 확장하는데 사용할 수 있다. 이렇게 하면 테스트를 마킹하고 이러한 마커를 기반으로 테스트를 선택하거나 제외하는 것이 가능해진다.
    • 사용자 정의 플러그인을 등록

      1
      2
      3
      def pytest_configure(config):
      # 사용자 정의 플러그인 등록
      config.pluginmanager.register(MyPlugin(), "myplugin")
      • pytest_configure 함수 내에서 pytest 플러그인 관리자를 사용하여 플러그인을 등록할 수 있다.
  • pytest_report_header

    • 테스트 보고서의 헤더를 사용자 정의하는 데 사용된다. 사용자는 이 hook을 재정의하여 테스트 보고서에 추가 정보를 포함시킬 수 있다.
      1
      2
      def pytest_report_header(config):
      return f"프로젝트 이름: 내 프로젝트, pytest 버전: {config.getoption('version')}"
      1
      2
      3
      4
      ============================= test session starts ==============================
      프로젝트 이름: 내 프로젝트, pytest 버전: x.y.z
      platform ...
      ...

4. 인프라스트럭처 테스트

  • 인프라스트럭처 테스트는 IT 시스템의 인프라스트럭처 부분에 초점을 맞춘 테스트 활동이다. 인프라스트럭처 테스트는 서버, 데이터베이스, 네트워크 기기, 보안 시스템, 클라우드 리소스 등 소프트웨어가 적절하게 작동하는데 필요한 기본 시스템 요소가 적절하게 설정되어 있는지 확인하는데 중점을 둔다.
  • DevOps 및 CI/CD 파이프라인에서 인프라스트럭처 테스트는 필수적이다. 이것은 프로덕션 환경에서 발생할 수 있는 문제를 미리 감지하고, 애플리케이션의 안정성과 성능을 보장하는데 큰 도움을 준다.

4-1 인프라스트럭처 테스트 유형

  1. 파라미터화된 테스트: pytest는 테스트 파라미터화를 지원하여 다양한 입력에 대해 동일한 테스트를 쉽게 반복 실행할 수 있습니다.

  2. 성능 테스트: 시스템 인프라스트럭처가 사용자의 요구를 충족시키는 성능을 발휘하는지 테스트한다. 보통 사용자의 트래픽이나 데이터 처리에 대한 부하를 시뮬레이션하여 시스템이 요구 사항을 충족하는지 확인한다.

  3. 리소스 테스트: 시스템의 CPU, 메모리, 디스크 공간 등과 같은 리소스가 적절하게 활용되고 있는지 확인한다. → 비용

  4. 네트워크 테스트: 네트워크 속도, 안정성, 보안 등의 측면에서 시스템이 적절하게 동작하는지 확인한다.

  5. 통합 테스트: 여러 시스템 컴포넌트가 원활하게 함께 작동하는지 확인한다.

    → 코드에서는 동작 잘 이루어짐 but 실제는 X

    → 부하 테스트 진행, 네트워크 부하 테스트 진행 → 잘 됨 → 실제 배포 (잘 안됨)

    → 1, 2, 3, 4번 다 포함해서 동시해 실행.

4-2 인프라르트럭처 테스트를 위한 도구

  • infrastructure as Code (IaC)를 사용하여 인프라를 프로그래밍 방식으로 관리하고 테스트하는 데 도움을 주는 도구
    • Ansible, Terraform, Chef, Puppet, Docker
  • 인프라스트럭처 테스트를 수행하는 데 특화된 도구
    • Serverspec, Testinfra, Inspec

Testinfra 예시

  • Testinfra는 Python 기반의 테스트 프레임워크로, 인프라스트럭처 테스트에 주로 사용된다. Testinfra를 사용하면 서버의 구성 상태를 검증하거나 실행 중인 서비스를 확인하는 등의 작업을 쉽게 수행할 수 있다.

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    '''
    Testinfra를 사용하여 리눅스 시스템에서 특정 파일이 존재하고 적절한 권한이 설정되어 있는지 확인하는 간단한 예시
    -> Ansible(using Bash shell)로도 간단하게 해결 가능.
    '''
    def test_passwd_file(host):
    passwd = host.file("/etc/passwd")
    assert passwd.exists
    assert passwd.user == "root"
    assert passwd.group == "root"
    assert passwd.mode == 0o644

    • host.file 메소드는 주어진 경로의 파일에 대한 File 객체를 반환하고, 이 객체의 속성과 메소드를 사용하여 파일에 대한 다양한 검증을 수행

      1
      2
      3
      4
      5
      6
      7
      '''
      Testinfra를 사용하여 특정 서비스가 실행 중인지 확인하는 예시
      '''
      def test_nginx_is_running(host):
      nginx = host.service("nginx")
      assert nginx.is_running
      assert nginx.is_enabled
    • host.service 메소드는 주어진 이름의 서비스에 대한 Service 객체를 반환하고, 이 객체의 속성을 사용하여 서비스가 실행 중이며 부팅 시에 자동으로 시작되도록 설정되어 있는지 확인

      1
      2
      3
      4
      5
      '''
      Testinfra 테스트는 일반적인 Pytest 테스트와 동일하게 실행할 수 있다. 예를 들어, 위의 테스트를 test_infra.py라는 파일에 저장한 경우,
      테스트를 실행하기 위해 다음과 같이 입력할 수 있다
      '''
      pytest test_infra.py

Testinfra에서 지원하는 모든 연결 타입

  • 로컬
  • Paramiko (python으로 ssh 연결 수행해서 작업할 수 있는 것)
  • Docker
  • SSH
  • Salr
  • Ansible
  • Kubernetes (kubectl을 통해 연결)
  • WinRM
  • LXC