코딩 테스트 스터디 환경 만들기 : pytest

프롤로그


얼마전 스터디를 했는데 스터디가 파토 나버렸습니다. 음… 제가 헀던 스터디 경험으로 비춰보면 정해진 발표? 를 하는 스터디는 대부분 성공적이지 못했던거 같습니다. 발표 하는 날에는 열심히 준비 하지만, 제 차례가 아닌 시간에는 집중하기가 쉽지 않더라고요. 🙄

제가 생각하는 스터디의 성공의 키는 구성원 간의 활발한 소통인거 같습니다. 그래서 새로운 코딩테스트 스터디를 하는 김에 구성원 간의 피드벡을 주고 받을 수 있는 환경을 구축해야겠다고 생각했습니다.

어떤 환경을 만들지


우선은 코딩 테스트 사이트에서 문제를 풀어 보는게 가장 좋습니다. 실제 입사 시험과 동일한 환경에서 공부하는게 가장 좋으니까요.

하지만 안 풀리는 문제가 있는 경우를 위해서 테스트를 추가하고 돌려볼 수 있는 환경이 있으면 좋겠다고 생각했습니다. 게다가 혼자 하는 공부가 아니라 스터디 구성원의 참여를 이끌어 낼 수 있는게 목표입니다.

그래서 생각 필요한 환경의 핵심은 다음과 같습니다.

  1. 테스트를 추가 할 수 있고, 로컬에서 디버깅이 편리한 환경 → pytest
  2. 구성원 간의 피드벡, 동기부여를 할 수 있는 환경 → github Actions

🔗실제 스터디 저장소 링크

pytest 로컬 환경


우선은 로컬에서 pytest 로 테스트 할 수 있는 환경을 구축 했습니다. 로컬이라고 했지만 다른 구성원과 같이 사용하게 될 공간이므로 일정의 규칙이 필요했습니다.

  1. 다 같이 쓰지만 개인이 사용할 고유한 공간이 필요 → 깃허브 아이디
  2. 같은 테스트를 사용하지만 내가 원하는 코드를 동적으로 사용 가능한 테스트 환경 → pytest_addoption, import_module

테스트 환경의 기본적인 규칙


우선은 깃허브 Actions 에서도 테스트를 동작하게 만들기 위해서 구분자가 필요했습니다. 그래서 깃허브 아이디를 고유한 공간의 이름으로 사용한다는 전제로 테스트 환경을 구축했습니다.

기본적으로 책을 기반으로 하는 스터디이기 떄문에 우선은 문제를 챕터별로 폴더와 파일을 작성하게 만들었습니다.

  1. src/아이디/ch_숫자 폴더에 문제 풀이 파일과 함수를 규칙에 따라 작성.
  2. tests/ch_숫자 폴더에는 풀이 함수를 테스트하는 코드를 작성.
  3. 테스트는 동적으로 입력받은 아이디를 활용해서 해당하는 대상의 함수를 호출한다.

전체적인 프로젝트 구조

src
└─ 아이디
   └─ ch_01
      └─ solution_01.py :: soultion()
      └─ solution_02.py :: soultion()
   └─ ch_02
   
tests
└─ ch_01
   └─ test_01.py :: test()
   └─ test_02.py :: test()
└─ ch_02
   └─ ...
   └─ ...
└─ conftest.py
└─ .env
pytest.ini

pytest : 동적인 입력값 받기


위에서 만든 규칙에서 중요한 것은 각자 아이디 밑에 테스트 대상이 있다는 점입니다. 그래서 우선은 pytest 가 동작 할 때, 사용자 옵션을 받을 수 있는 pytest_addoption 를 사용하여 아이디 값을 받아 올 사용자 정의 옵션을 만들어 줍니다.

tests/conftest.py
import pytest
from dotenv import load_dotenv

def pytest_addoption(parser):
    # .env 파일을 현재 작업 디렉토리에서 읽어옴
    load_dotenv()

    # 환경 변수 읽기
    USER_ID = os.getenv("USER_ID")

    parser.addoption(
        "--id",
        action="store",
        default=USER_ID,
        help="여기를 수정하지 마세요! .tests/.env 사용하기!",
    )

pytest 직접 실행

추가한 사용자 정의 커맨드 라인은 pytest 를 실행하면 입력값을 받아 올 수 있습니다.

$ pytest --id=아이디

VScode 에서 실행 or 디버깅

