如何在Python单元测试中模拟httpx的post()和get()调用?

huangapple go评论66阅读模式
英文:

How to mock the post() and get() calls of httpx in python unitest?

问题

以下是您要翻译的部分:

The following test works when I patch the entire function get_post() and get_call(). How can I patch the httpx.post() and httpx.get()?

In src/app.py

import httpx

class Client:
    def __init__(self, url):
        self.url = url

    def post_call(self, payload):
        response = httpx.post(
                url=self.url,
                json=payload,
            )
            response.raise_for_status()
            return response.json()

    def get_call(self, id):
        response = httpx.get(
                url=self.url,
                params={"id": id},
            )

        response.raise_for_status()
        return response.json()

In test/test.py

from unittest.mock import patch

import httpx
import pytest
from src import app


@pytest.mark.anyio
@patch("app.get_call", return_value=httpx.Response(200, json={"status":"passed"}))
def test_get(mocker):
    cl = Client("test-url")
    result = cl.get_call("test")
    assert result.json() == {"status":"passed"}

@pytest.mark.anyio
@patch("app.get_post", return_value=httpx.Response(200,json={"status":"passed"}),)
def test_post(mocker):
    cl = Client("test-url")
    result = cl.post_call({"data":"test"})
    assert result.json() == {"status":"passed"}

When I tried to patch the httpx call then I get thrown with the error:
ModuleNotFoundError: No module named 'app.Client'; 'app' is not a package

英文:

The following test works when I patch the entire function get_post() and get_call(). How can I patch the httpx.post() and httpx.get()?

In src/app.py

import httpx

class Client:
    def __init__(self, url):
        self.url = url

    def post_call(self, payload):
        response = httpx.post(
                url=self.url,
                json=payload,
            )
            response.raise_for_status()
            return response.json()

    def get_call(self, id):
        response = httpx.get(
                url=self.url,
                params={"id": id},
            )

        response.raise_for_status()
        return response.json()

In test/test.py

from unittest.mock import patch

import httpx
import pytest
from src import app


@pytest.mark.anyio
@patch("app.get_call",return_value=httpx.Response(200, json ={"status":"passed"}))
def test_get(mocker):
    cl = Client("test-url")
    result = cl.get_call("test")
    assert result.json() == {"status":"passed"}

@pytest.mark.anyio
@patch("app.get_post", return_value=httpx.Response(200,json={"status":"passed"}),)
def test_post(mocker):
    cl = Client("test-url")
    result = cl.post_call({"data":"test"})
    assert result.json() == {"status":"passed"}

When I tried to patch the httpx call then I get thrown with the error:
ModuleNotFoundError: No module named 'app.Client'; 'app' is not a package

答案1

得分: 2

修复导入路径

假设您的目录结构如下:

.
├── src
│   ├── app.py
└── test
    └── test_app.py

然后,您不应该使用 from src import app,因为 src 不是一个包,它只是一个目录。为了帮助 pytest 定位 app.py,请创建一个空的 src/conftest.py 文件,以便您有以下结构:

.
├── src
│   ├── app.py
│   └── conftest.py
└── test
    └── test_app.py

然后在 test_app.py 中,不要使用:

from src import app

而应该使用:

import app

修复语法错误

在您的测试代码中缺少一个 ",如目前的写法会导致测试失败并显示如下错误:

