Django の Abstract Model を py.test でテストする
※これは Python Advent Calendar 2015 2日目のエントリです。
各モデルが共通のカラムや振る舞いを持つようになってきたら
Django
でアプリケーションを開発していると、各モデルに共通のカラムを持たせたり、共通の振る舞いをさせたりしたいということが起こると思います。
そのような場合は、各モデルに対して同じような実装を施すのではなく、Abstract Model を定義して各モデルがそれを継承するようにするのが自然でしょう。
例えば以下の様な感じです。
from django.db import models class CommonAbstractModel(models.Model): created_datetime = models.DateTimeField(auto_now_add=True) updated_datetime = models.DateTimeField(auto_now=True) class Meta: abstract = True
from common.models import CommonAbstractModel class User(CommonAbstractModel): name = models.models.CharField(max_length=100)
このように実装することによって、User
モデルに直接 created_datetime
, updated_datetime
を定義する必要がなくなるので便利です。
ではテストをどうするか
CommonAbstractModel
を継承したモデルが確実に created_datetime
, updated_datetime
を持っているかということをテストした気持ちになるのが自然ですよね。
ただ abstract = True
なモデルは manage.py makemigrations
でマイグレーションが作成されないため、通常のモデルのように普通にテストを書くことがかないません。
とはいえ、子クラスである User
に対してそれらテストを行うのは、あくまで User
のテストであって CommonAbstractModel
をテストするということにはならないです(なるかも知れないけど、例えば CommonAbstractModel
に論理削除の実装が入ったりしたらそれも子クラスでテストするんですか?という話になりますね) 。
py.test
を使った例
実はこの例(継承したモデルが確実に created_datetime
, updated_datetime
を持っているかというテスト)だと、擬似的なマイグレーションを行う必要はまったくなく、擬似的な子クラスだけがあれば良かったりします。。。
Abstract Model に論理削除とかを実装したくなったときには必要ですが。
import pytest import datetime from common.models import CommonAbstractModel from django.db import connection from django.core.management.color import no_style @pytest.fixture(scope='module') def fx_TestInheritedModel(): class TestInheritedModel(CommonAbstractModel): class Meta: app_label = 'common' return TestInheritedModel @pytest.yield_fixture(scope='function') @pytest.mark.django_db def fx_test_model_cls(request, fx_TestInheritedModel): cursor = connection.cursor() statements, pending = connection.creation.sql_create_model(fx_TestInheritedModel, no_style()) for sql in statements: cursor.execute(sql) yield fx_TestInheritedModel statements = connection.creation.sql_destroy_model(fx_TestInheritedModel, (), no_style()) for sql in statements: cursor.execute(sql) @pytest.mark.django_db class TestCommonAbstractModel: @pytest.mark.parametrize(('attr_name'), [ 'created_datetime', 'updated_datetime', ]) def test_inherited_model_has_proper_fields(self, fx_TestInheritedModel, attr_name): inherited_model = fx_TestInheritedModel() assert hasattr(inherited_model, attr_name), 'the inherited model must have %s field' % (attr_name, )
ちょっとだけ解説
テスト内部で擬似クラスを作成してそれに CommonAbstractModel
を継承させて、それを擬似マイグレーションをさせてテストしてしまおうという戦略です。
擬似クラス
フィクスチャをこのように定義します。
app_label = 'common'
がめっちゃ重要で、これを定義しておかないと擬似マイグレーションさせることが出来ません。
@pytest.fixture(scope='module') def fx_TestInheritedModel(): class TestInheritedModel(CommonAbstractModel): class Meta: app_label = 'common' return TestInheritedModel
擬似マイグレーション
各テストメソッドが実行される前にテーブルが作成され、終わったら消されます。
yield_fixture
を使うのがポイントです(scope='function'
はデフォルトなのであえて指定する必要はないかもしれませんが、Explicit is better than implicit.
です。)
@pytest.yield_fixture(scope='function') @pytest.mark.django_db def fx_test_model_cls(request, fx_TestInheritedModel): cursor = connection.cursor() statements, pending = connection.creation.sql_create_model(fx_TestInheritedModel, no_style()) for sql in statements: cursor.execute(sql) yield fx_TestInheritedModel statements = connection.creation.sql_destroy_model(fx_TestInheritedModel, (), no_style()) for sql in statements: cursor.execute(sql)
あとは...
実際に fx_test_model_cls
を利用するテストをガシガシ書いていくだけです!
django.test.TestCase
を使いたい場合
普通に setUp
, tearDown
を使って同じようなことすれば良さそう。
EOF
Python でオブジェクト内部の dict に直接アクセスする
- Ruby でいうとこのこういうやつ。
class Hage def initialize() @hage = { bozu: 1, hage: 2 } end def [](key) @hage[key] end def []=(key, value) @hage[key] = value end end h = Hage.new() p h #=> #<Hage:0x007f94428a2c20 @hage={:bozu=>1, :hage=>2}> p h[:bozu] #=> 1 h[:fusa] = 3 p h[:fusa] #=> 3
- Python でやるとこう。
class Hage: def __init__(self): self.hage = { 'bozu': 1, 'hage': 2 } def __getitem__(self, key): return self.hage[key] def __setitem__(self, key, value): self.hage[key] = value h = Hage() print h #=> <__main__.Hage instance at 0x109f6aef0> print h['hage'] #=> 2 h['fusa'] = 3 print h['fusa'] #=> 3
thank you for letting me, @rrreeeyyy!
n 種類のカラーコードのちょうど平均のカラーコードが知りたいとき
2 つのカラーコードのちょうど中間のカラーコードを教えてくれる便利なやつないかな
— Satoshi SUZUKI (@studio3104) 2015, 4月 21
R,G,B でそれぞれの平均値で再構成したらちょうど真ん中になるかな?
— Satoshi SUZUKI (@studio3104) 2015, 4月 21
R,G,B でそれぞれの平均値で再構成したらちょうど真ん中になるのでは?
という感じで冗長だけど書いた
自作ライブラリを GitHub に置いて、そこから pip インストール出来るようにするまで
Rubyist でしたが Python もはじめました。
パーフェクト Python を Kindle で購入しましたが、検索出来ないし文字を選択できないのでコピペ出来なくて大変不便なので、物理版を購入すれば良かったと後悔しております。
話が逸れました。本題に。
これを書いている現在、当方 Python 歴数日 なので温かいアドバイスなどあれば是非ともよろしくお願いいたしますm(__)m
雛形を作る
paster create
を実行し、プロジェクトの設定を対話的に入力していきます。
$ pip install python_boilerplate_template
$ paster create
こんな感じになる。
$ tree -C hoge hoge ├── hoge │ └── __init__.py ├── hoge.egg-info │ ├── PKG-INFO │ ├── SOURCES.txt │ ├── dependency_links.txt │ ├── entry_points.txt │ ├── not-zip-safe │ └── top_level.txt ├── setup.cfg └── setup.py 2 directories, 9 files
実装
コードを書きます。
依存ライブラリを書いておく
requirements.txt
に依存ライブラリを書いていくのが普通っぽい。
んだけど、上述の雛形の作成方法だと requirements.txt
は作られないし、作成された setup.py
からは当然参照されなくて不便。
なので、setup.py
から requirements.txt
を参照させるようにして、作成したライブラリがインストールされるときにちゃんと依存ライブラリも一緒にインストールされるようにする。
--- a/setup.py +++ b/setup.py @@ -1,8 +1,14 @@ from setuptools import setup, find_packages -import sys, os +from pip.req import parse_requirements +import sys, os, pip version = '0.1' +requirements = [ + str(requirement.req) + for requirement in parse_requirements('requirements.txt', session = pip.download.PipSession()) +] + setup(name='hoge', version=version, description="test project", @@ -17,9 +23,7 @@ setup(name='hoge', packages=find_packages(exclude=['ez_setup', 'examples', 'tests']), include_package_data=True, zip_safe=False, - install_requires=[ - # -*- Extra requirements: -*- - ], + install_requires=requirements, entry_points=""" # -*- Entry points: -*- """,
あとは
普通に実装が終わったら普通に GitHub にコードを上げる。
で、使うときはこんな感じで。
作成するアプリケーションの依存に突っ込みたい場合
requirements.txt に追記
-e git://github.com/youraccount/youreggname#egg=eggname
インストール
$ pip install -r requirements.txt
普通にインストールする場合
$ pip install -e git://github.com/youraccount/youreggname#egg=eggname
この手順で作ったやつ
大物 alter table の途中で Lost connection to MySQL server during query が発生したときに確認したいこと
5時間モノの alter table が Lost connection to MySQL server during query したんだけど show processlist するとまだ動いてるっぽいから放置しとけば完遂するのかなと思ってるんだけど大丈夫だろうか、、、
とりあえず show processlist
してまだ動いてるっぽかったら、datadir 配下で #sql-xxxx_xxxxx.ibd
な名前のファイルが育っているかどうか確認する。
タイムスタンプが更新されていればテンポラリテーブルへのデータコピーが生きてるということになるので、あとは show processlist
を監視して終わるのを待てば良い。
で、終わったっぽかったら show create table
を確認して変更が反映されてれば完了。
って @la_luna_azul さんに教えてもらった。ありがとうございます!
ゆるく `my.cnf` の比較などが出来る gem を作った
複数の my.cnf
を比較して差異を知りたいが、通常のファイル同士の比較(diff
) ではわかりにくいしそもそも知りたい情報を得るのはつらい。
ということがあり作成していた書き捨てのスクリプトがあったのだが、整理してテストまで書いて rubygems
に放流した。
mycnf | RubyGems.org | your community gem host
mycnf.gem
機能概要
parse
my.cnf
のファイルパスを食わせると parse して hash を返す
generate
上記の parse
のフォーマットの hash を食わせると my.cnf
を文字列で返す
compare
上記の parse
のフォーマットの hash を複数食わせると、比較しやすいフォーマットで hash を返す
(ファイルパスを複数食わせて同様の結果を返す compare_files
もある)
diff
上記の compare
の結果から、差異のあるパラメタのみを select して返す
(ファイルパスを複数食わせて同様の結果を返す diff_files
もある)
使い方
こんな感じの my.cnf
があったとする。
$ cat /etc/my.cnf.1 [client] port = 3306 socket = /var/lib/mysql/mysql.sock [mysql] no_auto_rehash [mysqld] datadir = /var/lib/mysql port = 3306 socket = /var/lib/mysql/mysql.sock
$ cat /etc/my.cnf.2 [client] port = 3308 socket = /var/lib/mysql/mysql.sock [mysql] no_auto_rehash safe-updates [mysqld] datadir = /var/lib/mysql port = 3308 socket = /var/lib/mysql/mysql.sock
parse
MyCnf.parse('/etc/my.cnf.1')
{ client: { port: 3306, socket: '/var/lib/mysql/mysql.sock' }, mysql: { no_auto_rehash: '' }, mysqld: { datadir: '/var/lib/mysql', port: 3306, socket: '/var/lib/mysql/mysql.sock' } }
generate
MyCnf.generate({ client: { port: 3306, socket: '/var/lib/mysql/mysql.sock' } })
[client] port = 3306 socket = /var/lib/mysql/mysql.sock
compare
MyCnf.compare(MyCnf.parse('/etc/my.cnf.1'), MyCnf.parse('/etc/my.cnf.2')) MyCnf.compare_files('/etc/my.cnf.1', '/etc/my.cnf.2')
{ client: { port: [ 3306, 3308 ], socket: [ '/var/lib/mysql/mysql.sock', '/var/lib/mysql/mysql.sock' ] }, mysql: { no_auto_rehash: [ '', '' ], safe_updates: [ nil, '' ] }, mysqld: { datadir: [ '/var/lib/mysql', '/var/lib/mysql' ], port: [ 3306, 3308 ], socket: [ '/var/lib/mysql/mysql.sock', '/var/lib/mysql/mysql.sock' ] } }
diff
MyCnf.diff(MyCnf.parse('/etc/my.cnf.1'), MyCnf.parse('/etc/my.cnf.2')) MyCnf.diff_files('/etc/my.cnf.1', '/etc/my.cnf.2')
{ client: { port: [ 3306, 3308 ] }, mysql: { safe_updates: [ nil, '' ] }, mysqld: { port: [ 3306, 3308 ] } }
出来ないこと(0.0.1 現在)
暗黙のデフォルト値との比較は出来ない
たとえば、innodb_buffer_pool_size
の値が明示されいるものとそうでないものを比較した場合は以下のような結果が返る。
$ cat /tmp/my.a.cnf [mysqld] server_id = 1 innodb_buffer_pool_size = 128M $ cat /tmp/my.b.cnf [mysqld] server_id = 2 $ pry [1] pry(main)> require 'mycnf' => true [2] pry(main)> MyCnf.diff_files('/tmp/my.a.cnf', '/tmp/my.b.cnf') => {:mysqld=>{:server_id=>[1, 2], :innodb_buffer_pool_size=>["128M", nil]}}
MySQL 5.6 系において、innodb_buffer_pool_size
のデフォルト値は 128MB
であるが、そういったデフォルト値を考慮した比較をする実装にはなっていない。
あくまで明示的に記述されているパラメタのみの比較を行う。
!include, !includedir の先に定義してある設定を追いかける
すべてのメソッドにおいて、!include
, !includedir
の先で定義してあるパラメタを追うことは出来ない。
あくまで引数に与えたファイル及び hash の中だけで完結する範囲で処理を行います。
さて
こういうのって書き捨てて終わっちゃうことが多くて、あとでまた使いたいときに見つからないとかあったりして困ることがある。
ので、まぁこの程度ならすぐまた書けるべって思わなくもないけど、整理して次の機会にもまたすぐに使えるようにしておいた。