ラベル pytest の投稿を表示しています。 すべての投稿を表示
ラベル pytest の投稿を表示しています。 すべての投稿を表示

2024年8月25日日曜日

Python の unittest mock を使って requests の get をモックする方法

Python の unittest mock を使って requests の get をモックする方法

概要

unittest の mock を使って request.get をモックする方法を紹介します
更に pytest の fixture を使います

環境

  • macOS 11.7.10
  • Python 3.11.6
  • requests 2.32.3
  • pytest 8.2.2

サンプルコード

httpbin にリクエストしてその結果を dict に変換します

import requests


class HttpBinClient:
    def __init__(self) -> None:
        pass

    def fetch(self) -> requests.Response:
        return requests.get("https://httpbin.org/get")

    def to_json(self, response: requests.Response) -> dict:
        return response.json()


if __name__ == "__main__":
    hbc = HttpBinClient()
    res = hbc.fetch()
    print(hbc.to_json(res))

テストコード

httpbin のレスポンスには url というフィールドがありアクセスした url が含まれているのでそれが正しいかテストするコードを作成します

  • vim test/test_app.py
from app import HttpBinClient


class TestHttpBinClient:
    def test_fetch(self):
        hbc = HttpBinClient()
        response = hbc.fetch()
        result = hbc.to_json(response)
        assert result["url"] == "https://httpbin.org/get"
  • pipenv run pytest -s test/test_app.py

これで実行すると問題なく成功します
今回はこのテストにおいて requests.get で実際に外部にリクエストする部分をモックします

モックする conftest

conftest を使ってモックします
ポイントは requests.get に patch を当てつつ更にレスポンスのオブジェクトを正しく生成することです

  • vim test/conftest.py
from unittest.mock import Mock, patch

import pytest
import requests


@pytest.fixture(autouse=True)
def mock_request_get():
    with patch("requests.get") as m:
        # requests のレスポンスオブジェクトを生成、理由は requests.get の返り値の型が requests.Response なため
        response = requests.Response()
        # json 関数をモックします、関数をモックする場合は Mock クラスを使って return_value でその関数の返り値を指定します
        response.json = Mock(return_value={"url": "http://localhost"})
        response.status_code = 200
        # あとはモックオブジェクト (requests.get) の返り値に対してモックしたレスポンスを割り当てます
        m.return_value = response
        # 今回は with 句を使って patch しているため yield でモックオブジェクトを指定しなければいけません
        yield m

動作確認

  • pipenv run pytest -s test/test_app.py

で先程成功したテストが失敗することを確認します
モックでは実際にアクセスしないのでモックに指定した偽の返り値になるためエラーになります

最後に

pytest + unitest mock で requests.get をモックしていました
同じように post などもモックできます

2024年2月28日水曜日

pytest でテストする順番を制御する方法

pytest でテストする順番を制御する方法

概要

pytest-order というライブラリを使って実現します
pytest は基本的には上から実行されます
ファイルが別の場合は名前順に実行されます
またテストに親クラスが設定されている場合は親クラスから順番に実行されます

例えば親クラスにあるテストを後からテストしたい場合には pytest.mark.order というデコレータを使います

環境

  • macOS 11.7.10
  • Python 3.11.6
  • pytest 8.0.2
  • pytest-order 1.2.0

pytest-order なしの場合

  • vim test.py
import pytest


# Testxxx だと2度実行されてしまうので xxxTest という名前にする
class BaseTest:
    def test_two(self):
        print("two")
        assert 2 == 2

    def test_three(self):
        print("three")
        assert 3 == 3


class TestNumer(BaseTest):
    def test_one(self):
        print("one")
        assert 1 == 1
two
three
one

pytest-order ありの場合

import pytest


# Testxxx だと2度実行されてしまうので xxxTest という名前にする
class BaseTest:
    @pytest.mark.order(2)
    def test_two(self):
        print("two")
        assert 2 == 2

    @pytest.mark.order(3)
    def test_three(self):
        print("three")
        assert 3 == 3


class TestNumer(BaseTest):
    @pytest.mark.order(1)
    def test_one(self):
        print("one")
        assert 1 == 1
one
two
three

最後に

順番を制御したい場合には便利です
ただ 1 -> 2 -> 3 という order で 2 と 3 の間に入れたい場合は番号を一つずつインクリメントする必要があるので大変です
そんな場合には整数以外の order も設定できるので 1 -> 2 -> before 3 -> 3 という感じで設定することもできます

import pytest


# Testxxx だと2度実行されてしまうので xxxTest という名前にする
class BaseTest:
    @pytest.mark.order(2)
    def test_two(self):
        print("two")
        assert 2 == 2

    @pytest.mark.order(3)
    def test_three(self):
        print("three")
        assert 3 == 3

    @pytest.mark.order(before="test_three")
    def test_two_point_five(self):
        print("two_point_five")
        assert 2.5 == 2.5


