티스토리 뷰

반응형

이 글을 쓰게된 계기는 fast api 공식문서와 sqlalchemy 공식문서대로 했을 때 테스트를 진행했을 때 멱등성 있게 진행이 안되었기에 다른 사람들이 이 글을 보고 조금이나마 도움이 되길 하는 생각으로 작성하게 되었습니다. 제가 생각했을 때에도 그렇게 효율적이지는 않은 것 같지만 제가 지금 할 수있는 가장 확실한 방법이기에 글을 남기고자 합니다.

 

계기

처음에 fast api 공식문서대로 진행을 하였다.

def override_get_db():
    try:
        db = TestingSessionLocal()
        yield db
    finally:
        db.close()


app.dependency_overrides[get_db] = override_get_db

client = TestClient(app)

 

근데 문제점이 있었다. 다른 테스트 결과에서 생긴 생성된 결과값이 다른 테스트에 영향을 주는 것이었다.

 

두 번째 문제점은 Query를 직접 때려서 확인 해보는 수단을 알려주지 않는 것

이 두개의 문제점 때문에 꽤 골머리를 앓았다.

 

그래서 저는 우선 fast api 만드신 개인 깃헙 플젝에서 어떻게 테스트를 진행하나 확인해보았습니다.

 

그래서 다음과 같은 힌트를 확인했습니다.

 

@pytest.fixture(scope='function')
def db() -> Generator:
    yield SessionLocal()


@pytest.fixture(scope="module")
def client() -> Generator:
    with TestClient(app) as c:
        yield c

 

 

def test_create_user(db: Session) -> None:
    email = random_email()
    password = random_lower_string()
    user_in = UserCreate(email=email, password=password)
    user = crud.user.create(db, obj_in=user_in)
    assert user.email == email
    assert hasattr(user, "hashed_password")

 

db 세션을 다음과 같이 pytest.fixture를 이용해서 사용할 수 있다는 점과 client도 또한 pytest.fixture를 이용했다는 점입니다.

 

이로써 2번째 문제점을 어느정도 해결할 수 있었습니다.

 

하지만 제게는 첫번째 문제점을 해결할 수는 없었습니다. 어떻게 해야 멱등성 있게 테스트구조를 가져갈 수 있을까....

 

그래서 그냥 제가 만들기로 했습니다. 비록 다소 비효율적이라도 ...(다른 예제에서는 테이블을 내리고 다시 만들기도 하던데 그에 비해서는 효율적?이라고 볼 수도 있겠습니다. ㅎㅎ)

 

우선 사용하는 모델들을 리스트로 세팅을 해놓았습니다.

 

models = [Routine, RoutineDay, RoutineResult, Retrospect, Snapshot]

 

 

그리고 이를 어떻게 이용하나?

 

바로 데코레이터를 이용하여 구성할 예정입니다.

 

데코레이터는 __call__ 로 이루어져 있으며 선, 후처리가 가능하기에  가장 적합하다고 생각했습니다.

 

def complex_transaction(func):
    def inner(db: Session, client: TestClient):
        try:
            print('--transaction--')
            ret = func(db, client)
        finally:
            for model in models:
                db.query(model).delete()
            db.commit()
        return ret
    return inner

 

데코레이터는 다음과 같이 구성했습니다.

 

우선 테스트케이스의 함수를 실행합니다. 그리고 나서 모델들의 테이블에 있는 모든 row를 지우는 것입니다. 그러면 해당 테스트를 진행하면서 다른 데이터를 읽을 일이 없겠죠?

 

사용 방법은 다음과 같습니다.

 

@complex_transaction
def test_루틴_수행여부_값_저장_오늘이_수행하는_날일_때(db: Session, client: TestClient):
    # given
    now = get_now()
    weekday = now.weekday()
    weekday = Week.get_weekday(weekday)
    create = {
        'title': 'wake_up',
        'account_id': 1,
        'category': 1,
        'goal': 'daily',
        'is_alarm': True,
        'start_time': '10:00:00',
        'days': ['MON', 'TUE', 'WED', 'THU', 'FRI', 'SAT', 'SUN']
    }
    # when
    response = client.post(
        f'{routines_router_url}',
        json=create
    )
    routine = db.query(Routine).first()
    now = str(get_now())
    day = str(convert_str2date(now))
    date = convert_str2datetime(day)
    routine_data = {
        'result': 'DONE',
        'weekday': weekday.value,
        'date': str(date)
    }
    # when
    response = client.post(
        f'{routines_router_url}/{routine.id}/check-result',
        json=routine_data
    )
    # then
    result = response.json()
    message = result['message']
    data = result['data']
    assert_that(message['status']).is_equal_to('ROUTINE_OK')
    assert_that(message['msg']).is_equal_to('루틴 결과 업데이트에 성공했습니다.')
    assert_that(data['success']).is_true()
    routine_result = db.query(RoutineResult).filter(and_(RoutineResult.routine_id == routine.id, RoutineResult.yymmdd == date)).first()
    assert_that(routine_result.result).is_equal_to(Result.DONE)

 

이렇게 구성하면 요청은 요청대로 할 수 있고 그에 대한 데이터 변화도 확인할 수 있고, 다른 테스트에도 영향을 주지 않는 환경을 구성했습니다!!


여기서 주의할 점이 있다.

 

client.post
clien.get
client.patch
...

등과 같이 e2e 테스트하면서 생긴 결과와 불일치가 생길 수가 있다.

그 이유는 다음 아래와 같다.

같은 테스트 함수 안에서 나온 세션 값들이다.

 

이러한 이유로 인하여 하나의 세션이 아닌 다양한 세션으로 변한 데이터를 가져오는 부분에서 싱크 문제로 인해 깨지는 현상이 생길 수가 있다.

 

그러므로 만약에 테스트를 진행하고 싶다면 e2e로 테스트해서 결과값을 확인하고 싶다면 오로지 e2e를 통해 결과값을 사용하거나 아니면 repository를 통해 세션 값을 넘겨서 하나의 세션으로 처리하는 것을 추천한다.

 

저는 후자를 더 추천을 한다. 왜냐하면 테스트에서는 멱등성이 가장 중요한데 하나의 세션으로 처리하면 이러한 것을 많이 방지할 수 있기 때문이다.

 

 

좀 더 자세한 내용을 보고 싶으시면 아래 링크를 통해 확인해주세요

https://github.com/again-minning/again-minning-routine

 

GitHub - again-minning/again-minning-routine

Contribute to again-minning/again-minning-routine development by creating an account on GitHub.

github.com

 

관련 PR은 

https://github.com/again-minning/again-minning-routine/pull/24/files

 

테스트케이스 유닛테스트 후처리 방법을 데코레이터로 처리 by VIXXPARK · Pull Request #24 · again-minnin

그냥 모든 테이블 데이터 삭제 def complex_transaction(func): def inner(db: Session, client: TestClient): ret = func(db, client) for model in models: db.query(model).delete() ...

github.com

읽어주셔서 감사합니다.

반응형
반응형
공지사항
최근에 올라온 글
최근에 달린 댓글
Total
Today
Yesterday
링크
«   2024/05   »
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
26 27 28 29 30 31
글 보관함