안녕하세요. Bappe 입니다.

최근 장마와 무더위, 태풍 그리고 또 다시 활발해진 그 녀석으로 인해 어수선하네요. 이럴 때 일수록 행복한 일, 좋은 일만 가득해서 잘 이겨내시길 바라겠습니다.

pytest

이번 글에서는 pytest를 알아보고자 합니다.

pytest란 무엇인가? 정말 이름 그대로 py(thon)을 test 하는 프레임워크를 의미합니다.
pytest 공식 홈페이지에서는 다음과 같이 설명하고 있습니다.

pytest is a mature full-featured Python testing tool that helps you write better programs.

Python testing tool로써 좋은 프로그램을 작성하도록 도와준다라, 이걸 이해하기 위해서는 왜 테스트가 존재해야 하는가에 대해 생각할 필요가 있습니다.

TDD

최근 많이 주목받고 있는 TDD (Test Driven Development)를 아시나요? 짧게 설명드리면 본격적인 개발에 들어가기 전에 테스트 계획 및 코드를 작성하는 것을 의미합니다. 테스트가 개발을 이끌어 나가는 것이 되는 것이죠.

그럼, 왜 TDD 같은 테스트가 우선시 되는 개발이 나오게 되었고, 주목받고 있는 것일까요?

자, 개발 중 에러 및 오류가 발생했다고 합시다.

이 상황이 정말 작은 소규모 개발 중에 일어난 것이라면 사실 큰 문제가 되지 않습니다. 개발한 모듈간에 연결성도 적을 뿐 아니라, 코드 양이 방대하지 않을 것이기 때문에 바로 찾아 문제를 해결할 수 있을 것입니다.

하지만 아주 대규모의 개발 상황이라고 가정해봅시다. 수 많은 모듈, 함수간 종속성과 매우 많은 코드 양이 있기 때문에 오류 및 에러를 잡는데 많은 시간과 인력을 투입하게 될 것입니다. 이러한 상황은 비즈니스적으로도 효율적이지 못하겠죠? 당연히 안정적인 프로그램을 개발해 나가는데도 많은 걸림돌이 될 것입니다.

이러한 문제를 해결하기 위해 TDD. 즉, 테스트 주도 개발이 나오게 된 것입니다. 그리고 Python에서 TDD를 하기 위해 나온 프레임워크가 pytest입니다.

예시

pytest는 대규모 오픈소스에서도 사용이 되고 있습니다.

데이터 분석에서 많이 사용되고 있는 pandas에서도 pytest를 통해 코드 테스트를 진행하고 있습니다.
pandas 공식 문서에서 test의 중요성을 이야기하고 있으면서, pytest의 사용법까지 설명하고 있는 것을 확인하실 수 있습니다.

pandas is serious about testing and strongly encourages contributors to embrace test-driven development (TDD).

pandas uses pytest and the convenient extensions in numpy.testing.

또 다른 예시로는 SQLAlchemy가 있습니다.

python에서 많이 사용되는 SQL ORM toolkit 중 하나로 DB 관련한 개발에서 많이 활용되고 있는 오픈소스입니다. SQLAlchemy 또한 공식문서에서 test의 중요성을 이야기하고 있고, pytest를 사용함을 알려주고 있습니다.

SQLAlchemy has over many thousands of tests which span in focus from unit to integration, and a full continuous integration run over multiple Python versions and database backends runs well over two hundred thousand individual tests.

Always include tests with the change in code. Please don't submit one-liner PRs without tests.

이외에도 정말 수 많은 오픈소스들에서 pytest를 활용하고 있으며, 이러한 오픈소스에 코드 기여를 할 때는 무조건 적으로 test를 실시해 오류가 없는 수정사항들만 merge하고 있습니다.

Getting Started

이제 본격적으로 어떻게 사용하는지 배워봅시다.

Install

pip install -U pytest

또는

pip3 install -U pytest

으로 설치할 수 있습니다.

설치를 완료하신 후엔, --version을 통해 잘 설치가 되었는지 확인을 해봅시다.

$ pytest --version
pytest 6.0.1

Test code

First

그럼 이제 첫 테스트 코드를 작성하고, 테스트를 진행해봅시다.
간단한 테스트 코드에서는 assert를 사용해 테스트를 진행할 수 있습니다.

# first_test.py

# 테스트를 해보고 싶은 함수
def func(x):
    return x + 1

# 테스트 함수
def test_answer():
    assert func(3) == 5

이렇게 코드를 작성한 후, 해당 디렉토리로 들어가 pytest file_name.py로 테스트를 진행해볼 수 있습니다.

위 코드를 테스트한 경우, 다음과 같은 결과가 나왔네요.

