Django + pytestで、setupやteardown、fixtureのscopeなどの実行タイミングを調べてみた

Django + pytestでテストの初期化や後始末を書く場合、

  • django.test.TestCaseを使った、setUp()tearDown()
  • pytestを使った、classic xUnitスタイルのsetup_xxx()teardown_xxx()
  • pytestを使った、@fixtureのscope

などのテストフィクスチャが使えます。

今回、それぞれがどのタイミングで動作するのか知りたくなったため、実際に試してみることにしました。

なお、pytest全体については以下が参考になりました。
[Python] 初中級者のためのpytest入門 - くろのて

 
目次

 

環境

 
Djangoアプリの事前準備は以下の通りです。

>virtualenv -p c:\python35-32\python.exe env
>env\Scripts\activate
(env) >pip install django pytest-django
(env) >django-admin startproject myproject .
(env) >python manage.py startapp myapp

 

django.test.TestCaseのsetUp/tearDownで書く

django.test.TestCasePythonunittest.TestCaseのサブクラスであるため、setUpなどが使えます。
26.4.8.1. テストクラス - 26.4. unittest — ユニットテストフレームワーク — Python 3.5.1 ドキュメント

なお、動作中の関数・メソッド名を取得するため、以下を参考にして実装しました。
実行中の関数・メソッド名を取得したい - Qiita  

# myapp/tests/test_unittest_style.py
from django.test import TestCase
import sys

class Test_UnittestStyle(TestCase):
    @classmethod
    def setUpClass(cls):
        print('unittest style - setup > {}'.format(sys._getframe().f_code.co_name))
        
    @classmethod
    def tearDownClass(cls):
        print('unittest style - teardown > {}'.format(sys._getframe().f_code.co_name))
    
    def setUp(self):
        print('unittest style - setup > {}'.format(sys._getframe().f_code.co_name))
    
    def tearDown(self):
        print('unittest style - teardown > {}'.format(sys._getframe().f_code.co_name))
    
    
    # test method
    def testSpam(self):
        print('unittest style - setup > [{}]'.format(sys._getframe().f_code.co_name))
        assert True

    def testHam(self):
        print('unittest style - setup > [{}]'.format(sys._getframe().f_code.co_name))
        assert True

 
実行結果は以下の通りです*1

>py.test myapp/tests/test_unittest_style.py -s
...
myapp\tests\test_unittest_style.py
unittest style - setup > setUpClass
unittest style - setup > setUp
unittest style - setup > [testHam]
unittest style - teardown > tearDown
.
unittest style - setup > setUp
unittest style - setup > [testSpam]
unittest style - teardown > tearDown
.
unittest style - teardown > tearDownClass

 

pytestのxunitスタイルで書く

pytestでは、classic xunit-styleで書けるよう、setup_xxxやteardown_xxxが用意されています。
classic xunit-style setup - pytest

# myapp/tests/test_classic_xunit_style.py
from django.test import TestCase
import sys

def setup_module(module):
    print('classic xunit style - setup > {}'.format(sys._getframe().f_code.co_name))
    
def teardown_module(module):
    print('classic xunit style - teardown > {}'.format(sys._getframe().f_code.co_name))

def setup_function(function):
    print('classic xunit style - setup > {}'.format(sys._getframe().f_code.co_name))
    
def teardown_function(function):
    print('classic xunit style - teardown > {}'.format(sys._getframe().f_code.co_name))


class Test_ClassicXunitStyle(TestCase):        
    @classmethod
    def setup_class(cls):
        print('classic xunit style - setup > {}'.format(sys._getframe().f_code.co_name))
        
    @classmethod
    def teardown_class(cls):
        print('classic xunit style - teardown > {}'.format(sys._getframe().f_code.co_name))
        
    def setup_method(self, method):
        print('classic xunit style - setup > {}'.format(sys._getframe().f_code.co_name))
        
    def teardown_method(self, method):
        print('classic xunit style - teardown > {}'.format(sys._getframe().f_code.co_name))
    
    
    # test method
    def testSpam(self):
        print('classic xunit style > [{}]'.format(sys._getframe().f_code.co_name))
        assert True

    def testHam(self):
        print('classic xunit style > [{}]'.format(sys._getframe().f_code.co_name))
        assert True

 
実行結果は以下の通りです。今回の場合書き方が悪いのか、setup_function()は実行されませんでした。

(env) >py.test myapp/tests/test_classic_xunit_style.py -s
...
myapp\tests\test_classic_xunit_style.py 
classic xunit style - setup > setup_module
classic xunit style - setup > setup_class
classic xunit style - setup > setup_method
classic xunit style > [testHam]
.
classic xunit style - teardown > teardown_method
classic xunit style - setup > setup_method
classic xunit style > [testSpam]
.
classic xunit style - teardown > teardown_method
classic xunit style - teardown > teardown_class
classic xunit style - teardown > teardown_module

 

pytestのfixtureで書く

pytestのfixtureでは、scopeパラメータを使うことで、xUnitスタイルのsetup/teardownと同様のことができます。

 
fixtureを使う場合、

として実装します。

また、今回はautouse=Trueとしてfixtureを自動的に使うようにしています。
Autouse fixtures (xUnit setup on steroids) - pytest fixtures: explicit, modular, scalable