E     File "/home/lars/tmp/python/test/test_app.py", line 19
E       result = cl.post_call({"data:"test"})
E                                         ^
E   SyntaxError: unterminated string literal (detected at line 19)

我们需要将第19行改为:

result = cl.post_call({"data": "test"})

另外,在您的测试函数中有以下代码:

cl = Client("test")

这将失败,因为您导入了 app,而不是 from app import Client。我们需要修复导入语句或修复测试代码:

cl = app.Client("test")

修复 @patch 调用

您现有的 patch 调用是有问题的,因为您的 app 模块既没有 get_call 方法也没有 post_call 方法 — 这些方法在 Client 类中。幸运的是,我们需要替换它们来修补 httpx.gethttpx.post

from unittest.mock import patch

import httpx
import pytest
import app

@patch("app.httpx.get")
def test_get(fake_httpx_get):
    fake_httpx_get.return_value = httpx.Response(
        200,
        json={"status": "passed"},
        request=httpx.Request("GET", "test"),
    )
    cl = app.Client("test-url")
    result = cl.get_call("test")
    assert result == {"status": "passed"}

@patch("app.httpx.post")
def test_post(fake_httpx_post):
    fake_httpx_post.return_value = httpx.Response(
        200,
        json={"status": "passed"},
        request=httpx.Request("POST", "test"),
    )
    cl = app.Client("test-url")
    result = cl.post_call({"data": "test"})
    assert result == {"status": "passed"}

我将设置模拟返回值的代码移到函数内部,因为我们还需要设置 request 参数;如果不这样做,您的调用 res.raise_for_status() 将失败:

RuntimeError: Cannot call `raise_for_status` as the request instance has not been set on this response.

使用上述目录结构和测试代码,当我们从顶层目录运行 pytest 时,结果如下:

========================================================= test session starts =========================================================
platform linux -- Python 3.11.1, pytest-7.2.1, pluggy-1.0.0 -- /home/lars/.local/share/virtualenvs/python-LD_ZK5QN/bin/python
cachedir: .pytest_cache
rootdir: /home/lars/tmp/python
plugins: anyio-3.6.2
collected 2 items

test/test_app.py::test_get PASSED                                                                                               [ 50%]
test/test_app.py::test_post PASSED                                                                                              [100%]

========================================================== 2 passed in 0.08s ==========================================================

我们还可以测试您的代码如何响应 HTTP 错误:

@patch("app.httpx.post")
def test_post_failure(fake_httpx_post):
    fake_httpx_post.return_value = httpx.Response(
        400,
        json={"status": "failed"},
        request=httpx.Request("POST", "test"),
    )
    cl = app.Client("test-url")
    with pytest.raises(httpx.HTTPStatusError):
        cl.post_call({"data": "test"})
英文:

Fix import paths

Assuming that your directory layout looks like this:

.
├── src
│   ├── app.py
└── test
    └── test_app.py

Then you don't want from src import app, because src isn't a package. It's just a directory. To help pytest locate app.py, create an empty src/conftest.py so that you have:

.
├── src
│   ├── app.py
│   └── conftest.py
└── test
    └── test_app.py

Then in test_app.py, instead of:

from src import app

Write:

import app

Fix syntax errors

There is a missing " in your test code; as written, attempting to run a test will fail with:

E     File "/home/lars/tmp/python/test/test_app.py", line 19
E       result = cl.post_call({"data:"test"})
E                                         ^
E   SyntaxError: unterminated string literal (detected at line 19)

We need to correct line 19 to read:

result = cl.post_call({"data": "test"})

Additionally, in your test functions you have:

cl = Client("test")

This will fail because you have import app, not from app import Client. We need to either fix the import statement or fix the test code:

cl = app.Client("test")

Fix @patch calls

Your existing patch invocations are broken as written, since your app module has neither get_call or get_post methods -- these are methods on the Client class. Fortunately, we need to replace this by patching httpx.get and httpx.post:

from unittest.mock import patch

import httpx
import pytest
import app


@patch("app.httpx.get")
def test_get(fake_httpx_get):
    fake_httpx_get.return_value = httpx.Response(
        200,
        json={"status": "passed"},
        request=httpx.Request("GET", "test"),
    )
    cl = app.Client("test-url")
    result = cl.get_call("test")
    assert result == {"status": "passed"}


@patch("app.httpx.post")
def test_post(fake_httpx_post):
    fake_httpx_post.return_value = httpx.Response(
        200,
        json={"status": "passed"},
        request=httpx.Request("POST", "test"),
    )
    cl = app.Client("test-url")
    result = cl.post_call({"data": "test"})
    assert result == {"status": "passed"}

I've moved setting the return value on the mock inside the functions because we also need to set the request parameter; without that your calls to res.raise_for_status() will fail:

RuntimeError: Cannot call `raise_for_status` as the request instance has not been set on this response.

With the above directory structure and test code, when we run pytest from the top-level directory, it results in:

========================================================= test session starts =========================================================
platform linux -- Python 3.11.1, pytest-7.2.1, pluggy-1.0.0 -- /home/lars/.local/share/virtualenvs/python-LD_ZK5QN/bin/python
cachedir: .pytest_cache
rootdir: /home/lars/tmp/python
plugins: anyio-3.6.2
collected 2 items

test/test_app.py::test_get PASSED                                                                                               [ 50%]
test/test_app.py::test_post PASSED                                                                                              [100%]

========================================================== 2 passed in 0.08s ==========================================================

We can also test how your code responds to HTTP errors:

@patch("app.httpx.post")
def test_post_failure(fake_httpx_post):
    fake_httpx_post.return_value = httpx.Response(
        400,
        json={"status": "failed"},
        request=httpx.Request("POST", "test"),
    )
    cl = app.Client("test-url")
    with pytest.raises(httpx.HTTPStatusError):
        cl.post_call({"data": "test"})

huangapple
  • 本文由 发表于 2023年2月24日 10:17:46
  • 转载请务必保留本文链接:https://go.coder-hub.com/75552070.html
匿名

发表评论

匿名网友

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen:

确定