fixture, monkeypatch - pytest tutorial (2)

3 minute read

이번에는 pytest의 fixture, monkeypatch를 소개하면서, unittest보다 훨씬 간편한 pytest를 더 잘 쓰기 위한 방법을 정리합니다.

fixture

코드를 테스트를 하려면 다음과 같은 작업이 필요합니다.

  1. 테스트를 하기 위한 준비
  2. 테스트 하려는 무언가(함수, 메서드, test suite)를 호출
  3. 호출 결과가 예상한 대로 나왔는지 확인

fixture는 위 단계 중 1단계인 준비 단계를 도와주는 함수입니다. 테스트 하는 것보다 테스트를 하기 위한 환경을 세팅하는게 생각보다 더 어렵더라구요. 2, 3번은 assert my_funtion(1) == 2 같은 작업이 되겠습니다.

테스트하려는 코드마다 필요한 준비가 다릅니다. 임시 파일이나 폴더, 환경변수가 필요할 수도 있고, log, 추가 속성을 주입해야하거나 혹은 호출하려는 클래스의 메소드를 잠시 바꿔야할 수도 있습니다. pytest에서는 fixture를 통해 이런 일들을 할 수 있습니다.

pytest의 fixture는 데코레이터로 구현되어 있습니다. 그래서 @pytest.fixture 와 같은 문법으로 내가 만드는 함수 위에 데코레이트를 해주면 fixture를 만들 수 있습니다. 예시는 공식 페이지에서 가져왔습니다.

import pytest

class Fruit:
    def __init__(self, name):
        self.name = name

    def __eq__(self, other):
        return self.name == other.name

@pytest.fixture
def my_fruit():
    return Fruit("apple")

@pytest.fixture
def fruit_basket(my_fruit): # 위에서 만든 my_fruit fixture를 받습니다.
    return [Fruit("banana"), my_fruit]

# 테스트하려는 함수, 실행시키려면 parameter가 필요합니다.
def test_my_fruit_in_basket(my_fruit, fruit_basket):
    assert my_fruit in fruit_basket

이렇게 만든 fixture인 my_fruite, fruite_basket은 재사용이 가능하고 여러 개를 한꺼번에 사용할 수도 있습니다. 여러 개의 fixture를 사용할 때는 higher scope, dependencies, 그리고 auto-use 을 기준으로 먼저 실행될 fixture가 결정됩니다. 순서가 명확하지 않아서 같은 우선 순위를 가지는 가지는 경우에는 뭐가 먼저 실행될 지 모르다고 하니, 디버깅시에 유의해야겠네요. (it could go anywhere 라고 합니다.)

tmp_path

지난 포스트에서는 tmp_path 를 이용해 고유한 임시 파일 경로를 받아 그 경로에서 임시로 파일을 생성하고, 다시 불러와서 값이 제대로 들어갔는 지 테스트 해봤습니다.

monkeypatch

monkeypatch는 pytest에서 built-in으로 만들어둔 fixture 중의 하나입니다. 이것만으로도 많은 문제를 해결할 수 있습니다. monkeypatch를 이용해서 임시적으로 클래스나 클래스의 메소드, 함수, 환경변수(os.environ) 등의 object를 조작할 수 있습니다. 이 예시도 공식 문서에서 가져 왔습니다.

from pathlib import Path

def getssh():
    # home directory에 ".ssh"를 붙인 경로를 전달하는 함수입니다.
    return Path.home() / ".ssh" 

def test_getssh(monkeypatch): # 
    # 이 함수는 언제나 "/abc" 경로를 return 합니다. 
    def mockreturn():
        return Path("/abc")

    # 여기서 Path의 home method를 위에서 만든 mockreturn으로 바꿉니다.
    monkeypatch.setattr(Path, "home", mockreturn) 

    # getssh() 함수는 내부에서 Path.home을 사용하는데, 이제 home을 호출하면 mockreturn이 실행됩니다.
    x = getssh()
    assert x == Path("/abc/.ssh") # 그래서 home 경로 대신 abc 경로가 호출됩니다.

더 자세한 monkeypatch 사용법이 궁금하다면: pytest - monkeypatch

where it comes from? monkeypatch
처음에는 runtime 중에 다른 코드들을 슬쩍 바꾼다는 의미의 guerrilla patch 라고 불렸습니다. 그런데 음절이 비슷해 gorilla patch 로 잘못 쓰이는 경우가 많았다고 합니다. 그 후에 gorilla 라는 단어가 너무 세서 부드러운 단어로 바꾸려다보니 monkeypatch가 되었고 그게 고착되었다고 합니다. source

other fixtures

외에도 많은 fixture가 있습니다. 필요할 때마다 찾아가며 사용하면 될 듯 합니다. 저는 아직 monkeypatch와 tmp_path 정도만 사용해봤지만요. 사실 이번 글을 쓰기 전에는 이렇게 fixture가 많은지도 몰랐고 monkeypatch가 fixture 종류인 줄도 몰랐어요. 공식문서 보기 되게 불편하다… 생각했는데 보다보니 또 꽤나 잘 설명 되어있네요.

더 많은 fixture 사용 예제가 궁금하다면: pytest - fixture

outro

테스트에 관한 많은 얘기들이 있어서 적당히 정리해보려고 합니다. TDD(Test Driven Develop)이라는 단어가 유행하듯이(왔다 갔다 하는 듯 하네요) 높은 품질의 코드 유지를 위해 테스트 코드의 중요성이 높아지고 있죠. 테스트 코드는 메인 코드를 수정할 때마다 흔하게 발생하는 많은 실수를 방지하기도 합니다. 테스트 가능한 코드를 작성하기 위해 기능을 쪼개고 함수를 간결화해서 더 나은 방법을 고민하게 되기도 하구요. 저는 전자보다 후자가 더 와 닿았어요.

그래서 만약 ‘테스트 코드 작성이 어렵다’는 생각이 들면, 방금 말한 것처럼 코드가 너무 많은 일을 하는지 고민해보면 좋습니다. 테스트하기 어려운 코드는 좋은 코드가 아닐 확률이 높다고 하더라구요. 그러니 테스트가 가능한 코드를 작성하려고 노력하다보면 오히려 더 좋은 코드를 만들어낼 수 있습니다. (제가 받은 피드백이기도 합니다.)

그리고 좋은 코드를 작성했다면, 그리고 리소스가 여의치 않다면 그때는 테스트 코드를 생략할 수도 있겠죠. 결국 테스트는 메인 코드를 유지하고 보수하는 일을 돕는 도구니까요.

Leave a comment