http/https 패킷 캡쳐를 할 수 있는 selenium-wire 패키지에 대한 포스팅 입니다. (공식 문서)
$ pip install selenium-wire
selenium 패키지가 아닌 seleniumwire 패키지로부터 webdriver를 import 합니다. (기본 selenium webdriver가 내재되어 있기 때문에, selenium webdriver의 기능을 모두 사용할 수 있습니다.)
selenium_func.py
from selenium import webdriver
from seleniumwire import webdriver as wired_webdriver
from selenium.webdriver.chrome.service import Service
from webdriver_manager.chrome import ChromeDriverManager
def set_chrome_driver(wired=False):
chrome_options = webdriver.ChromeOptions()
if wired:
driver = wired_webdriver.Chrome(service=Service(ChromeDriverManager(log_level=40).install()), options=chrome_options)
else:
driver = webdriver.Chrome(service=Service(ChromeDriverManager(log_level=40).install()), options=chrome_options)
return driver
저는 네트워크 패킷 캡쳐가 필요한 경우에만 seleniumwire 패키지의 webdriver를 사용하도록 설정하였습니다.(동작에는 이슈가 없지만, 브라우저가 Selenium Wire CA 인증서를 신뢰하지 못하는 문제가 있어 기본 기능이 내재되어있음에도 불구하고 필요한 경우에만 사용하고 있습니다. - 관련 이슈 링크)
우선 공식문서에 있는 예제 코드와 동일하게 구글 사이트에 접근하여 네트워크 패킷을 캡쳐한 후 요청 url, 응답 코드, 응답 헤더의 Content-Type을 출력해보았습니다.
import selenium_func
if __name__ == '__main__':
driver = selenium_func.set_chrome_driver(wired=True)
try:
driver.get('https://www.google.com')
for request in driver.requests:
if request.response:
print(f'{request.url}, 응답코드 {request.response.status_code}, 컨텐츠 유형: {request.response.headers["Content-Type"]}')
finally:
driver.quit()
위 코드를 실행하여 출력된 결과 일부 입니다.
https://www.google.com/, 응답코드 200, 컨텐츠 유형: text/html; charset=UTF-8
https://www.google.com/tia/tia.png, 응답코드 200, 컨텐츠 유형: image/png
https://www.gstatic.com/inputtools/images/tia.png, 응답코드 200, 컨텐츠 유형: image/png
https://www.google.com/complete/search?q&cp=0&client=gws-wiz&xssi=t&hl=ko&...이하생략, 응답코드 200, 컨텐츠 유형: application/json; charset=UTF-8
실제로 크롬 브라우저로 구글 사이트 접근한 후, 개발자도구 네트워크 탭에서 보면 위의 패킷을 확인할 수 있습니다.
특정 네트워크 패킷을 캡쳐할 때까지 기다리도록 설정할 수 있습니다.
캡쳐할 패킷의 패턴 매칭을 위해 정규표현식을 사용하며, timeout은 디폴트 10초로 설정되어 있어 10초 내 해당 패킷이 캡쳐되지 않는다면 TimeoutException이 발생합니다.
위 예제에서는 구글 사이트 접근 시 통신하는 모든 패킷을 캡쳐하였는데, 이번에는 '구글 로고 이미지'만 캡쳐해보도록 하겠습니다.
driver.get('https://www.google.com')
request = driver.wait_for_request('.*/googlelogo.*')
print(f'{request.url}, 응답코드 {request.response.status_code}, 컨텐츠 유형: {request.response.headers["Content-Type"]}')
아래와 같이 로고 이미지 관련 패킷만 캡쳐된 것을 확인할 수 있었습니다.
https://www.google.com/images/branding/googlelogo/1x/googlelogo_color_272x92dp.png, 응답코드 200, 컨텐츠 유형: image/png
selenium-wire의 response body 반환 타입은 bytes
입니다.
bytes -> str 타입으로 변환하려면 아래와 같이 디코딩을 해야합니다.
response.body.decode('utf-8')
위와 같이 디코딩하였는데 UnicodeDecodeError 가 발생한다면, str 타입으로 변환하기 이전에 서버의 응답 헤더의 인코딩 타입에 맞게 디코딩을 해야합니다. (참고)
from seleniumwire.utils import decode
response = decode(response.body, response.headers.get('Content-Encoding', 'identity'))".decode('utf-8')
만약 A 패킷을 캡쳐한 이후 같은 동작을 반복하여 동일한 패턴의 B 패킷을 캡쳐해야하는 경우, 이전의 패킷(A)이 캡쳐되지 않기 위해 B 패킷 캡쳐 이전에 초기화를 해야합니다.
request = driver.wait_for_request('~~') # A 패킷 캡쳐
del driver.requests # 초기화
request = driver.wait_for_request('~~') # B 패킷 캡쳐
간단한 예제로 selenium-wire를 사용하여 테스트에 적용해봅니다.
어떤 사이트에 적용해볼까 하다가 아래 REST API 테스트 사이트를 이용하기로 하였습니다.
https://pokeapi.co/
(API 테스트 사이트라 requests 모듈을 사용하면 더 간단하게 구현할 수 있지만, selenium-wire를 사용하여 구현하는데 의의를 두었습니다.)
conftest.py : seleniumwire 패키지의 webdriver로 세팅합니다.(selenium_func.py는 윗 내용의 크롬 웹 드라이버 설정에 코드가 있습니다.)
import selenium_func
import pytest
@pytest.fixture(scope="module")
def wired_driver():
driver = selenium_func.set_chrome_driver(wired=True)
yield driver
driver.quit()
test_sample.py : 테스트 코드를 작성합니다.
from selenium.webdriver.common.by import By
from selenium.webdriver import Keys
from seleniumwire.utils import decode
import pytest
import json
@pytest.fixture(scope="module")
def setup(wired_driver):
url = "https://pokeapi.co/"
wired_driver.get(url)
def submit_request(driver, pokemon): # 포켓몬 이름 입력 후 Submit 버튼 클릭
input_element = driver.find_element(By.ID, 'url-input')
input_element.clear()
request_key = 'pokemon/' + pokemon
input_element.send_keys(request_key)
driver.find_element(By.CSS_SELECTOR, 'button[class^="Input-module__button"]').send_keys(Keys.ENTER)
def check_response(driver, pokemon): # 응답 검증
request = driver.wait_for_request('.*/api/v2/pokemon/' + pokemon)
status_code = request.response.status_code
response_str = decode(request.response.body, request.response.headers.get('Content-Encoding', 'identity')).decode('utf-8')
assert status_code == 200, f'{request.url} 요청에 대한 응답 실패: {status_code}: {response_str}'
response_json = json.loads(response_str)
assert response_json['name'] == pokemon
@pytest.mark.parametrize('pokemon', ['squirtle', 'abc'])
def test_pokemon(setup, wired_driver, pokemon):
submit_request(driver=wired_driver, pokemon=pokemon)
check_response(driver=wired_driver, pokemon=pokemon)
테스트케이스 2가지 중
1. 존재하는 포켓몬 이름 입력 - 'squirtle'
2. 존재하지 않는 포켓몬 이름 입력 - 'abc'
1번의 경우, 응답코드 200에 응답값 내 포켓몬 이름과 일치하여 pass되었고,
2번의 경우, 응답코드 404로 fail 됨을 확인할 수 있었습니다.
예제에서는 selenium-wire 위주로 다루느라 UI단 검증 없이 진행하였지만, 실제 테스트 자동화 구현 시, UI단 검증 이전에 API 요청에 대한 응답이 제대로 내려오고 있는지 먼저 확인하고, 이후 UI단을 확인하는데 유용하게 쓰일 듯합니다 :)