ほとんどこれの話
tests
パッケージを用意sample
├── __init__.py
├── admin.py
├── apps.py
├── forms.py
├── models.py
├── utils.py
├── urls.py
├── views.py
└── tests
├── __init__.py
├── test_admin.py
├── test_forms.py
├── test_models.py
├── test_utils.py
└── test_views.py
# sample/utils.py ----
from datetime import date
def diff_days(from_date, to_date):
"""
- from_date から to_date までの日数を返す。
- from_date が to_date 以降であれば None を返す。
"""
if from_date >= to_date:
return None
return (to_date - from_date).days
# Usage --
date1 = date(2018, 1, 1)
date2 = date(2018, 1, 6)
print(diff_days(date1, date2)) # => 5
print(diff_days(date2, date1)) # => None
# sample/tests/test_utils.py ----
import unittest
from datetime import date
class TestDiffDays(unittest.TestCase):
def _callFUT(self, from_date, to_date):
from spam import diff_days
return diff_days(from_date, to_date)
def test_from_date_is_before(self):
""" from_date が to_date より古い日付 """
actual = self._callFUT(date(2018, 1, 1), date(2018, 1, 6))
self.assertEqual(5, actual)
def test_from_date_is_after(self):
""" from_date が to_date と同じか新しい日付 """
actual = self._callFUT(date(2018, 1, 6), date(2018, 1, 1))
self.assertIsNone(actual)
# FUT = Function Under the Test = テスト対象の関数
def _callFUT(self, from_date, to_date):
from spam import diff_days
return diff_days(from_date, to_date)
# Bad --
from spam import diff_days # ImportErrorが発生する
class TestDiffDays(unittest.TestCase):
def test_from_date_is_before(self):
# 〜 省略 〜
class TestOther(unittest.TestCase): # <- X 関係ないテストも実行されない
# Bad --
def test_all_test_cases(self):
# from_date < to_date
actual = self._callFUT(date(2018, 1, 1), date(2018, 1, 6))
self.assertEqual(5, actual)
# from_date >= to_date
actual = self._callFUT(date(2018, 1, 6), date(2018, 1, 1))
self.assertIsNone(actual)
def test_from_date_is_before(self):
""" from_date が to_date より古い日付 """
actual = self._callFUT(date(2018, 1, 1), date(2018, 1, 6))
self.assertEqual(5, actual)
# 1日前だったらという境界値
actual = self._callFUT(date(2018, 1, 1), date(2018, 1, 2))
self.assertEqual(1, actual)
def test_from_date_is_after(self):
""" from_date が to_date と同じか新しい日付 """
actual = self._callFUT(date(2018, 1, 6), date(2018, 1, 1))
self.assertIsNone(actual)
# 同日だったらという境界値
actual = self._callFUT(date(2018, 1, 1), date(2018, 1, 1))
self.assertIsNone(actual)
このような場合
# Bad --
def test_say_hello(self):
self.asserEqual(say_hello('tell-k'), 'hello tell-k') # 1. 失敗
self.asserEqual(say_hello('hirokiky'), 'hello hirokiky') # 2. 以後のアサーションは無視
self.asserEqual(say_hello('django'), 'hello django')
self.asserEqual(say_hello('bucho'), 'hello bucho')
self.asserEqual(say_hello('james'), 'hello james')
self.asserEqual(say_hello('nakagami'), 'hello nakagami')
self.asserEqual(say_hello('crohaco'), 'hello crohaco')
# Good --
def test_say_hello(self):
names = ['tell-k', 'james', 'django', 'bucho', ...]
for name in names:
with self.subTest(name=name, expected='hello %s' % name):
self.assertEqual(say_hello(name), expected) # <- テストが失敗しても次のサブテストは実行される
from django.test import TestCase
from sample.models import Item
class TestSample(TestCase):
def test_one(self):
Item.objects.create(name='name1')
self.assertEual(1, len(Item.objects.all()))
# テストケースが終わるとDBの中身はクリア(rollbackされる)
def test_two(self):
Item.objects.create(name='name1')
self.assertEual(1, len(Item.objects.all()))
$ python manage.py test
# テストt用に設定ファイルを用意して
$ python manage.py test --settings sample.settings_test
$ python manage.py test
Creating test database for alias 'default'... # <- テスト用DB生成
System check identified no issues (0 silenced).
.....................................................................
.....................................................................
----------------------------------------------------------------------
Ran 279 tests in 15.139s
OK (skipped=0)
Destroying test database for alias 'default'... # <- テスト用DB破棄
XXXTest
というクラス名はデフォルトでは探してくれないTestXXX
という風に Test
プレフィックスが必要
unittest.TestCase
(≒ django.test.TestCase
) と組み合わせると一部使えない機能がある
__repr__
とかねsetUp
, tearDown
で用意できるsetUp
… メソッド単位の前処理tearDown
… メソッド単位の後処理setUpClass
… クラス単位の前処理tearDownClass
… クラス単位の後処理from django.test import TestCase
class TestDoSomething(TestCase):
def _callFUT(self, data):
from sample.api import compose_data
return do_something(data)
def setUp(self):
# フィクスチャーの生成
self.good_data = make_fixture_data(good=True)
self.bad_data = make_fixture_data(bad=True)
def tearDown(self):
# フィクスチャーの破棄
del self.good_data
del self.bad_data
def test_do_something_ok(self):
self.assertTrue(self._callFUT(self.good_data))
def test_do_something_ng(self):
self.assertFalse(self._callFUT(self.bad_data))
- 無駄に生成している
# Good --
from django.test import TestCase
class TestDoSomething(TestCase):
def _callFUT(self, data):
from sample.api import compose_data
return do_something(data)
def test_do_something_ok(self):
good_data = make_fixture_data(good=True)
self.assertTrue(self._callFUT(good_data))
def test_do_something_ng(self):
bad_data = make_fixture_data(bad=True)
self.assertFalse(self._callFUT(bad_dataa))
だけど..
from django.test import TestCase
from myapp.models import Animal
class AnimalTestCase(TestCase):
fixtures = ['animals.json']
# sample/tests/factories.py
import factory
from account.tests.factories import UserFactory
class ItemFactory(factory.django.DjangoModelFactory):
name = factory.Sequence(lambda n: 'name{}'.format(n))
email = factory.Sequence(lambda n: 'hoge{}@example.com'.format(n))
price = 100
owner = factory.SubFactory(UserFactory)
class Meta:
model = "sample.Item"
item = ItemFactory()
print(item.name) # => name0
print(item.user) # => User object
# フィールドの値も指定できる
ItemFactory(name='newitem')
# 一気に複数オブジェクトを生成することもできる
ItemFactory.create_batch(10)
# テスト対象
def get_display_price(item):
return "{}円".format(item.price)
# Bad --
def test_display_price(self):
item = ItemFactory() # <- ItemFactory.price 100から変更されたらテスト失敗
expected = '100円'
self.assertEqual(expected, self._callFUT(item))
# Good --
def test_display_price(self):
item = ItemFactory(price=100) # <- 100固定
expected = '100円'
self.assertEqual(expected, self._callFUT(item))
# Good --
def test_display_price(self):
item = ItemFactory()
expected = '{}円'.format(item.price) # <- item.price を使って期待値を生成
self.assertEqual(expected, self._callFUT(item))
# sample/api.py ---
from item.api import calc_tax_included_price
# テスト対象
def get_display_price(item):
price = calc_tax_included_price(item) # <- これをモック(Test Stub)に置き換える
return "{}円".format(price)
def test_display_price(self):
item = ItemFactory()
# patch を通して 108 という間接入力値 をテスト対象(get_display_price) に渡してる
with mock.patch('sample.api.calc_tax_included_price', return_value=108) as m:
expected = '108円'
self.assertEqual(expected, self._callFUT(item)) # => OK
# calc_tax_included_price に item引数が渡ったかチェック
m.assert_called_with(item)
# egg.py ---
import spam
def say_egg():
return spam.say_spam() # <- patch対象
from unittest import mock
from egg import say_egg
with mock.patch('spam.say_spam', return_value="Patched!"):
print(say_egg()) # => Patched!
# egg.py ---
- import spam
+ from spam import say_spam
def echo():
- return spam.say_spam()
+ return say_spam()
from import
で importされたものは、元のモジュールから切り離されるsay_egg
) が利用してるものに patch
をあてる。# Good
- with mock.patch('spam.say_spam', return_value="Patched!"):
+ with mock.patch('egg.say_spam', return_value="Patched!"):
print(say_egg()) # => Patched!
patch
の影響下を局所化する意味でも import されてるところで patchする方が良いです。
- テストが意図した通りにパスしてるか
- テストが書かれてない場所がないか確認
- 不要なコードをないか(使われてない, 到達不能なコード)
$ coverage run --omit 'manage.py','*/migrations/*' python manage.py test
$ coverage report -m
--branch
オプションをつけて実行するName Stmts Miss Cover Missing
-----------------------------------------------------------
account/__init__.py 1 0 100%
account/admin.py 145 22 85% 19, 24-26, 59, 63-64, 100, 104-105, 137, 141-142, 173, 177-178, 209, 213-214, 247, 251-252
account/apps.py 9 0 100%
account/forms.py 29 0 100%
account/models.py 239 0 100%
account/views.py 150 5 97% 59, 101, 199
〜 省略 〜
-----------------------------------------------------------
TOTAL 28606 1240 96%
[run]
omit = */migrations/*,manage.py
- テストがない ところがわかる
- 素早くテストも終わる
# 特定のモジューブ
$ python manage.py test spam.tests.test_models
# 特定のテストクラス
$ python manage.py test spam.tests.test_models.TestSpamClass
# 特定のテストケース
$ python manage.py test spam.tests.test_models.TestSpamClass.test_sham
request
オブジェクトを view 以外に持ち出さない -> requsetから取り出したデータを渡すTemplateResponse.context
はcontextの辞書をそのあまま持ってるのでテストしやすい -> render
はもってないclass TestItemDetailView(TestCase):
def _getTarget(self, pk):
return reverse('item:detail', kwargs={'pk': pk})
def test_not_found(self):
res = self.client.get(self._getTarget(1))
self.assertEqual(404, res.status_code)
def test_display_item(self):
item = ItemFactory()
res = self.client.get(self._getTarget(item.id))
self.assertEqual(item.id, res.context['item'].id)
self.assertEqual(200, res.status_code)
self.assertTemplateUsed(res, 'item/detail.html')
manage.py test
では 実際にメールは送信されないmail.outbox
から取得可能 + それをテストするfrom django.core import mail
from django.test import TestCase
class TestEmail(TestCase):
def test_send_email(self):
mail.send_mail('subject',
'body.',
'from@example.com',
['to@example.com'], fail_silently=False)
self.assertEqual(len(mail.outbox), 1)
self.assertEqual(mail.outbox[0].subject, 'subject')
class TestSpamCommand(TestCase):
def _callCommand(self, *args, **kwargs):
from django.core.management import call_command
return call_command('spam', *args, **kwargs)
def test_spam_command(self):
...
>>> import logging
>>> from testfixtures import LogCapture
>>> with LogCapture() as l:
... logger = logging.getLogger()
... logger.info('a message')
... logger.error('an error')
...
>>> l.check(
... ('root', 'INFO', 'a message'),
... ('root', 'ERROR', 'an error'),
... )
class LoginTestCase(TestCase):
@override_settings(LOGIN_URL='/other/login/')
def test_login(self):
response = self.client.get('/sekrit/')
self.assertRedirects(response, '/other/login/?next=/sekrit/')
class MiddlewareTestCase(TestCase):
@modify_settings(MIDDLEWARE={
'append': 'django.middleware.cache.FetchFromCacheMiddleware',
'prepend': 'django.middleware.cache.UpdateCacheMiddleware',
})
def test_cache_middleware(self):
response = self.client.get('/')
どうすれば良いのか?
CELERY_ALWAYS_EAGER = True
[tox]
envlist = py36, flake8
skipsdist = true
[testenv]
deps = -r{toxinidir}/dev-requires.txt
setenv =
DJANGO_SETTINGS_MODULE = settings.test
changedir = {toxinidir}/src
commands = coverage run --omit "manage.py","*/migrations/\*" python manage.py test {posargs}
[testenv:flake8]
deps =
flake8
mccabe
commands = flake8 .
[flake8]
exclude = tests/\*, \*/migrations/\*, urls.py, manage.py
max-line-length = 100
$ tox # 全ての testenv が実行される
$ tox -e flake8 # flake8のtestenvだけ実行される
トピック