class TestNumer(BaseTest):
    @pytest.mark.order(1)
    def test_one(self):
        print("one")
        assert 1 == 1
one
two
two_point_five
three

もしくは事前に整数を飛ばして間にテストが入っても良いようにしておきましょう

2023年6月29日木曜日

SQLAlchemyでautomap_baseを使った場合にpytestでmonkeypatchする方法

SQLAlchemyでautomap_baseを使った場合にpytestでmonkeypatchする方法

概要

前回 automap 機能を使ってみました
今回はそのコードに対してデータベースに接続しないで pytest を実行できるように monkeypatch を当ててみました

環境

  • macOS 13.4.1
  • Python 3.11.3
  • sqlalchemy 2.0.17
  • PyMySQL 1.1.0

コード

テストするコードは以下の main メソッドをテストします
実際に mysql が動作していなくてもテストが通るようにします

  • vim app.py
import sqlalchemy.ext.automap
from sqlalchemy import create_engine

# from sqlalchemy.ext.automap import automap_base # monkeypatch が当てられないパターン、必ずコード側とテストコード側では同じ import 方式にする
from sqlalchemy.orm import Session


def main():
    # automap を使用するための準備
    Base = sqlalchemy.ext.automap.automap_base()
    # Base = automap_base() # monkeypatch が当てられないパターン
    # エンジンの作成
    engine = create_engine("mysql+pymysql://root@localhost/test?charset=utf8mb4")
    # テーブル定義の読み込み
    Base.prepare(autoload_with=engine)
    # user テーブルから user モデルの抽出
    User = Base.classes.user
    # CRUD 用セッションの作成
    session = Session(engine)
    # ex) データ全件取得
    users = session.query(User).all()
    for user in users:
        print(user.name)

テストコード

conftest で monkeypatch を当てまくります
ポイントは

  • Base.prepare で接続しに行かせない
  • Session で好きな情報を返却させる

になります

  • vim test/conftest.py
import pytest
import sqlalchemy.ext.automap
from sqlalchemy.orm import Session


# automap_base に関するモック、これでデータベースへの接続を回避する
class DummyUser:
    def __init__(self) -> None:
        self.name = "hoge"


class DummyClasses:
    def __init__(self) -> None:
        self.user = DummyUser()


class DummyBase:
    def __init__(self) -> None:
        self.classes = DummyClasses()

    def prepare(self, autoload_with: bool = True):
        pass


def dummy_automap_base(declarative_base=None, **kwargs):
    return DummyBase()


# session.query 対するモック、データベースからの返却される値はこっちでコントロールする
class DummyQuery:
    def all(self):
        return []


def dummy_query(*args, **kwargs):
    return DummyQuery()


@pytest.fixture(autouse=True)
def mock_automap_base(monkeypatch):
    monkeypatch.setattr(sqlalchemy.ext.automap, "automap_base", dummy_automap_base)
    monkeypatch.setattr(Session, "query", dummy_query)
  • vim test/test_app.py
from app import main


class TestApp:
    def test_app(self):
        main()

動作確認

  • pipenv run pytest -s .

でデータベースが停止していてもテストが通ることを確認します

最後に

かなり無理やりな感じはしますが automap を使ったコードにデータベースに接続しないように monkeypatch を当ててみました
この方式の場合せっかくモデルを自動生成できるのにテスト側ではモデルを手動で生成しないといけないのが辛いです
なので automap を使ったユニットテストを書く場合には データベースが必須という状況でもいいのかもしれません

2023年3月28日火曜日

pytest-covでファイルや関数を無視する方法

pytest-covでファイルや関数を無視する方法

概要

カバレッジの計算からは外したい場合などがあると思います
その方法をいろいろ紹介します

環境

  • Ubuntu 18.04
  • Python 3.10.2
  • pytest-cov 2.10.1

.coveragerc

[report]
exclude_lines =
    if __name__ == .__main__.:
    pragma: no cover
omit =
    path/to/file/mymodule.py
    path/to/file/mymodule2.py

ちょっと解説

if __name__ はメイン関数の実行をテスト対象にしたくない場合に使います
if __name__ 配下でコールされているメソッドやクラスはすべてテストのカバレッジ対象から外れます

pragma: no cover は除外したい関数があるときに便利です
関数の名前の横にコメントで # pragma: no cover とすることでその関数をカバレッジの対象から除外できます
この方法の場合大本の関数を除外してもその関数内でコールされている他の関数は対象になるのですべて除外したい場合はすべてにコメントを入れる必要があります

omit を使ってファイルを指定するとそのファイル自体をすべて除外することができます
ファイルは複数指定することが可能です

参考サイト