View All Posts. MiCHiLU.com powered by Django ;-)

Start Presentation via s6 by amachang.


[Django][s6]: Django Testing

Django勉強会 Disk 4 でプレゼンした資料です。

Django Testing

MiCHiLU

takanao at endoh dot tk

2007-08-25

TDD

Test Driven Development

開発サイクル

  1. Test
  2. Coding
  3. Refactoring

要するに

チェンジセットにTESTが含まれてればいい

あとから書いてもいいんじゃない?

コードのCheckInと同時にTestも含める

何がうれしいか

基本的な使い方

$ ./manage.py test [app_name ...]

どのTESTが実行されるか

settings.INSTALLED_APPS に含まれる app ディレクトリの

docstring, unittest.TestCase のサブクラス

TESTファイルの配置

project
|---__init__.py
|---app
|   |---__init__.py
|   |---models.py
|   |---tests.py
|   `---views.py
|---manage.py
|---settings.py
|---media
|---templates
`---urls.py

実行例

$ ./manage.py test
Creating test database...
...
---------------------------
Ran 11 tests in 31.182s

OK
Destroying test database...

doctest

TESTの記述

Pythonインタプリタの出力を貼付けるだけ

Interactive Console

$ ./manage.py shell [--plain]
>>> import django
>>> django.VERSION
(0, 97, 'pre')
>>> assert(django.VERSION == (0, 96, 'pre'))
...
AssertionError

doctest

# -*- coding: utf-8 -*-
"""
>>> import django
>>> django.VERSION
(0, 97, 'pre')

# 次は AssertionError になる
>>> assert(django.VERSION == (0, 96, 'pre'))
...
AssertionError
"""

HttpResponseをテストしたい

Test Client

>>> from django.test.client import Client
>>> c = Client()
>>> response = c.get("/")
>>> response.status_code
200
>>> "%d bytes" % len(response.content)
'8456 bytes'
>>> response.headers.get("Content-Type")
'text/html; charset=utf-8'
>>> res = c.get("/redirect"); res.status_code, res.headers["Location"]
(301, '/redirect/')
>>> res = c.post("/not.found", data=dict(form="dummy")); res.status_code, res.cookies
(404, <SimpleCookie: >)

# response.template, response.context

contentの解析

>>> response.content.splitlines()[0]
'<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"'

simplejson

