aioresponses에 대한 본격적인 내용은 서론 아래에 있습니다
목차
- @aioresponses() 데코레이터 사용
- Context Manager를 활용한 aioresponses()
- 동일 URL 반복 호출
- 특정 URL만 실제 api로의 요청을 허용하는 방법
파이썬 서버 테스트 과정에서 공부한 외부 api mock out 방법에 대해 기록하고자 한다
현 회사에서 최근 서버 개발을 위한 unit test를 작성하는 방법에 대해 새롭게 익히고 있다.
기존에는 postman을 통해 e2e test를 중심으로 개발하였는데, e2e test는 사용자 입장에서의 테스트이기 때문에 개발 디버깅용으로는 비효율적인 방식이었다.
일반적으로 서버가 MVC 방식으로 모듈화 되어있다고 했을 때, 각 모듈이 어떻게 동작하는지를 먼저 테스트 해야한다.
이때 사용하는 것이 unit test(단위 테스트)다.
파이썬 unit test 툴로는 가장 유명한 것이 Pytest와 Unitttest 두 가지가 있는데, 필자는 pytest를 중심으로 공부하고 있다.
최근 크롤링 등의 이유로 외부 api를 많이 호출하면서 단위 테스트 과정에서 문제가 되었다.
예를 들어 크롤링해야하는 웹페이지는 로그인을 해야지만 접근할 수 있다면 테스트를 할 때 authorization 헤더에 매번 올바른 토큰값을 넣어줘야 한다.
이를 위해 주기적으로 로그인을 한 후 토큰값을 가져와 헤더에 넣어주는 절차를 거쳐야하는데, 비용 측면에서 큰 낭비가 될 것이다.
이 때 사용할 수 있는 방법은 외부 페이지를 mocking 하는 것이다.
mock은 서로 의존성이 강한 객체가 있을 때 가짜 객체를 만들어 사용하는 것을 말한다
그렇다면 외부 api를 호출할 때 어떻게 mocking한 페이지를 가져올까? 이 때 사용하는 것이 aioresponses 라이브러리다
aioresponses는 aiohttp request를 테스트할 때 사용할 수 있는 helper다.
requests 모듈을 테스트할 때 사용하는 툴은 httpretty, responses, requests-mock 등 많이 제공되고 있지만 aiohttp와 같이 비동기 방식이 적용되면 테스트 난이도가 올라가게 된다. 이때 사용할 수 있는것이 aioresponses다
aioresponses는 aiohttp session을 통해 외부 api로 request를 보낼때, mocking한 가상의 response를 대신 받을 수 있도록 한다.
aioresponses를 사용하는 방법에 대해 알아보자
@aioresponses() 데코레이터 사용
다음 코드를 보자
import aiohttp
import asyncio
from aioresponses import aioresponses
TEST_URL = "https://test.co.kr"
@aioresponses()
async def test(mocked):
# 가상의 응답 설정
mocked.get(TEST_URL, status=200, payload=dict(test="Success"))
# 외부 api 호출
session = aiohttp.ClientSession()
async with session.get(TEST_URL) as response:
print(await response.json())
await session.close()
if __name__ == '__main__':
asyncio.run(test())
결과
{'test': 'Success'}
@aioresponses() 데코레이터를 단 함수는 첫번째 인자로 aioresponses 객체를 받는다.
이 객체를 사용하면 어떤 api 호출에 어떤 응답을 줄 것인지 지정할 수 있다.
mocked 변수에 받은 aioreponses 객체를 사용하는 코드를 살펴보자
mocked.get(TEST_URL, status=200, payload=dict(test="Success"))
aioresponses에는 get, post, put, delete 등 http method를 지원한다.
인자로는 호출하고자 하는 api url를 전달해야하며, status, payload, body 등 다양한 응답을 지정할 수 있다.
payload의 인자로는 dict, str, list 등을 사용할 수 있지만 body의 경우 str만 전달이 가능하다
만약 json을 응답으로 전달한다면 payload가 좋을 것이고, html을 전달한다면 body가 나은 선택일 수 있다.
이외에 headers, exception, status 등도 직접 설정이 가능하다.
Context Manager를 활용한 aioresponses
위 코드에서 데코레이터를 활용한 방식을 소개했다면 이번에는 context manager를 사용한 방식을 소개하려 한다
여기서 context manager란 with 구문을 사용하여 특정 자원을 정확한 시간동안 사용하고 종료할 수 있도록 하는 것을 말한다.
즉, with 구문 아래에 들여쓰기가 되어있는 코드에만 할당한 자원을 사용할 수 있도록 하는 것을 말한다.
위 코드에서 이미 aiohttp session을 사용하는데 활용했기 때문에 여기서는 간단하게만 설명하였다
다음 코드를 살펴보자
TEST_URL = "https://test.co.kr"
async def test():
# context manager를 사용한 aioresponses
with aioresponses() as mocked:
mocked.get(TEST_URL, status=200, payload=dict(test="Success"))
session = aiohttp.ClientSession()
async with session.get(TEST_URL) as response:
print(await response.json())
await session.close()
결과
{'test': 'Success'}
위 코드를 보면 aioresponses 메서드에서 해당 객체를 mocked 변수에 받아 사용하고 있다.
아래에 코드는 데코레이터를 사용한 코드와 동일하다.
이 방법은 외부 api를 사용하는 부분이 전체 코드의 일부분이고, 복잡한 코드로 인해 에러가 우려되거나 메모리 누수(resource leak) 우려가 있는 경우 사용할 수 있다.
데코레이터는 함수 전체가 끝날 때까지 aioresponses 객체를 계속 유지한다.
때문에 외부 api 호출이 주요 기능인 경우가 아니라면 위와 같이 context manager를 사용해서 일정 시간동안만 리소스를 사용하고 안전하게 닫는 것이 더 좋은 방법일 수 있다.
동일 URL 반복 호출
이렇게 설정한 가상의 응답은 기본적으로 1회만 사용이 가능하다.
아래의 예시 코드를 보자
TEST_URL = "https://test.co.kr"
@aioresponses()
async def test(mocked):
mocked.get(TEST_URL, status=404) # 첫번째 응답
mocked.get(TEST_URL, status=200) # 두번째 응답
session = aiohttp.ClientSession()
async with session.get(TEST_URL) as response:
print(response.status)
async with session.get(TEST_URL) as response:
print(response.status)
await session.close()
결과
404
200
위 코드에서는 같은 url에 대해 mocking된 응답을 2개 만든 뒤 같은 url을 2번 호출하였다.
결과는 보다시피 첫번째 요청은 첫번째 응답을, 두번째 요청은 두번째 응답을 받았다.
만약 같은 url에 대해 다양한 응답을 받고 싶다면 위와 같은 방법을 사용하면 된다.
그렇다면 만약 같은 url을 여러 번 호출하고 계속 같은 응답을 받고 싶다면 어떻게 하면 될까?
이때 사용할 수 있는 파라미터가 repeat 이다.
가령 url은 같지만 body를 바꿔가며 여러개의 응답을 전부 받아야하는 경우가 있을 수 있는데 repeat=False (default)로 되어있는 경우 Connection refused 에러가 발생하게 된다.
내가 mocking 한 응답을 재사용한다는 의미로 repeat=True를 해주어야만 반복적인 요청이 가능하다.
예시 코드를 보자
TEST_URL = "https://test.co.kr"
@aioresponses()
async def test(mocked):
mocked.get(TEST_URL, status=200, repeat=True) # repeat 인자 설정
session = aiohttp.ClientSession()
async with session.get(TEST_URL) as response:
print(response.status)
async with session.get(TEST_URL) as response:
print(response.status)
await session.close()
결과
200
200
특정 URL만 실제 api로의 요청을 허용하는 방법
aioresponses는 외부 api를 호출하는 모든 요청을 통제하고 응답이 mocking된 URL 요청만 허용한다.
만약 mocking되지 않은 URL을 임의로 호출하면 aioresponses는 해당 요청을 차단하고 에러를 보낸다.
아래의 코드를 보자
REAL_URL = "https://real.url.com"
@aioresponses()
async def test(mocked):
session = aiohttp.ClientSession()
# mocking하지 않은 외부 api 호출
async with session.get(REAL_URL) as response:
print(await response.json())
await session.close()
결과
aiohttp.client_exceptions.ClientConnectionError: Connection refused: GET https://real.url.com
위와 같이 mocking되지 않은 api를 호출하면 해당 요청은 Connection refused 되었다는 에러메세지를 볼 수 있다.
경우에 따라, 일부 api는 mocking된 응답을 받고 일부는 외부에서 실제 데이터를 가져와야 할 수 있다.
그런 경우에 사용할 수 있는 것이 passthrough 파라미터다.
aioresponses 함수를 호출할 때, passthrought 파라미터에 특정 url 리스트를 인자로 전달하면 해당 url은 외부로 요청할 수 있도록 허용한다.
아래의 코드를 보자
TEST_URL = "https://test.co.kr"
REAL_URL = "https://real.url.com"
# https://real.url.com은 예시를 위한 임의의 URL입니다.
@aioresponses(passthrough=[REAL_URL])
async def test(mocked):
mocked.get(TEST_URL, status=200, payload=dict(test="Success"))
session = aiohttp.ClientSession()
# mocking된 요청
async with session.get(TEST_URL) as response:
print(await response.json())
# 실제 외부 api로의 요청
async with session.get(REAL_URL) as response:
print(await response.json())
await session.close()
결과
{'test': 'Success'}
{'real_url': 'Connected'}
위 코드는 mocking 된 TEST_URL과 외부로 실제 요청을 보낼 REAL_URL 두개에 각각 요청을 보내고 있다.
TEST_URL은 가상의 응답을 받아왔고, REAL_URL은 외부 서버에서 응답을 받아온 것을 확인할 수 있다.
본 포스트는 제가 공부하는 과정에서 작성하였습니다. 정정사항 댓글은 언제나 환영합니다!
'Python' 카테고리의 다른 글
[Locust] User 클래스에 커스텀 arguments 전달 (0) | 2022.04.29 |
---|---|
[Locust] TaskSet, SequentialTaskSet 사용하기 (0) | 2022.04.28 |
[Locust] 서버 부하테스트 툴 Locust 튜토리얼 (0) | 2022.04.21 |
[Scrapy] Scrapy 튜토리얼3 - 네이버 검색 결과 크롤링(데이터 추출) (0) | 2022.03.17 |
[Scrapy] Scrapy 튜토리얼2 - 네이버 검색 결과 크롤링(크롤러 설정) (0) | 2022.03.17 |