谈技术简友广场读书

单元测试中的模拟、存根和间谍

2023-03-12  本文已影响0人  技术的游戏

在昨天的测试策略文章之后,我收到了一位读者的邮件:

嘿,介意写一篇解释测试替身的文章吗?我有点理解它,但我正在尝试寻找一个更简单的定义,比如间谍、假货、存根,以及一个更好看的例子,说明何时使用这些类型的测试替身。

好问题!

如果你不知道模拟、间谍、替身、假货,随便你怎么称呼他们;不用担心!我有你想要的!

Mocking 定义

当您测试您的代码时,有时您想要调用代码的某个部分,但您不希望它全部运行。

在大多数测试库中,都有用于拦截函数调用(或整个类/对象)以伪造它们的实现和响应的工具。

我将这些伪造的实现称为“模拟”。这就是我将在本文中使用的术语。

一个例子

模拟在实践中有何帮助?

免责声明:此代码仅用于演示目的。它接近于正确的代码,但它肯定不起作用,目前无法运行!不要像教程一样复制粘贴并期望它有效。

这是一些代码:

from datetime import datetime
from fastapi import FastApi, HTTPException
import requests

app = FastAPI()

API_BASE_URL = 'www.external-weather-api.com/api'

@app.get("/temperature/{location_str}")
def get_temperature_for_location(location_str: str)
    """Get the current weather for a location"""
    weather_response = fetch_current_weather(location_str)
    return {'temperature': weather_response.body['temperature']}

def fetch_current_weather(location_str: str) -> dict:
    """Query external API for current weather in a location"""
    now_isoformat = datetime.utcnow().isoformat()
    location_id = get_location_id(location_str)
    weather_response = requests.get(
        f'{API_BASE_URL}/weather/{location_id}?date={now_isoformat}'
    )
    if weather_response.status_code != 200:
        raise HttpException(
            status_code=weather_response.status_code, 
            detail=weather_response.reason,
        )
    return weather_response.json()

def fetch_location_id(location_str: str) -> str:
    """Query external API for a location and return the location ID"""
    location_response = requests.get(
        f'{API_BASE_URL}/locations?search={location}'
    )
    if location_response.status_code != 200:
        raise HttpException(
            status_code=location_response.status_code, 
            detail=location_response.reason,
        )

    location_id = location_response.json()['data']['id']
    return location_id

基本上,我们查询外部 API 以获取我们应用程序中某个位置的当前温度。

在编写单元测试时,最佳做法是在运行测试时不要查询外部 API。因此,我们需要某种方式来模拟外部 API 响应。

模拟外部 API

每个测试库实现模拟都略有不同。对于这些示例,我将使用 Python 的标准 unittest

我开始用最小的、最低级别的单元编写单元测试。在这种情况下,fetch_location_id() 可能是最好的起点。

import unittest

class TemperatureTestCase(unittest.TestCase):
    def test_fetch_location_id_success(self):
        mock_location_response = unittest.mock.MagicMock()
        mock_location_response.status_code = 200
        mock_location_response.json.return_value = {'location_id': 'foobar'}

        with unittest.mock.patch(
            'requests.get', return_value=mock_location_response
        ):
            actual_response = fetch_location_id('bazqux')

        self.assertEqual(actual_response, 'foobar')

在这里,当代码在 with 块内运行时,我使用 unittest.mock 来替换 requests.get 的实现。

unittest.mock 允许我劫持执行并返回我自己的假响应,而不是向外部 API 发出实际请求。 这样,我们就不会在每次运行单元测试时都实际调用外部 API。

模拟失败

我还可以用来 unittest.mock 模拟外部 API 中的故障。

这是一个失败的单元测试:

class TemperatureTestCase(unittest.TestCase):
    def test_fetch_location_id_error(self):
        mock_location_response = unittest.mock.MagicMock()
        mock_location_response.status_code = 400
        mock_location_response.reason = 'Some error'

        with unittest.mock.patch(
            'requests.get', return_value=mock_location_response
        ):
            with self.assertRaises(HttpException) as exc:
                fetch_location_id('bazqux')

            self.assertEqual(exc.errors[0].status_code, 400)
            self.assertEqual(exc.errors[0].detail, 'Some error')

我可以让模拟调用做各种事情,包括引发错误。如果我使用类似的东西unittest.mock.patch('some_function', side_effect=Exception('Scary error!')),我可以模拟错误的发生并测试我的异常处理。

什么时候使用 Mocking

Mocking 对于编写好的测试非常有价值。以下是何时使用它的一些想法:

所有其他词是什么意思?

早些时候我们看到了一些与 Mocking 有关的其他词。这是一个小词汇表。

来自Stack Overflow,这是一个很好的概述:

这些都是微不足道的区别,而且在我看来是不必要的分裂。我几乎把所有东西都称为“模拟”。

如果您想更深入地了解这个术语,并在此过程中了解很多关于测试的知识,您可以查看 Martin Fowler 的“Mocks Aren't Stubs”

每日清单

我每天早上都会为软件开发人员写一些新东西。

如果你喜欢我的文章,点赞,关注,转发!

上一篇 下一篇

猜你喜欢

热点阅读