>>> response = c.get("/blog/posts.json")
>>> from django.utils import simplejson
>>> json = simplejson.read(response.content)
>>> json[0].items()
[(u'pk', u'1'), (u'model', u'blog.entry'), (u'fields', {u'content': u'\u30c6\u30b9\u30c8 ...
>>> json[0].get("fields").items()
[(u'content', u'\u30c6\u30b9\u30c8'), (u'add_date', u'2007-08-18 16:05:14'), (u'tag', [1])]

@login_required

0.97-pre

>>> c.login(username="test", password="none")
False
>>> c.login(username="test", password="secret")
True

テストデータ

追加したり、更新したり、削除したり。

追加したり、更新したり。。。

orz

Fixtures

データの固まりをimportできる

JSON, XML, YAML

django.core.management

0.97-pre

>>> from django.core import management
>>> management.call_command("loaddata", "utils/fixtures/auth.json")
Loading 'utils/fixtures/auth' fixtures...
Installing json fixture 'utils/fixtures/auth' from absolute path.
Installed 46 object(s) from 1 fixture(s)
>>> management.call_command("flush")
Loading 'initial_data' fixtures...
No fixtures found.

django.core.management

0.96

>>> from django.core import management
>>> management.load_data(["helpdoc/tests/auth.json"])
Loading 'utils/fixtures/auth' fixtures...
Installing json fixture 'utils/fixtures/auth' from absolute path.
Installed 46 object(s) from 1 fixture(s)
>>> management.flush()
Loading 'initial_data' fixtures...
No fixtures found.

テストデータを作る

管理画面で作る

$ ./manage.py dumpdata app --format=json --indent=2 >app/fixtures/test.json

スクリプトで作る

>>> from django.core import serializers
>>> from michilu.blog.models import Tag
>>> [(field.name, field.get_internal_type()) for field in Tag._meta.fields]
[('value', 'CharField'), ('id', 'AutoField')]
>>> tag = Tag(value="DUMMY_VALUE")
>>> template = serializers.serialize("json", queryset=[tag])
>>> template
'[{"pk": "None", "model": "blog.tag", "fields": {"value": "DUMMY_VALUE"}}]'

スクリプトで作る

>>> template
'[{"pk": "None", "model": "blog.tag", "fields": {"value": "DUMMY_VALUE"}}]'
>>> template = template[1:-1].replace('"None"', '"%d"').replace('"DUMMY_VALUE"', '"%s"')
>>> template
'{"pk": "%d", "model": "blog.tag", "fields": {"value": "%s"}}'
>>> fixture = "[%s]" % (",".join([template % (i,"Spam!"*i) for i in range(1,4)]))
>>> fixture
'[{"pk": "1", "model": "blog.tag", "fields": {"value": "Spam!"}},
  {"pk": "2", "model": "blog.tag", "fields": {"value": "Spam!Spam!"}},
  {"pk": "3", "model": "blog.tag", "fields": {"value": "Spam!Spam!Spam!"}}]'
>>> json = simplejson.read(fixture)
>>> json[0]
{u'pk': u'1', u'model': u'blog.tag', u'fields': {u'value': u'Spam!'}}

lorem ipsum

Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.

django.contrib.webdesign

lorem_ipsum.py 0.97-pre

>>> from django.contrib.webdesign import lorem_ipsum
>>> lorem_ipsum.sentence()  #ramdom
u'Laudantium nihil facere, eveniet laudantium sapiente atque harum voluptatem expedita dolor unde,
 provident itaque harum ipsam doloremque laudantium ex corrupti quisquam necessitatibus?'
>>> lorem_ipsum.paragraph()  #between 1 and 4 sentences
u'Hic dolore voluptas ducimus magni deserunt id quo eum suscipit, ...
>>> lorem_ipsum.paragraphs(5)  #paragraph lists
['Lorem ipsum dolor sit amet, consectetur adipisicing elit, ...
>>> lorem_ipsum.words(5, common=False)  #ramdom words
u'dolores necessitatibus vitae aut voluptatum'

寿限無、寿限無

寿限無、寿限無 五劫の擦り切れ 海砂利水魚の 水行末 雲来末 風来末 食う寝る処に住む処 やぶら小路の藪柑子 パイポパイポ パイポのシューリンガン シューリンガンのグーリンダイ グーリンダイのポンポコピーのポンポコナーの 長久命の長助。

jugem.py

>>> from utils.contrib.webdesign import jugem
>>> print jugem.sentence()
シューリンガンのグーリンダイ magna lorem 五劫の擦り切れ dolor
>>> print jugem.paragraph()
Magna やぶら小路の藪柑子 ut dolore incididunt 長久命の長助。, やぶら小路の藪柑子 incididunt 五劫の擦り切れ tempor ...
>>> print jugem.paragraphs(5)
[u'\u5bff\u9650\u7121\u3001\u5bff\u9650\u7121\n\u4e94\u52ab\u306e\u64e6\u308a\u5207 ...
>>> print jugem.words(5, common=False)
labore 食う寝る処に住む処 eiusmod 長久命の長助。 sed

思いつく限りのテストを投入していくと...

もっとサクサクやりたい

unittest

TestCase

app/tests.py

# -*- coding: utf-8 -*-
"""
... doctest here ...
"""
from django.test import TestCase
from django.test.client import Client

class SimpleTest(TestCase):
    fixtures = ['mammals.json', 'birds']

    def test_details(self):
        response = self.client.get('/customer/details/')
        self.failUnlessEqual(response.status_code, 200)

テストメソッドを指定して実行する

0.97-pre

$ ./manage.py test
$ ./manage.py test app
$ ./manage.py test app.SimpleTest
$ ./manage.py test app.SimpleTest.test_details

django.test.TestCase

0.97-pre

assertContains(response, text, count=None, status_code=200)
assertFormError(response, form, field, errors)
assertRedirects(response, expected_path, status_code=302, target_status_code=200)
assertTemplateNotUsed(response, template_name)
assertTemplateUsed(response, template_name)

doctests.py

http://michilu.googlecode.com/svn/trunk/utils/doctests.py

>>> from utils.doctests import Test
>>> t = Test()
>>> response = t.client.get("/admin")
>>> t.assertRedirects(response, "/admin/", status_code=301)
>>> response = t.client.get("/admin/")
>>> t.assertContains(response, "Password")
>>> t.client.login(username="test", password="secret")
True
>>> response = t.client.get("/admin/")
>>> t.assertContains(response, "Password")
Found 0 instances of 'Password' in response (expected 1)

doctest vs unittest

doctest

unittest

使い分け

doctest

unittest

0.96 vs 0.97-pre

Fixturesを指定して起動する

0.97-pre

$ ./manage.py testserver testdata.json
Creating test database...
...
Loading 'testdata' fixtures...
Installing json fixture 'testdata' from absolute path.
Installed 46 object(s) from 1 fixture(s)
Validating models...
Django version 0.97-pre, using settings 'michilu.settings'
Development server is running at http://127.0.0.1:8000/
Quit the server with CONTROL-C.
^C
Server stopped.
Note that the test database, ':memory:', has not been deleted. You can explore it on your own.

Others

More ...

http://michilu.com/django/doc-ja/testing/

Sat, 25 Aug 2007 11:47:15 +0900 source edit
Creative Commons License
This work is licensed under a Creative Commons Attribution-Noncommercial-Share Alike 2.1 Japan License.
View All Posts. MiCHiLU.com powered by Django ;-)