VScode 에서 디버깅 탐색기에서 디버깅으로 실행하는 경우를 위한 디폴트 값도 설정해 줍니다. 여기서는 load_dotenv()를 사용하여 .env 파일을 로드하여 parser.addoption() 에 default 로 전달 해줬습니다. .vscode/launch.json--id 값을 설정하는 방법도 가능합니다.

# src/tests/.env

USER_ID=tiaz0128

테스트 실행 할때 경로를 구하는 함수


이제 동적인 파일을 가져올 수 있는 pytest.fixture 를 만들어 줍니다. 실제 테스트 파일이 실행될 때 경로가 필요합니다. 왜냐면 실제 테스트 파일의 경로 값을 이용해서 실제 테스트 할 대상을 찾기 위해서 입니다.

어떤 테스트 파일이 실행 할 때, 그 테스트와 매칭되는 문제 풀이 함수를 경로를 구해주는 함수를 fixture 로 만들어 주겠습니다. 또한 입력 받은 사용자 아이디도 fixture 로 만들어 줍니다.

tests/conftest.py
# 기존 pytest_addoption 코드...

def get_test_file_path(file_path, user_id):
    script_path = os.path.abspath(file_path)

    # 부모 폴더 이름
    parent_path = os.path.dirname(script_path)
    parent_directory = os.path.split(parent_path)[-1]

    # 파일명에서 숫자 추출
    file_name = os.path.basename(file_path)
    numbers = re.findall(r"\d+", file_name)[-1]

    return f"src.{user_id}.{parent_directory}.solution_{numbers}"


@pytest.fixture(name="user_id")
def setup(request):
    user_id = request.config.getoption("--id")
    return user_id


@pytest.fixture(name="func")
def setup_lib():
    return get_test_file_path

importlib.import_module() : 동적으로 테스트 대상 import


위에서 만든 지금 실행하는 경로를 구해주는 함수 get_test_file_path 를 각 테스트 파일마다 작성합니다. 이렇게 하는 이유는 각 파일이 실행될 때 자동으로 생성되는 파일 속성인 __file__ 을 이용하기 위해서 입니다.

  1. 현재 실행되는 테스트 파일의 경로를 가지고 있는 __file__ 를 인자값으로
  2. fixture 로 전달 받은 get_test_file_path 를 호출.
  3. 해당 함수로 현재 테스트 파일에 매칭되는 테스트 대상 함수가 있는 경로를 구함.
  4. 마지막으로 해당 경로 함수를 importlib.import_module 를 통해서 코드가 실행 할 때, 동적으로 import 합니다.
tests/ch_01/test_01.py
@pytest.fixture(name="module")
def setup_module(user_id, func):
    path = func(__file__, user_id)

    return importlib.import_module(path)

@pytest.fixture(
    name="test_input",
    params=[
        ([1, -5, 2, 4, 3], [-5, 1, 2, 3, 4]),
        ([2, 1, 1, 3, 2, 5, 4], [1, 1, 2, 2, 3, 4, 5]),
        ([1, 6, 7], [1, 6, 7]),
    ],
)
def setup(request):
    return request.param

@pytest.mark.ch_01
def test(module, test_input):
    # given
    *args, excepted = test_input

    # when
    result = module.solution(*args)

    # then
    assert result == excepted

선택해서 테스트


pytest 의 옵션과 마커를 사용하면 손쉽게 여러 상황을 테스트 할 수 있습니다. 기본적인 폴더 구성은 챕터별 테스트 파일을 넣어두었고, 같은 챕터의 테스트 파일마다 pytest.mark.ch_XX 로 마커를 붙여 놓았습니다. 또한 각 테스트 파일마다 고유한 문제 번호로 붙여서 test_숫자로 만들었기 떄문에 하나의 테스트만 돌려 볼 수도 있습니다.

# 챕터별로 테스트
$ pytest -m ch_01

# 특정 테스트만 
$ pytest -k 'test_01'

다음으로 Actions


pytest 를 베이스로 동적인 테스트 환경을 만들어 봤습니다. 실제로 만들고 보니, 코딩 테스트 스터디 용도 뿐만 아니라 다양한 스터디에 활용 할 수 있지 않을까 생각이 듭니다.

여기서 끝나지 않고 이제 이걸 구성원이 다 같이 쓸 수 있게 github Actions 를 연결 해보겠습니다. 조금 더 적극적인 구성원들의 참여를 이끌어 내봅시다. 😊

python pytest