결과는 당연하게도 테스트 실패입니다. 3+1 == 5에서 오류를 발견하고 이에 에러가 있음을 알려줍니다. 뿐만 아니라, 현재 어떠한 platform에서 작동하고 있고, 어떤 에러가 발생했는지 그리고 마지막에 요약을 통해 총 몇 개의 테스트가 통과(pass), 실패(fail)했는 지와 함께 총 테스트 시간을 알려줍니다.

그럼, 만약 func(3)이 아닌 올바르게 작동할 수 있도록 func(4)을 넣어주면 어떻게 될까요?

이렇게 passed를 통해 테스트에 성공하였음을 보여줍니다.

Second

이번엔 클래스와 문자열을 가지고 테스트 해봅시다.

  • 테스트 1: "Hello, hi"에 "h"가 포함되어있어야 한다.
  • 테스트 2: Object x 에서 "who" attribute가 있어야 한다. (hasattr)

라는 가정이 있는 상태에서 이것에 맞는 테스트 코드를 다음과 같이 작성하였습니다.

# Second test
class TestClass:
    def test_one(self):
        x = "Hello, hi"
        assert "h" in x

    def test_two(self):
        x = "what"
        assert hasattr(x, "who")

예상하셨듯 결과는 다음과 같습니다.

Structure

지금까지는 Command line에서 pytest 명령어를 통해 테스트를 실행했고, 또한 한 파일에 일반 함수와 테스트 코드들이 공존했습니다.

하지만 실제로 프로젝트에서 활용되는 데 있어서는 테스트 코드를 따로 관리하고, 이에 맞게 끔 구조를 구성해놓는 것이 효율적입니다.

그래서 테스트 코드는 프로젝트 코드들과 다르게 tests 라는 디렉토리를 통해서 관리를 합니다.

전체적으로 디렉토리 트리를 보면 다음과 같습니다.

project/
    core_code/
        __init__.py
        sample_code1.py
        sample_code2.py
        sample_code3.py
    tests/
        test_sample1.py
        test_sample2.py
        test_sample3.py

이 때, pytest를 실행하기 위해서는 project 디렉토리로 이동한 후,

$ python –m pytest tests

또는

$ python3 -m pytest tests

로 테스트를 실행할 수 있습니다.

그럼, pytest로 바로 테스트하는 것과 python -m pytest로 실행하는 것이 무엇이 다른가? 하는 궁금증이 생길 수 있는데요.

사실 큰 차이는 없습니다. python -m pytest로 실행하면 sys.path로 현재 디렉토리를 가져온다는 점만 차이가 있습니다. 즉, 테스트 코드에서 프로젝트 프로그램 코드들을 직접 가져와 실행할 수 있는 것이죠.

This is almost equivalent to invoking the command line script pytest [...] directly, except that calling via python will also add the current directory to sys.path.

pytest fixtures

fixture란 무엇인가?

이제 pytest의 특징인 fixture에 대해 알아보도록 하죠!

그 전에 먼저 fixture는 무엇인가?에 대해 알아볼 필요가 있습니다.
Test Fixtures - Software에 따르면, fixture란

  • 시스템의 필수조건을 만족하는 테스팅 프로세스를 설정하는 것

A software test fixture sets up a system for the software testing process by initializing it, thereby satisfying any preconditions the system may have.

  • 같은 설정의 테스트를 쉽게 반복적으로 수행할 수 있도록 도와주는 것

The advantage of a test fixture is that it allows for tests to be repeatable since each test is always starting with the same setup. Test fixtures also ease test code design by allowing the developer to separate methods into different functions and reuse each function for other tests.

라고 명시되어 있습니다. 결국, 간략하게 말하면 수행될 테스팅에 있어 필요한 부분들을 가지고 있는 코드 또는 리소스라고 말할 수 있습니다.

pytest fixtures

앞서 배운 정의, 개념을 가지고 pytest fixtures를 보도록 하죠!

들어가기 이전에 !

  • pytest fixture의 경우, python decorator를 사용합니다.
  • 사실 잘 모르셔도 어느정도 이해는 가능합니다만, 그래도 decorator에 대해 잘 모르는데 알고 싶으신 분은 이 글을 참조해주세요.

그럼 먼저 예시를 볼까요?
여기서는 계산기 클래스를 사용해보겠습니다.

# calculator.py
class Calculator(object):
    """Calculator class"""
    def __init__(self):
        pass

    @staticmethod
    def add(a, b):
        return a + b

    @staticmethod
    def subtract(a, b):
        return a - b

    @staticmethod
    def multiply(a, b):
        return a * b

    @staticmethod
    def divide(a, b):
        return a / b

이 계산기 클래스의 모든 함수에 대해서 테스팅 코드를 작성해야 한다면, 다음과 같이 작성할 수 있을 것입니다.

# test_calculator.py
from src.calculator import Calculator
def test_add():
    calculator = Calculator()
    assert calculator.add(1, 2) == 3
    assert calculator.add(2, 2) == 4

