코드로 우주평화
pytest _ 하나의 함수에서 복수 케이스 테스트 하는 방법 본문
단일 케이스 테스트
보통 테스트는 단일 케이스로 이루어집니다.
단일 케이스로 테스트를 하면 최소의 단위로 테스트를 진행하기 때문에 코드의 정확도를 높일 수 있고,
보는 이에게도 명확한 케이스 사례를 보여줄 수 있다는 장점이 있습니다.
예를 들어 회원가입 API를 테스트한다고 할 때 케이스를 다음처럼 나눌 수 있습니다.
- 성공 : 회원가입이 성공했는가?
- 실패1 : 회원가입 시 이메일이 중복되는가?
- 실패2 : 회원가입 시 닉네임이 중복되는가?
- 실패3 : 회원가입 시 패스워드가 유효한가?
하지만 유사한 케이스를 모두 단일 케이스로 테스트할 경우
비슷한 코드가 반복되면서 가독성과 효율이 떨어질 수 있습니다.
위의 케이스 중에서 실패1, 실패2를 살펴보겠습니다.
이 둘은 중복여부에 의한 실패를 확인하는 테스트로 엄연히 다른 케이스이지만 그 형태가 유사합니다.
실패1, 실패2 의 코드를 자세히 보면 다음과 같습니다. (약 60줄)
import pytest
from httpx import AsyncClient
from sqlalchemy.ext.asyncio import AsyncSession
from tests.conftest import create_user
# 이메일 중복 시 회원가입 실패 테스트
@pytest.mark.asyncio
async def test_fail_signup_user_when_duplicate_email(
client: AsyncClient, session: AsyncSession
):
# given
data = {
"nickname": "test",
"email": "test@test.com",
"password": "1q2w3e4r",
}
await create_user(session, **data)
# when
data = {
"nickname": "pass",
"email": "test@test.com",
"password": "1q2w3e4r",
}
r = await client.post("/signup", json=data)
# then
r_message = r.json()
assert r.status_code == 400
assert r_message.get("detail") == "Duplicate email"
# 닉네임 중복 시 회원가입 실패 테스트
@pytest.mark.asyncio
async def test_fail_signup_user_when_duplicate_nickname(
client: AsyncClient, session: AsyncSession
):
# given
data = {
"nickname": "test",
"email": "test@test.com",
"password": "1q2w3e4r",
}
await create_user(session, **data)
# when
data = {
"nickname": "test",
"email": "pass@test.com",
"password": "1q2w3e4r",
}
r = await client.post("/signup", json=data)
# then
r_message = r.json()
assert r.status_code == 400
assert r_message.get("detail") == "Duplicate nickname"
data에 담기는 ‘email’과 ‘password’ 그리고 응답에 담긴 ‘detail’의 값만 다를 뿐 다른 코드들은 모두 동일합니다.
게다가 코드의 줄 수가 많다 보니 가독성 측면에서도 좋지 않습니다.
그렇다면 이런 유사 케이스들을 하나의 함수로 묶으면 어떨까요?
복수 케이스 테스트
pytest에서는 매개변수를 활용해 복수 케이스를 테스트할 수 있도록 지원합니다.
그 방법은 @pytest.mark.parametrize 를 사용하는 것입니다.
pytest.mark.parametrize 데코레이터는 테스트 함수에 매개변수를 넣을 수 있도록 도와줍니다.
이 데코레이터를 이용해 위의 두 테스트 함수를 합치면 다음과 같습니다. (코드 줄 약 40% 감소)
import pytest
from httpx import AsyncClient
from sqlalchemy.ext.asyncio import AsyncSession
from tests.utils import create_user
# 이메일 혹은 닉네임 중복 시 회원가입 실패 테스트
@pytest.mark.parametrize(
"nickname, email, detail",
(
("pass", "test@test.com", "Duplicate email"),
("test", "pass@test.com", "Duplicate nickname"),
),
)
async def test_fail_signup_user_when_duplicated_nickname_or_email(
nickname: str, email: str, detail: str, client: AsyncClient, db: AsyncSession
):
# given
data = {
"nickname": "test",
"email": "test@test.com",
"password": "1q2w3e4r",
}
await create_user(db, **data)
# when
data = {
"nickname": nickname,
"email": email,
"password": "1q2w3e4r",
}
r = await client.post("/api/v1/signup", json=data)
# then
r_message = r.json()
assert r.status_code == 400
assert r_message.get("detail") == detail
데코레이터 안에 변수 값을 적었더니 테스트 함수 하나에서 두 가지 케이스를 테스트할 수 있게 되었습니다.
테스트 코드 라인이 크게 줄었고, 중복되었던 테스트 내용을 한 눈에 파악할 수 있게 되었습니다.
만약 복수 케이스 중에 한 케이스가 통과를 하지 못하더라도 pytest가 아래처럼 구분하여 표시해줍니다.
test_fail_signup_user_when_duplicated_nickname_or_email[pass-pass@test.com-Duplicate nickname] - assert 200 == 400
또한 테스트 함수가 하나지만 한 개의 테스트로 처리되는 것이 아닌 매개변수로 입력한 케이스 수만큼 테스트 수가 계산됩니다.
예를 들어 테스트 함수가 한 개이지만 테스트 수는 두 개가 되는 것입니다.
적용 방법
복수 케이스 적용 방법은 생각보다 간단합니다.
먼저 테스트에 사용할 매개변수를 특정합니다.
...
data = {
"nickname": nickname,
"email": email,
"password": "1q2w3e4r",
}
...
assert r_message.get("detail") == detail
위의 테스트 케이스에서는 ‘nickname’, ‘email’, ‘detail’ 변수만 달라지므로 이 세 가지를 데코레이터에 넣어줍니다.
@pytest.mark.parametrize() 안에 첫 번째 인자로 위에 지정한 세 개의 매개변수를 텍스트 형태로 넣어줍니다. 이때 구분은 콤마로 해줍니다.
@pytest.mark.parametrize("nickname, email, detail",
그다음 인자로는 실제로 테스트할 때 사용할 값을 넣어줍니다
튜플 안에 튜플(2차원 튜플) 형태로 넣어주시면 됩니다.
(
# 이메일 중복 테스트에 사용할 매개변수
("pass", "test@test.com", "Duplicate email"),
# 닉네임 중복 테스트에 사용할 매개변수
("test", "pass@test.com", "Duplicate nickname"),
)
데코레이터 안에 인자를 모두 넣은 모습은 다음과 같습니다.
@pytest.mark.parametrize(
"nickname, email, detail",
(
("pass", "test@test.com", "Duplicate email"),
("test", "pass@test.com", "Duplicate nickname"),
),
)
자 그럼 이제 실제 테스트 함수에서 사용하기 위해 가져와야겠죠?
방법은 매우 쉽습니다. 평소 함수에 인자를 받듯이 매개변수로 지정하면 됩니다.
async def test_fail_signup_user_when_duplicated_nickname_or_email(
nickname: str, email: str, detail: str, client: AsyncClient, db: AsyncSession
):
이제 함수 내부에서 해당 매개변수들을 불러와 사용할 수 있습니다.
매개변수의 내용은 위에 데코레이터에 넣은 값들이며, 테스트 시 순서대로 불러오게 됩니다.
이제 pytest를 실행해보면 정상적으로 테스트가 되는 것을 확인하실 수 있습니다!
마치며
단일 케이스를 복수 케이스로 합치는 것이 꼭 정답은 아닙니다.
오히려 무리하게 합치려다 유닛 테스트의 목적을 잊어버릴 수도 있습니다.
그럼에도 일부 유사 케이스에 한해 사용하는 것은 바람직해 보입니다.
테스트 코드 작성 효율과 가독성, 테스트의 정확성 등등,
상황에 맞게 선택하여 테스트 코드를 작성하시기 바랍니다!
레퍼런스
'나는 이렇게 학습한다 > Debug' 카테고리의 다른 글
SQLAlchemy _ NotSupportedError(InvalidCachedStatementError) 에러 해결 (0) | 2022.04.29 |
---|---|
컬럼 속성 변경 후 alembic migration할 때 ‘None’ 생기는 현상 (0) | 2022.04.26 |
@pytest.fixture 로 test 데이터 세팅하기 (0) | 2022.04.14 |
내가 쓴 Python 테스트 코드 coverage 알아보기 (0) | 2022.03.20 |
[TIL]django__데이터 중복_IntegrityError_ValidationError (1) | 2021.11.21 |