pytestでmonkeypatchを使う
(旧ブログからの移行記事です)
pytestでテストしたいクラスの一部メソッドをモック化したいときにmonkeypatchをつかう。この記事はそのメモ。
-- 例えば以下のようなクラスのテストをすることを想定する。
class Hoge: x = 10 y = 20 def sum_hoge(self): return self.x + self.y def sum_power(self): sum_x_y = self.sum_hoge() return pow(sum_x_y,2)
sum_hoge
をテストするときに、sum_hoge
内で参照しているクラス変数xを変えても確からしい結果(ここでは「変更後のx+yの値」になること)になるか確かめたい
このときmonkeypatchを使うと以下のようにテストできる
import Hoge # ここはディレクトリ構成等に合わせて変える def test_sum_hoge_success(monkeypatch): hoge = Hoge() monkeypatch.setattr(hoge, "x", 20) expected_result = 40 actual_result = hoge.sum_hoge() assert expected_result == actual_result
monkeypatchは組み込み関数なので新たにimportする必要はない。
次にsum_power
をテストする
このメソッドの中ではsum_hoge
を使っているので、sum_hoge
メソッドをモックしてsum_power
をテストする
import Hoge # ここはディレクトリ構成等に合わせて変える def test_sum_power_success(monkeypatch): def mock_sum_hoge(): return 50 hoge = Hoge() monkeypatch.setattr(hoge, "sum_hoge", mock_sum_hoge) expected_result = 2500 actual_result = hoge.sum_power() assert expected_result == actual_result
mock_sum_hoge
はHoge
クラスのsum_hoge
メソッドをモックしている関数
mockerを使う方法もあるようだが、違いはよくわかっていない
ひとまずはmonkeypatchでしのげるためこれでいく
【メモ】Azure FunctionsをVisual Studioから初めてデプロイする - その1
以下のドキュメントに従って作業を進める https://docs.microsoft.com/en-us/azure/azure-functions/functions-develop-vs?tabs=in-process
最初は単にログを表示するだけのFunctionにして、Blobとかにアクセスさせるのはあとでやることにする
Publishする
Publish to AzureのところでResource GroupやPlan Typeを選ぶところがある
Resource GroupやStorage Accountは事前に作っておけばよいだけ
Plan TypeはConsumptionのままが良さそう
←他のを選ぶとApp Service建てられてしまい、起動回数ベースの課金というメリットを失ってしまう模様
←今回は1日1回実行すればよいようなFunctionを考えているため、App Service建てられると困る
App Insightsの設定をする
Functionが出力するログを確認するにはApp Insightsの設定もする必要があるのだが、最初にデプロイした時点では未設定となっている(=自動で設定してくれるわけではない)
以下手順のNew function app in the portalの部分に従い設定するとMonitorでログが確認できるようになる
https://docs.microsoft.com/en-us/azure/azure-functions/configure-monitoring?tabs=v2#enable-application-insights-integration
(以下が実際のログ/最初はTimer Triggerでログ出力するだけのFunctionをデプロイしている)
そもそもこういったログのプライシングはどうなっているのか(Azure Functions自体が起動回数ベースの課金で安く済ませられてもログでお金がかかりすぎてはよくない)
https://azure.microsoft.com/en-us/pricing/details/monitor/
そもそもApp Insightsについてきちんと知らない(のでいつかドキュメント読む)
次にやりたいこと
Blob StorageやSQL Databaseにアクセスする
たぶんつづく
【メモ】 単体テストとinternalメソッド
publicにはしたくないけれどもテストをしたいメソッドの処遇をどうするべきか 例えばユーザの住所を登録するクラスをpublicとprivateのメソッドからなる以下のような処理で書いたとする
public class UserAddress { public void Register(string address) { var detailedAddress = AddressDetails(address); (DBに登録する処理) } private AddressDetails DecomposeAddress(string address) { var addressDetails = new AddressDetails { (住所をAddressDetailsに入るよう分割する処理) }; return addressDetails; } } public class AddressDetails { public string PostalCode { get; set; } public string Prefecture { get; set; } public string City { get; set; } }
ここでDecomposeAddressへ入力する住所が不正なときに想定した挙動になっているかテストしたいのだが、privateのままだと単体テストができない(Reflectionとやらを使ったらできるとのことだが、今回の話からそれるので今は触れない) しかしながらこのクラス自体は「(ユーザから)住所の文字列を受け取って(DBへ)登録する」という責務で作られているため、DecomposeAddressメソッドを外部に向けて公開する必要がない
C#の場合、このprivateメソッドをinternalにすることでテスト用のプロジェクトからアクセスできるようにするという方法を取れるらしい
具体的にはinternalなクラスをテストする場合、 [assembly: InternalsVisibleTo("テストのアセンブリ")]
というアトリビュートをクラスのあるファイル内に記述するとそのように設定できるとのこと(こちらの記載より)
このような方法がとれるのであれば、テストしたいような詳細は最初からinternalで書いてしまえばよい気がしている 実際こちらのStack Overflowの回答をみているとinternalクラスはテストされるべきで、またこちらのdiscussionからするとテストされるべき詳細はinternalで書くべきという主張を確認できる ※「テストされるべき詳細はinternalで書くべき」は本文からすると言いすぎかもしれない
InternalsVisibleToのアトリビュートを使えばテスト以外のアセンブリからも参照することは可能だが、それをしたいならそもそもpublicにすればよいだけなので、あくまで開発上は使いたいけど公開したくないようなクラス・メソッドに対して使うのがいいのかなと感じた
pytestで環境変数の一時的な設定をしたい
TL;DR
環境変数に関係するメソッドのテストで、一時的に環境変数を書き換えたい→mock.patch
を使うことで実現できる
やりたいこと
異常系のテストとして、環境変数IDが設定されていないときはKeyErrorが発生することを確認するテストを設けたい
設定
ファイルの構成は以下の通り
my_dir/ ├ src/ │ └ my_code.py └ test/ └ test_my_code.py
環境変数を取得するメソッドが以下のように存在していたとする(my_code.py)
from dataclasses import dataclass import os @dataclass class getEnvNames: def __init__(self): self.id = os.environ["ID"] self.name = os.environ["NAME"]
テストの方針
環境変数を一時的に書き換える場合はunittest.mock
のmock.patch
を使う
使い方は以下の通り
from unittest.mock import patch with patch.dict("os.environ", {"ID": "test"}): test_func()
まずは正常系のテスト
単純に置き換えるだけであれば以下のような感じでOK
from unittest.mock import patch from my_code import getEnvNames def test_getEnvNames_success(): with patch.dict("os.environ", {"ID": "1", "NAME": "test"}): target_class = getEnvNames() assert target_class.id = "1" assert target_class.name = "test"
次に異常系のテスト
今回の主題である環境変数が設定されていなときのバージョン いまのところ調べた感じだとwith句の中で環境変数の削除をするしかなさそう ここで設定された環境変数は一時的に削除されるようで、with句を出たらもとに戻っている様子
import pytest from unittest.mock import patch from my_code import getEnvNames def test_getEnvNames_failure(): os.environ["ID"] = "2" os.environ["NAME"] = "sample" with patch.dict("os.environ", {"ID": "1", "NAME": "test"}): os.environ.pop("ID", default=None) with pytest.raises(KeyError): getEnvNames() assert os.environ["ID"] == "2" assert os.environ["NAME"] == "sample"
elasticsearch.pyのexpand_wildcards指定について
背景
ES投入時にインデックスを削除する処理をしているが、そこで使っているメソッドにexpand_wildcards
という引数が用意されていた
これがデフォルトでopen
になっているらしく、このままではcloseしたインデックスは削除できないのでは?という疑問が生じた
例えば以下のような処理を想定している(以下のコードは動作確認していない)
# 準備 from elasticsearch import Elasticsearch es = Elasticsearch() # 以下の部分が es_client.indices.delete(index="target-index") # 本当はあるべきではないか? es_client.indices.delete(index="target-index", expand_wildcards="all")
ドキュメントにあたる
es-insertのインデックス削除に使っているのは上述の通りElasticsearch
クラスだが、このクラスのdelete
メソッドではexpand_wildcards
という引数は存在してなさそう
https://elasticsearch-py.readthedocs.io/en/7.x/api.html#elasticsearch.Elasticsearch.delete
一方IndicesClient
のdelete
にはexpand_wildcards
という引数が存在している
https://elasticsearch-py.readthedocs.io/en/7.x/api.html#elasticsearch.Elasticsearch.delete
この違いは一体何なのだ
そもそもElasticsearchとIndicesClientどちらのクラスを使うべきか?
Elasticsearch
クラスに関する記述を読んでみると、以下のようなことが書いてある(リンク)
The instance has attributes cat, cluster, indices, ingest, nodes, snapshot and tasks that provide access to instances of CatClient, ClusterClient, IndicesClient, IngestClient, NodesClient, SnapshotClient and TasksClient respectively. This is the preferred (and only supported) way to get access to those classes and their methods.
つまるところ、Elasticsearch
クラスのindices
というattributeはIndicesClient
にアクセスするためのものらしく、そちらを使ってほしいとのこと
ならなぜdelete
メソッドに使える引数が異なるのか引っかかるところだが、一旦その気持ちは抑えて、Elasticsearchクラスをそのまま使い次に進もうと思う
expand_wildcardsとはなにか
とはいえ結局IndicesClient
は使っているので、問題のこのクラス(メソッド)について見てみる
IndicesClient
クラスのdelete
メソッドに用意されているexpand_wildcards
の説明をみると以下のようなことが記載されている
Whether wildcard expressions should get expanded to open or closed indices (default: open) Valid choices: open, closed, hidden, none, all Default: open
Whether wildcard expressions should get expanded to open or closed indices
とあることから、あくまでこの引数は削除対象のインデックス名を指定する際にワイルドカードを用いた場合につかうものであるようだ
当初の疑問に対して答えると
- index引数にhoge-index
のようなインデックス名を指定している場合、そのインデックスがopenでもcloseでも削除できる
- index引数にhoge-*
のようなワイルドカードを用いた指定をしている場合、そのパターンにマッチするインデックスであってもcloseされているものは削除の対象にならない
ということになる
確証のない話
ElasticsearchにはResolve APIなるものがあるらしく、そちらが元なのか?とも感じている https://www.elastic.co/guide/en/elasticsearch/reference/current/indices-resolve-index-api.html