def test_subtract():
    calculator = Calculator()
    assert calculator.subtract(5, 1) == 4
    assert calculator.subtract(3, 2) == 1

def test_multiply():
    calculator = Calculator()
    assert calculator.multiply(2, 2) == 4
    assert calculator.multiply(5, 6) == 30

def test_divide():
    calculator = Calculator()
    assert calculator.divide(8, 2) == 4
    assert calculator.divide(9, 3) == 3

적은 양의 코드라 간결해보일 수 있어도, 중복되는 코드가 보이시나요?
모든 테스트 코드에서 계산기 클래스를 직접 호출하는 것이 보이실 겁니다.

특정 테스트에 들어갈 때마다 계산기 클래스를 호출하는 코드를 작성해야하는 불편함과 코드 중복성 그리고 이로 인한 개발 및 유지보수의 어려움 같은 문제가 발생할 수 있습니다.

여기서 fixture를 활용하면 이러한 문제를 해결할 수 있습니다.

import pytest
from src.calculator import Calculator

@pytest.fixture
def calculator():
    calculator = Calculator()
    return calculator
    
def test_add(calculator):
    assert calculator.add(1, 2) == 3
    assert calculator.add(2, 2) == 4

def test_subtract(calculator):
    assert calculator.subtract(5, 1) == 4
    assert calculator.subtract(3, 2) == 1

def test_multiply(calculator):
    assert calculator.multiply(2, 2) == 4
    assert calculator.multiply(5, 6) == 30

보시다시피, 먼저 @pytest.fixture를 통해 fixture를 선언합니다. 그리고 fixture function을 정의할 수 있습니다.
이렇게 정의된 fixture function를 parameter로 사용하여 테스트를 위한 클래스를 가져올 수 있는 것입니다. 이렇게 되면 중복코드는 물론이고, 계속해서 필요한 모듈, 클래스가 있을 때마다 선언을 하기보다 간단히 parameter를 통해 가져올 수 있습니다.

이렇게 보니, 앞서 정의된 test fixture에 대한 정의가 와닿지 않나요?

사실 fixture에 대한 것은 이게 끝이 아닙니다.
'대규모의 프로젝트인 경우엔 테스트마다 필요한 모듈, 클래스 등 리소스 및 코드들이 달라 필요한 fixture의 양이 매우 많아질 것입니다.
또한, 테스트 코드(py)마다 중복되는 fixture도 있을 겁니다. 예를 들어, A 테스트 코드에서도 계산기 클래스가 필요한데, B 테스트 코드에서도 계산기 클래스가 필요한 경우 말이죠. 지금까지의 경우로 보자면 두 테스트 코드 파일 위에 fixture를 따로 선언한 후 사용했어야 했습니다.

이러한 문제를 해결하기 위해 conftest.py를 사용합니다.
fixture 코드들은 conftest.py에 선언해두면, 모든 테스트 코드에서는 해당 fixture들을 공유하여 사용할 수 있습니다. 알아서 pytest에서 공유해주는 마법!

예시로 알아볼까요!

# Directory tree
src/
  __init__.py
  calculator.py
tests/
  conftest.py
  test_code1.py
  test_code2.py
  test_code3.py
# conftest.py
import pytest
from src.calculator import Calculator

@pytest.fixture
def calculator():
    calculator = Calculator()
    return calculator
# test_code.py
def test_add(calculator):
    """Test functionality of add."""
    assert calculator.add(1, 2) == 3
    assert calculator.add(2, 2) == 4
    assert calculator.add(9, 2) == 11

def test_subtract(calculator):
    """Test functionality of subtract."""
    assert calculator.subtract(5, 1) == 4
    assert calculator.subtract(3, 2) == 1
    assert calculator.subtract(10, 2) == 8

def test_multiply(calculator):
    """Test functionality of multiply."""
    assert calculator.multiply(2, 2) == 4
    assert calculator.multiply(5, 6) == 30
    assert calculator.multiply(9, 3) == 27

처음에 비해 매우 간단해진 테스트 코드들이 보이시나요?

pytest에서 제공하고 있는 fixture는 더 많은 기능들을 제공하고 있습니다. 꼭 공식문서도 참조해보시길 바라겠습니다.


다음 글에서 ...

쓰다보니 글이 길어져 버렸네요. 하나의 글로 끝내고 싶었는데 😂

다음 글에서는

에 대해 알아보겠습니다.


참조

  • https://docs.pytest.org/en/stable/contents.html
  • https://www.bangseongbeom.com/unittest-vs-pytest.html
  • https://github.com/yeongseon/PyCon-KR-2019
  • https://en.wikipedia.org/wiki/Test_fixture#Software
  • https://twpower.github.io/19-about-python-test-fixture
profile
언제나 호기심을 가지고.

0개의 댓글