今回は、標準出力に文字列を出力する実装に対して pytest でテストを書く必要があり、 unittest.mock と Python のライブラリリファレンスにかかれていた方法を組み合わせたら見事にハマったお話です。
広告
シンプルにするとこんな感じに、標準出力への文字列が唯一の副作用となる関数があります。
def hello(name=None): if name: print(f"Hello, {name}") else: print("Hello") if __name__ == "__main__": import sys if len(sys.argv) >= 2: name = sys.argv[1] hello(name) else: hello()
コマンドラインから実行するとこのように引数の有無で出力が異なる内容になります。
$ python hello.py Hello $ python hello.py John Hello, John
テストのお作法としては、文字列と出力を分けることでテストしやすくするのが筋ですが、それができないこともたまにあります 1。
def generate_message(name=None): if name: return f"Hello, {name}" else: return "Hello" def hello(name): print(generate_message(name))
また、上記の例なら単純に doctest で良いのですが、もう少し複雑だったりするとそうも言っていられません。
Python のリファレンスマニュアルに標準出力のモック方法が書いてあったので、それにならって pytest でテストを書いてみました。
from hello import hello import pytest from unittest.mock import patch from io import StringIO @pytest.fixture() def mock_stdout(): with patch("sys.stdout", new_callable=StringIO) as m: yield m def test_hello_without_name(mock_stdout): hello() assert mock_stdout.getvalue() == "Hello\n" def test_hello_with_name(mock_stdout): hello("John") assert mock_stdout.getvalue() == "Hello, John\n"
テストのデバッグをするためにいつもの -s
をつけて実行してみると、きちんと通ります。
$ pytest -s test_hello.py =========================================================================================================== test session starts =========================================================================================================== platform darwin -- Python 3.8.3, pytest-6.2.4, py-1.10.0, pluggy-0.13.1 rootdir: /Users/devgru/work/blog-sandbox/2021-05-05T21:20:52 collected 2 items test_hello.py .. ============================================================================================================ 2 passed in 0.10s ============================================================================================================
しかし、 PR を作って CI でも動作することを確認したら、何故か落ちます。
$ pytest test_hello.py =========================================================================================================== test session starts =========================================================================================================== platform darwin -- Python 3.8.3, pytest-6.2.4, py-1.10.0, pluggy-0.13.1 rootdir: /Users/devgru/work/blog-sandbox/2021-05-05T21:20:52 collected 2 items test_hello.py FF [100%] ================================================================================================================ FAILURES ================================================================================================================= _________________________________________________________________________________________________________ test_hello_without_name _________________________________________________________________________________________________________ mock_stdout = <_io.StringIO object at 0x11104f9d0> def test_hello_without_name(mock_stdout): hello() > assert mock_stdout.getvalue() == "Hello\n" E AssertionError: assert '' == 'Hello\n' E - Hello test_hello.py:13: AssertionError ---------------------------------------------------------------------------------------------------------- Captured stdout call ----------------------------------------------------------------------------------------------------------- Hello __________________________________________________________________________________________________________ test_hello_with_name ___________________________________________________________________________________________________________ mock_stdout = <_io.StringIO object at 0x110b205e0> def test_hello_with_name(mock_stdout): hello("John") > assert mock_stdout.getvalue() == "Hello, John\n" E AssertionError: assert '' == 'Hello, John\n' E - Hello, John test_hello.py:17: AssertionError ---------------------------------------------------------------------------------------------------------- Captured stdout call ----------------------------------------------------------------------------------------------------------- Hello, John ========================================================================================================= short test summary info ========================================================================================================= FAILED test_hello.py::test_hello_without_name - AssertionError: assert '' == 'Hello\n' FAILED test_hello.py::test_hello_with_name - AssertionError: assert '' == 'Hello, John\n' ============================================================================================================ 2 failed in 0.16s ============================================================================================================
どうやら、unittest.mock.patch()
で差し替えた StringIO()
に値が書き込まれていないようです。
-s
オプションの有無で挙動が変わるということは、おそらく pytest
自体がモックをしているために、その後に patch
しても届かないと予想されます。
散々悩んだ挙げ句、 python pytest mock stdout
でググったところ、テックブログ界の雄こと Developers IO 2 の記事が見つかりました。
曰く、 capfd
を用いるのが良いとのこと。
from hello import hello def test_hello_without_name(capfd): hello() captured = capfd.readouterr() assert captured.out == "Hello\n" def test_hello_with_name(capfd): hello("John") captured = capfd.readouterr() assert captured.out == "Hello, John\n"
pytest test_hello2.py =========================================================================================================== test session starts =========================================================================================================== platform darwin -- Python 3.8.3, pytest-6.2.4, py-1.10.0, pluggy-0.13.1 rootdir: /Users/devgru/work/blog-sandbox/2021-05-05T21:20:52 collected 2 items test_hello2.py .. [100%] ============================================================================================================ 2 passed in 0.03s ============================================================================================================
今度は無事通りました。
- 作者:BrianOkken
- 発売日: 2018/08/29
- メディア: Kindle版
Pythonでテスト自動化を実現しよう(pytest Jenkins Selenium 活用編)
- 作者:kenpapa
- 発売日: 2017/06/16
- メディア: Kindle版