Merge branch 'LCTT:master' into master

This commit is contained in:
zhaoxu_Lee 2022-08-19 10:47:54 +08:00 committed by GitHub
commit 9cdd571b42
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

View File

@ -3,16 +3,16 @@
[#]: author: "Miguel Brito https://opensource.com/users/miguendes"
[#]: collector: "lujun9972"
[#]: translator: "Yufei-Yan"
[#]: reviewer: " "
[#]: publisher: " "
[#]: url: " "
[#]: reviewer: "wxy"
[#]: publisher: "wxy"
[#]: url: "https://linux.cn/article-14944-1.html"
用 Python 测试 API 的 3 种方式
=====
单元测试可能令人生畏,但是这些 Python 模块会使你的生活变得更容易。
> 单元测试可能令人生畏,但是这些 Python 模块会使你的生活变得更容易。
![Puzzle pieces coming together to form a computer screen][1]
![](https://img.linux.net.cn/data/attachment/album/202208/18/180800clp08p82pi838zrs.jpg)
在这个教程中,你将学到如何对执行 HTTP 请求代码的进行单元测试。也就是说,你将看到用 Python 对 API 进行单元测试的艺术。
@ -35,25 +35,24 @@
### 使用一个天气状况 REST API 的演示程序
为了更好的解决这个问题,假设你正在创建一个天气状况的 app。这个 app 使用第三方天气状况 REST API 来检索一个城市的天气信息。其中一个需求是生成一个简单的 HTML 页面,像下面这个图片:
为了更好的解决这个问题,假设你正在创建一个天气状况的应用。这个应用使用第三方天气状况 REST API 来检索一个城市的天气信息。其中一个需求是生成一个简单的 HTML 页面,像下面这个图片:
![web page displaying London weather][3]
伦敦的天气OpenWeatherMap。图片是作者自己制作的。
*伦敦的天气OpenWeatherMap。图片是作者自己制作的。*
为了获得天气的信息,必须得去某个地方找。幸运的是,通过 [OpenWeatherMap][2] 的 REST API 服务,可以获得一切需要的信息。
_好的很棒但是我该怎么用呢_
通过发送一个 `GET` 请求到:`https://api.openweathermap.org/data/2.5/weather?q={city_name}&appid={api_key}&units=metric`,就可以获得你所需要的所有东西。在这个教程中,我会把城市名字设置成一个参数,并确定公制单位。
通过发送一个 `GET` 请求到:`https://api.openweathermap.org/data/2.5/weather?q={city_name}&appid={api_key}&units=metric`,就可以获得你所需要的所有东西。在这个教程中,我会把城市名字设置成一个参数,并确定使用公制单位。
### 检索数据
使用<ruby>`请求`<rt>requests</rt></ruby>来检索天气数据。可以创建一个接收城市名字作为参数的函数,然后返回一个 JSON。JSON 包含温度,天气状况的描述,日出和日落时间等数据。
使用 `requests` 模块来检索天气数据。你可以创建一个接收城市名字作为参数的函数,然后返回一个 JSON。JSON 包含温度、天气状况的描述、日出和日落时间等数据。
下面的例子演示了这样一个函数:
```
def find_weather_for(city: str) -> dict:
    """Queries the weather API and returns the weather data for a particular city."""
@ -65,7 +64,7 @@ def find_weather_for(city: str) -> dict:
这个 URL 是由两个全局变量构成:
```
BASE_URL = "<https://api.openweathermap.org/data/2.5/weather>"
BASE_URL = "https://api.openweathermap.org/data/2.5/weather"
API = BASE_URL + "?q={city_name}&amp;appid={api_key}&amp;units=metric"
```
@ -116,10 +115,9 @@ API 以这个格式返回了一个 JSON
  "cod": 200
```
当调用 `resp.json()` 的时候,数据是以 Python 字典的形式返回的。为了封装所有细节,可以用 `dataclass` 来代表。这个类有一个工厂方法,可以获得这个字典并且返回一个 `WeatherInfo` 实例。
这种办法很好,因为可以保持这种表示方法的稳定。比如,如果 API 改变了 JSON 的结构,就可以在同一个地方修改逻辑,在 `from_dict` 方法中。其他代码不会受影响。你也可以从不同的源获得信息,然后把他们都整合到 `from_dict` 方法中。
当调用 `resp.json()` 的时候,数据是以 Python 字典的形式返回的。为了封装所有细节,可以用 `dataclass` 来表示它们。这个类有一个工厂方法,可以获得这个字典并且返回一个 `WeatherInfo` 实例。
这种办法很好,因为可以保持这种表示方法的稳定。比如,如果 API 改变了 JSON 的结构,就可以在同一个地方(`from_dict` 方法中)修改逻辑。其他代码不会受影响。你也可以从不同的源获得信息,然后把它们都整合到 `from_dict` 方法中。
```
@dataclass
@ -154,18 +152,17 @@ def retrieve_weather(city: str) -> WeatherInfo:
很好,我们的 app 现在有一些基础了。在继续之前,对这些函数进行单元测试。
### 1\. 使用 mock 测试 API
### 1使用 mock 测试 API
[根据维基百科][4]<ruby>模拟对象<rt>mock object</rt></ruby>是通过模仿真实对象来模拟它行为的一个对象。在 Python 中,你可以使用 `unittest.mock` 库来<ruby>模拟<rt>mock</rt></ruby>任何对象,这个库是标准库中的一部分。为了测试 `retrieve_weather` 函数,可以模拟 `requests.get`,然后返回静态数据。
#### pytest-mock
在这个教程中,会使用 `pytest` 作为测试框架。通过插件,`pytest` 库是非常具有扩展性的。为了完成我们的模拟目标,要用 `pytest-mock`。这个插件抽象化了大量 `unittest.mock` 中的设置,也会让你的代码更简洁。如果你还好奇的话,我在[另一篇博文中][5]会有更多的讨论。
在这个教程中,会使用 `pytest` 作为测试框架。通过插件,`pytest` 库是非常具有扩展性的。为了完成我们的模拟目标,要用 `pytest-mock`。这个插件抽象化了大量 `unittest.mock` 中的设置,也会让你的代码更简洁。如果你感兴趣的话,我在 [另一篇博文中][5] 会有更多的讨论。
_好的说的够多的了现在看代码。_
下面是一个 `retrieve_weather` 函数的完整测试用例。这个测试使用了两个 fixture一个是由 `pytest-mock` 插件提供的 `mocker` fixture, 还有一个是我们自己的。就是从之前请求中保存的静态数据。
_好的言归正传现在看代码。_
下面是一个 `retrieve_weather` 函数的完整测试用例。这个测试使用了两个 `fixture`:一个是由 `pytest-mock` 插件提供的 `mocker` fixture, 还有一个是我们自己的。就是从之前请求中保存的静态数据。
```
@pytest.fixture()
@ -173,9 +170,9 @@ def fake_weather_info():
    """Fixture that returns a static weather data."""
    with open("tests/resources/weather.json") as f:
        return json.load(f)
```
[/code] [code]
```
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."""
@ -194,7 +191,6 @@ def test_retrieve_weather_using_mocks(mocker, fake_weather_info):
如果运行这个测试,会获得下面的输出:
```
============================= test session starts ==============================
...[omitted]...
@ -215,7 +211,6 @@ Process finished with exit code 0
而且,另一个不好的方面是你需要在调用函数之前进行大量设置——至少是三行代码。
```
...
    # Creates a fake requests response object
@ -230,9 +225,10 @@ Process finished with exit code 0
_我可以做的更好吗_
是的,请继续看。我现在看看怎么改进一点。
### 使用 responses
`mocker` 功能模拟 `requests` 有点问题,就是有很多设置。避免这个问题的一个好办法就是使用一个库,可以拦截 `requests` 调用并且给他们打<ruby>补丁<rt>patches</rt></ruby>。有不止一个库可以做这件事,但是对我来说最简单的是 `responses`。我们来看一下怎么用,并且替换 `mock`
`mocker` 功能模拟 `requests` 有点问题,就是有很多设置。避免这个问题的一个好办法就是使用一个库,可以拦截 `requests` 调用并且给它们 <ruby>打补丁<rt>patch</rt></ruby>。有不止一个库可以做这件事,但是对我来说最简单的是 `responses`。我们来看一下怎么用,并且替换 `mock`
```
@responses.activate
@ -250,7 +246,6 @@ def test_retrieve_weather_using_responses(fake_weather_info):
然后运行测试:
```
============================= test session starts ==============================
...
@ -266,9 +261,9 @@ tests/test_weather_app.py::test_retrieve_weather_using_responses PASSED  [100%]
#### 缺点
`unittest.mock` 很像,测试和实现再一次耦合了。如果替换 `requests`, 测试就不能用了。
`unittest.mock` 很像,测试和实现再一次耦合了。如果替换 `requests`测试就不能用了。
### 2\. 使用<ruby>适配器<rt>adapter</rt></ruby>测试 API
### 2、使用适配器测试 API
_如果用模拟让测试耦合了我能做什么_
@ -293,7 +288,7 @@ def find_weather_for(city: str) -> dict:
变成这样:
```
def find_weather_for(city: str) -&gt; dict:
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)
@ -309,7 +304,6 @@ def requests_adapter(url: str) -> dict:
现在到了重构 `retrieve_weather` 函数的时候:
```
def retrieve_weather(city: str) -> WeatherInfo:
    """Finds the weather for a city and returns a WeatherInfo instance."""
@ -319,17 +313,16 @@ def retrieve_weather(city: str) -> WeatherInfo:
所以,如果你决定改为使用 `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)
```
[/code] [code]
def retrieve_weather(city: str) -&gt; WeatherInfo:
```
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)
@ -353,7 +346,6 @@ def test_retrieve_weather_using_adapter(
如果运行测试,会获得:
```
============================= test session starts ==============================
tests/test_weather_app.py::test_retrieve_weather_using_adapter PASSED    [100%]
@ -362,7 +354,7 @@ tests/test_weather_app.py::test_retrieve_weather_using_adapter PASSED    [100%
#### 优点
这个方法的优点是可以成功将测试和实现解耦。使用[<ruby>依赖注入<rt>dependency injection</rt></ruby>][6]在测试期间注入一个假的适配器。你也可以在任何时候更换适配器,包括在运行时。这些事情都不会改变任何行为。
这个方法的优点是可以成功将测试和实现解耦。使用<ruby>[依赖注入][6]<rt>dependency injection</rt></ruby>在测试期间注入一个假的适配器。你也可以在任何时候更换适配器,包括在运行时。这些事情都不会改变任何行为。
#### 缺点
@ -376,15 +368,14 @@ def requests_adapter(url: str) -> dict:
在生产环境中,适配器会有问题,而且单元测试没办法发现。但是事实是,之前的方法也会有同样的问题。这就是为什么不仅要单元测试,并且总是要集成测试。也就是说,要考虑另一个选项。
### 3\. 使用 VCR.py 测试 API
### 3使用 VCR.py 测试 API
现在终于到了讨论我们最后一个选项了。诚实地说,我也是最近才发现这个。我用<ruby>模拟<rt>mock</rt></ruby>也很长时间了,而且总是有一些问题。`VCR.py` 是一个库,它可以简化很多 HTTP 请求的测试。
它的工作原理是将第一次运行测试的 HTTP 交互记录为一个 YAML 文件,叫做 _cassette_。请求和响应都会被序列化。当第二次运行测试的时候,`VCT.py` 将拦截对请求的调用,并且返回一个响应。
它的工作原理是将第一次运行测试的 HTTP 交互记录为一个 YAML 文件,叫做 `cassette`。请求和响应都会被序列化。当第二次运行测试的时候,`VCT.py` 将拦截对请求的调用,并且返回一个响应。
现在看一下下面如何使用 `VCR.py` 测试 `retrieve_weather`
```
@vcr.use_cassette()
def test_retrieve_weather_using_vcr(fake_weather_info):
@ -400,10 +391,9 @@ _cassette 文件是什么样_
好问题。这个文件里有很多东西。这是因为 VCR 保存了交互中的所有细节。
```
interactions:
\- request:
- request:
    body: null
    headers:
      Accept:
@ -415,7 +405,7 @@ interactions:
      User-Agent:
      - python-requests/2.24.0
    method: GET
    uri: [https://api.openweathermap.org/data/2.5/weather?q=London\&appid=\][7]<YOUR API KEY HERE>&units=metric
    uri: https://api.openweathermap.org/data/2.5/weather?q=London&appid=<YOUR API KEY HERE>&units=metric
  response:
    body:
      string: '{"coord":{"lon":-0.13,"lat":51.51},"weather":[{"id":800,"main":"Clear","description":"clearsky","icon":"01d"}],"base":"stations","main":{"temp":16.53,"feels_like":15.52,"temp_min":15,"temp_max":17.78,"pressure":1023,"humidity":72},"visibility":10000,"wind":{"speed":2.1,"deg":40},"clouds":{"all":0},"dt":1600420164,"sys":{"type":1,"id":1414,"country":"GB","sunrise":1600407646,"sunset":1600452509},"timezone":3600,"id":2643743,"name":"London","cod":200}'
@ -458,24 +448,21 @@ _确实很多_
* 如果你改了请求,比如说用了错误的 header测试会失败。
* 没有与代码实现耦合,所以你可以换适配器,而且测试会通过。唯一有关系的东西就是请求必须是一样的。
#### 缺点
再与模拟相比较,除了避免了错误,还是有一些问题。
如果 API 提供者出于某种原因修改了数据格式测试仍然会通过。幸运的是这种情况并不经常发生而且在这种重大改变之前API 提供者通常会给他们的 API 提供不同版本。
另一个需要考虑的事情是<ruby>就地<rt>in place</rt></ruby><ruby>端到端<rt>end-to-end</rt></ruby>测试。每次服务器运行的时候,这些测试都会调用。顾名思义,这是一个范围更广、更慢的测试。们会比单元测试覆盖更多。事实上,并不是每个项目都需要使用它们。所以,就我看来,`VCR.py` 对于大多数人的需求来说都绰绰有余。
另一个需要考虑的事情是<ruby>就地<rt>in place</rt></ruby><ruby>端到端<rt>end-to-end</rt></ruby>测试。每次服务器运行的时候,这些测试都会调用。顾名思义,这是一个范围更广、更慢的测试。们会比单元测试覆盖更多。事实上,并不是每个项目都需要使用它们。所以,就我看来,`VCR.py` 对于大多数人的需求来说都绰绰有余。
### 总结
就这么多了。我希望今天你了解了一些有用的东西。测试 API 客户端应用可能会有点吓人。然而,当武装了合适的工具和知识,你就可以驯服这个野兽。
[我的 Github][8] 上可以找到完整的 app
[我的 Github][8] 上可以找到这个完整的应用
* * *
_这篇文章最早发表在[作者的个人博客][9]并且已得到授权_
_这篇文章最早发表在 [作者的个人博客][9]授权转载_
--------------------------------------------------------------------------------
@ -483,8 +470,8 @@ via: https://opensource.com/article/21/9/unit-test-python
作者:[Miguel Brito][a]
选题:[lujun9972][b]
译者:[https://github.com/Yufei-Yan](https://github.com/译者ID)
校对:[校对者ID](https://github.com/校对者ID)
译者:[Yufei-Yan](https://github.com/Yufei-Yan)
校对:[wxy](https://github.com/wxy)
本文由 [LCTT](https://github.com/LCTT/TranslateProject) 原创编译,[Linux中国](https://linux.cn/) 荣誉推出
@ -496,6 +483,6 @@ via: https://opensource.com/article/21/9/unit-test-python
[4]: https://en.wikipedia.org/wiki/Mock_object
[5]: https://miguendes.me/7-pytest-plugins-you-must-definitely-use
[6]: https://stackoverflow.com/questions/130794/what-is-dependency-injection
[7]: https://api.openweathermap.org/data/2.5/weather?q=London\&appid=\
[7]: https://api.openweathermap.org/data/2.5/weather?q=London&appid=
[8]: https://github.com/miguendes/tutorials/tree/master/testing_http
[9]: https://miguendes.me/3-ways-to-test-api-client-applications-in-python