下面是一个 retrieve_weather函数的完整测试用例。这个测试使用了两个fixture:一个是由pytest-mock插件提供的mockerfixture, 还有一个是我们自己的。就是从之前请求中保存的静态数据。
@pytest.fixturedef fake_weather_info: """Fixture that returns a static weather data.""" with open("tests/resources/weather.json") as f: return json.load(f)def test_retrieve_weather_using_mocks(mocker, fake_weather_info): """Given a city name, test that a HTML report about the weather is generated correctly.""" # Creates a fake requests response object fake_resp = mocker.Mock # Mock the json method to return the static weather data fake_resp.json = mocker.Mock(return_value=fake_weather_info) # Mock the status code fake_resp.status_code = HTTPStatus.OK mocker.patch("weather_app.requests.get", return_value=fake_resp) weather_info = retrieve_weather(city="London") assert weather_info == WeatherInfo.from_dict(fake_weather_info)
如果运行这个测试,会获得下面的输出:
============================= test session starts ==============================...[omitted]...tests/test_weather_app.py::test_retrieve_weather_using_mocks PASSED [100%]============================== 1 passed in 0.20s ===============================Process finished with exit code 0
很好,测试通过了!但是...生活并非一帆风顺。这个测试有优点,也有缺点。现在来看一下。
优点
好的,有一个之前讨论过的优点就是,通过模拟 API 的返回值,测试变得简单了。将通信和 API 隔离,这样测试就可以预测了。这样总会返回你需要的东西。
而且,另一个不好的方面是你需要在调用函数之前进行大量设置——至少是三行代码。
... # Creates a fake requests response object fake_resp = mocker.Mock # Mock the json method to return the static weather data fake_resp.json = mocker.Mock(return_value=fake_weather_info) # Mock the status code fake_resp.status_code = HTTPStatus.OK... 我可以做的更好吗?
是的,请继续看。我现在看看怎么改进一点。
使用 responses
用 mocker功能模拟requests有点问题,就是有很多设置。避免这个问题的一个好办法就是使用一个库,可以拦截requests调用并且给它们 打补丁patch。有不止一个库可以做这件事,但是对我来说最简单的是responses。我们来看一下怎么用,并且替换mock。
@responses.activatedef test_retrieve_weather_using_responses(fake_weather_info): """Given a city name, test that a HTML report about the weather is generated correctly.""" api_uri = API.format(city_name="London", api_key=API_KEY) responses.add(responses.GET, api_uri, json=fake_weather_info, status=HTTPStatus.OK) weather_info = retrieve_weather(city="London") assert weather_info == WeatherInfo.from_dict(fake_weather_info)
这个函数再次使用了我们的 fake_weather_infofixture。
然后运行测试:
============================= test session starts ==============================...tests/test_weather_app.py::test_retrieve_weather_using_responses PASSED [100%]============================== 1 passed in 0.19s ===============================
非常好!测试也通过了。但是...并不是那么棒。
所以,这个:
def find_weather_for(city: str) -> dict: """Queries the weather API and returns the weather data for a particular city.""" url = API.format(city_name=city, api_key=API_KEY) resp = requests.get(url) return resp.json
变成这样:
def find_weather_for(city: str) -> dict: """Queries the weather API and returns the weather data for a particular city.""" url = API.format(city_name=city, api_key=API_KEY) return adapter(url)
然后适配器变成这样:
def requests_adapter(url: str) -> dict: resp = requests.get(url) return resp.json
现在到了重构 retrieve_weather函数的时候:
def retrieve_weather(city: str) -> WeatherInfo: """Finds the weather for a city and returns a WeatherInfo instance.""" data = find_weather_for(city, adapter=requests_adapter) return WeatherInfo.from_dict(data)
所以,如果你决定改为使用 urllib的实现,只要换一下适配器:
def urllib_adapter(url: str) -> dict: """An adapter that encapsulates urllib.urlopen""" with urllib.request.urlopen(url) as response: resp = response.read return json.loads(resp)def retrieve_weather(city: str) -> WeatherInfo: """Finds the weather for a city and returns a WeatherInfo instance.""" data = find_weather_for(city, adapter=urllib_adapter) return WeatherInfo.from_dict(data) 好的,那测试怎么做?