# myapp/tests/test_fixture_style.py
from django.test import TestCase
import pytest
import sys

class Test_PytestFixtureStyle(TestCase):
    @pytest.fixture(autouse=True)
    def scope_default(self, request):
        print('fixture style - setup > {}'.format(sys._getframe().f_code.co_name))
        
        def fin_scope_default():
            print('fixture style - teardown > {}'.format(sys._getframe().f_code.co_name))
        request.addfinalizer(fin_scope_default)
        
        
    @pytest.fixture(autouse=True, scope='function')
    def scope_function(self, request):
        print('fixture style - setup > {}'.format(sys._getframe().f_code.co_name))
        
        def fin_scope_function():
            print('fixture style - teardown > {}'.format(sys._getframe().f_code.co_name))
        request.addfinalizer(fin_scope_function)
        
        
    @pytest.fixture(autouse=True, scope='class')
    def scope_class(self, request):
        print('fixture style - setup > {}'.format(sys._getframe().f_code.co_name))
        
        def fin_scope_class():
            print('fixture style - teardown > {}'.format(sys._getframe().f_code.co_name))
        request.addfinalizer(fin_scope_class)
        
        
    @pytest.fixture(autouse=True, scope='module')
    def scope_module(self, request):
        print('fixture style - setup > {}'.format(sys._getframe().f_code.co_name))
        
        def fin_scope_module():
            print('fixture style - teardown > {}'.format(sys._getframe().f_code.co_name))
        request.addfinalizer(fin_scope_module)
        
        
    @pytest.fixture(autouse=True, scope='session')
    def scope_session(self, request):
        print('fixture style - setup > {}'.format(sys._getframe().f_code.co_name))
        
        def fin_scope_session():
            print('fixture style - teardown > {}'.format(sys._getframe().f_code.co_name))
        request.addfinalizer(fin_scope_session)
        
        
    # test method
    def testSpam(self):
        print('fixture style - setup > [{}]'.format(sys._getframe().f_code.co_name))
        assert True

    def testHam(self):
        print('fixture style - setup > [{}]'.format(sys._getframe().f_code.co_name))
        assert True

 
実行結果は以下の通りです。

(env) >py.test myapp/tests/test_fixture_style.py -s
...
myapp\tests\test_fixture_style.py 
fixture style - setup > scope_session
fixture style - setup > scope_module
fixture style - setup > scope_class
fixture style - setup > scope_default
fixture style - setup > scope_function
fixture style - setup > [testHam]
.
fixture style - teardown > fin_scope_function
fixture style - teardown > fin_scope_default
fixture style - setup > scope_default
fixture style - setup > scope_function
fixture style - setup > [testSpam]
.
fixture style - teardown > fin_scope_function
fixture style - teardown > fin_scope_default
fixture style - teardown > fin_scope_class
fixture style - teardown > fin_scope_module
fixture style - teardown > fin_scope_session

 

今までのパターンを一つのテストクラスに書いてみる

setUp/tearDown/fixtureをすべて1つのテストクラスに実装してみて、動作順を確認します。ソースコードtest_mix_style.pyです。

実行結果は以下の通りです。

(env) >py.test myapp/tests/test_mix_style.py -s
...
myapp\tests\test_mix_style.py 
mix style (classic xunit) - setup > setup_class
mix style (classic xunit) - setup > setup_method
mix style (fixture) - setup > scope_session
mix style (fixture) - setup > scope_module
mix style (unittest) - setup > setUpClass
mix style (fixture) - setup > scope_class
mix style (fixture) - setup > scope_default
mix style (fixture) - setup > scope_function
mix style (unittest) - setup > setUp
mix style > [testHam]
mix style (unittest) - teardown > tearDown
.
mix style (fixture) - teardown > fin_scope_function
mix style (fixture) - teardown > fin_scope_default
mix style (classic xunit) - teardown > teardown_method
mix style (classic xunit) - setup > setup_method
mix style (fixture) - setup > scope_default
mix style (fixture) - setup > scope_function
mix style (unittest) - setup > setUp
mix style > [testSpam]
mix style (unittest) - teardown > tearDown
.
mix style (fixture) - teardown > fin_scope_function
mix style (fixture) - teardown > fin_scope_default
mix style (classic xunit) - teardown > teardown_method
mix style (fixture) - teardown > fin_scope_class
mix style (unittest) - teardown > tearDownClass
mix style (classic xunit) - teardown > teardown_class
mix style (fixture) - teardown > fin_scope_module
mix style (fixture) - teardown > fin_scope_session

 
ちなみに、今まではテストファイルを一つだけ実行していましたが、複数のテストファイルを同時に実行すると

(env) >py.test -s
...
myapp\tests\test_fixture_style.py 
fixture style - setup > scope_session
fixture style - setup > scope_module
...
fixture style - teardown > fin_scope_module
...
myapp\tests\test_unittest_style.py
unittest style - setup > setUpClass
...
unittest style - teardown > tearDownClass
fixture style - teardown > fin_scope_session

scope=sessionであるfixtureのteardownが、一番最後に実行されました。

 

ソースコード

GitHubに上げました。
thinkAmi-sandbox/Django_pytest_setup_teardown_fixture-sample

*1:見やすくするため、一部に改行を入れています。以降も同様です。