Pythonのユニットテストの典型例

1. 推奨するテストの典型例

本当は’factory’を利用するパターンも記載しておきたいんだが、ちょっと長くなる事やDjango専用になってしまうことから今回は避ける。

1.1. pytestにおける典型例

""" 推奨するテストの典型例
"""
import pytest
import responses

class TestCase:
    @pytest.mark.parametrize("text", ["a", "a" * 50, "a" * 100])
    def test_param(text):
        """ パラメータ(テストデータ)は異なるがテスト内容は同じ場合に有効
        """
        # 準備
        from test_target import validate
        
        # 検証
        assert validate(text)

    @pytest.fixture
    def target_api(self):
        """ データベースのセットアップ等の共通の準備を定義する
            scope引数で以下の値により適用範囲を指定可能(デフォルトは'function')
            ・function: テストケースで適用
            ・class: テストクラスで適用
            ・module: テストファイル内で適用
            ・session: テスト全体で適用
        """
        return "/api/signulp"
    
    def test_fixture(self, target_api, django_app):
        """ ここでは共通準備を定義せずに、上記'fixture'に任せる
        """
        # 準備
        test_path=target_api

        # 実行
        res=django_app.post_json(test_path)

        # 検証
        assert res.status_code == 201
    
    @responses.activate
    def test_post(self):
        """ 外部へのWebリクエストをモックする
        """
        # 準備
        from test_target import post_to_sns
        responses.add(responses.POST, 'http://domain.example.com/posts', json={'body': 'レスポンス本文'})

        # 実行
        data = post_to_sns("投稿の本文")
        
        # 検証
        data['body'] == 'レスポンス本文'
    
    def test_CONST_VAL(self):
        """ テスト対象内の定数をモックしたい場合
        """
        # 準備
        from unittest import mock
        from test_target import is_enough_spam

        # 実行
        with mock.patch("hoge.NUM_OF_SPAM", new=1):
            actual = is_enough_spam()
        
        # 検証
        assert actual == True
    
    def test_raises(self):
        """ 仕様通りに例外が発生するか検証
        """
        # 検証
        with pytest.raises(TypeError):
            sum([1, None])

1.2. ファイルに関するテストの典型パターン

‘setup_method’と’teardown_method’が要としている。

‘setup_method’はテストの最初に実行され、ここではファイルの作成やファイル内容の定義を行っている。
‘teardown_method’はテストの最後に実行され、ここではファイルを閉じている。

""" ファイルに関するテストの典型パターン
"""
class TestCaseFile:
    def setup_method(self, method):
        """ テストケース毎に最初に実行される
        """
        import tempfile

        self.test_fp = tempfile.NamedTemporaryFile(mode='w', encoding="utf-8")
        self.test_csv = self.test_fp.name
        self.test_fp.writelines([
            'Spam,Ham,Egg\n',
            'Spam,Ham,Egg\n',
            'Spam,Ham,Egg\n',
            'Spam,Ham,Egg\n',
            'Spam,Ham,Egg\n',
        ])
        self.test_fp.seek(0)
    
    def teardown_method(self, method):
        """ テストケース毎に最後に実行される
        """
        self.test_fp.close()
    
    def test_import(self):
        """ このテストケースの前に'setup_method'が実行される
            このテストケースの後に'teardown_method'が実行される
        """
        # 準備
        from test_target import import_csv, Spam

        # 実行
        import_csv(self.test_csv)

        # 検証
        assert Spam.objects.count() == 5

2. テストケースのバッドケース

2.1. 過剰なモックの使用

パッと見で以下を理解できうるだろうか…。私には無理だ。

""" テストケースのバッドケース
"""
from django.test import TestCase
from unittest import mock

class TestBadCase(TestCase):
    @mock.patch('posts.search_posts', return_value=[{'title': 'タイトル', 'body': '本文'}])
    @mock.patch('forms.PostSearchForm')
    def test_search(self, m_search, m_form):
        """ 過剰なモックを使用すること
            ・客観的に何をしているか不明(助長なテストコード)
        """
        # 実行
        with mock.patch.object(m_form, 'is_valid', return_value=True):
            res = self.client.get('/posts', data={'search': '本文'})
        
        # 検証
        assert res == '本文'

コメントを残す