Python Advent Calendar 2013 の14日目です。
今日は、テストコードを書くときに便利なモジュール factory_boy を紹介します。
なお、この記事のサンプルコードは以下の環境で動作確認しています。
- Python 3.3.3(2.7でも動くように書いています)
- factory_boy 2.2.1
- SQLAlchemy 0.8.4
- SQLite3
インストール
インストールは、pipコマンド一発でできます:
pip install factory-boy
今回はSQLAlchemyも使うので、以下のコマンドでインストールしておきましょう:
pip install sqlalchemy
基本的な使い方
以下のコードをjojo.py という名前で保存してください:
import factory
from factory.alchemy import SQLAlchemyModelFactory
from sqlalchemy import Column, Integer, Unicode, create_engine
from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy.orm import scoped_session, sessionmaker
session = scoped_session(sessionmaker())
engine = create_engine('sqlite://')
session.configure(bind=engine)
Base = declarative_base()
class Character(Base):
u""" 登場人物 """
__tablename__ = 'character'
id = Column(Integer(), primary_key=True)
name = Column(Unicode(20))
def __str__(self):
return self.name
Base.metadata.create_all(engine)
class CharacterFactory(SQLAlchemyModelFactory):
FACTORY_FOR = Character
FACTORY_SESSION = session # the SQLAlchemy session object
id = factory.Sequence(lambda n: n)
name = factory.Sequence(lambda n: u'登場人物 %d' % n)
jojo.pyと同じディレクトリ上でpythonコマンドを実行して、以下のコードを入力してください:
>>> from jojo import session, Character, CharacterFactory
>>> session.query(Character).all() # ここではまだ何のデータも作られていない
[]
>>> CharacterFactory() # ここでfactory_boyがデータを生成する
<jojo.Character object at 0x10f08f8d0>
>>> session.query(Character).all() # データが1件追加されている
[<jojo.Character object at 0x10f08f8d0>]
>>> print(session.query(Character).all()[0])
登場人物 1
で、何が嬉しいの?
これだけだと何が嬉しいのか分かりませんね。では、コードをもう少し複雑にしてみましょう:
import factory
from factory.alchemy import SQLAlchemyModelFactory
from sqlalchemy import Column, Integer, Unicode, create_engine, ForeignKey
from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy.orm import scoped_session, sessionmaker, relationship, backref
session = scoped_session(sessionmaker())
engine = create_engine('sqlite://')
session.configure(bind=engine)
Base = declarative_base()
class Stand(Base):
u"""スタンド"""
__tablename__ = 'stand'
id = Column(Integer(), primary_key=True)
name = Column(Unicode(20))
def __str__(self):
return self.name
class Character(Base):
u""" 登場人物 """
__tablename__ = 'character'
id = Column(Integer(), primary_key=True)
name = Column(Unicode(20))
stand_id = Column(Integer, ForeignKey('stand.id'))
stand = relationship('Stand', backref=backref('character', uselist=False))
def __str__(self):
return "本体名:{0} スタンド名:{1}".format(self.name, self.stand.name)
Base.metadata.create_all(engine)
class StandFactory(SQLAlchemyModelFactory):
FACTORY_FOR = Stand
FACTORY_SESSION = session
id = factory.Sequence(lambda n: n)
name = factory.Sequence(lambda n: u'スタンド %d' % n)
class CharacterFactory(SQLAlchemyModelFactory):
FACTORY_FOR = Character
FACTORY_SESSION = session # the SQLAlchemy session object
id = factory.Sequence(lambda n: n)
name = factory.Sequence(lambda n: u'登場人物 %d' % n)
stand = factory.SubFactory(StandFactory)
CharacterモデルとStandモデルが1対1の関係になるように変更しました。再度pythonコマンドを実行して、以下のコードを入力してください:
>>> from jojo import session, Character, CharacterFactory
>>> session.query(Character).all() # ここではまだ何のデータも作られていない
[]
>>> CharacterFactory() # ここでfactory_boyがデータを生成する
<jojo.Character object at 0x105409910>
>>> print(session.query(Character).all()[0]) # Characterに紐づくStandも同時に生成されている
本体名:登場人物 1 スタンド名:スタンド 1
先ほどは、Characterモデルのデータが1個作られただけですが、今度はCharacterに紐づくStandも同時に生成されました。
さて、Character, Standモデルをテストコードの中で使う場面を想像してみてください。テストメソッド実行前にテストデータを投入する際、通常はどのような書き方になるでしょうか?
おそらく、テストデータをfixtureファイル(YAMLとかJSONとかXML)に書いて、それを読み込んでDBに登録するような形になるのではないかと思います。
fixtureファイルにCharacter, Standモデルのようにリレーションが存在するデータを書く際、どうしてもプライマリーキーを意識しなければなりません。
Characterに紐づくStandのidにうっかり実在しないものを書いてしまうと、当然テストデータ投入でコケてしまいます。
今回の例のリレーションは1体1のシンプルなものですが、実際のソフトウェア開発でのリレーションはもっと複雑です。1対多、多対多になると、まず頭のなかがグチャグチャになると思います(というか、私はなります)。
factory_boyを使うと、このfixtureファイル特有の煩わしさから開放されます。
10件テストデータを投入したいなら、こんなコードを書くだけで済みます:
>>> from jojo import session, Character, CharacterFactory
>>> for i in range(10):
... CharacterFactory()
...
<jojo.Character object at 0x10ac37450>
<jojo.Character object at 0x10ac37e10>
<jojo.Character object at 0x10ac37250>
<jojo.Character object at 0x10ac6e750>
<jojo.Character object at 0x10ac6e810>
<jojo.Character object at 0x10ac6e910>
<jojo.Character object at 0x10ac37190>
<jojo.Character object at 0x10ac37dd0>
<jojo.Character object at 0x10ac6ebd0>
<jojo.Character object at 0x10ac6eb10>
>>> for character in session.query(Character).all():
... print(character)
...
本体名:登場人物 1 スタンド名:スタンド 1
本体名:登場人物 2 スタンド名:スタンド 2
本体名:登場人物 3 スタンド名:スタンド 3
本体名:登場人物 4 スタンド名:スタンド 4
本体名:登場人物 5 スタンド名:スタンド 5
本体名:登場人物 6 スタンド名:スタンド 6
本体名:登場人物 7 スタンド名:スタンド 7
本体名:登場人物 8 スタンド名:スタンド 8
本体名:登場人物 9 スタンド名:スタンド 9
本体名:登場人物 10 スタンド名:スタンド 10
「登場人物1」とか「スタンド1」とかつまんない。もっと本物っぽいデータがほしいんだ!
データの内容はCharacterFactory, StandFactory に定義した内容に基づいて自動的に生成されますが、こんな風に具体的な値を指定することもできます:
>>> from jojo import session, Character, CharacterFactory, StandFactory
>>> CharacterFactory(name=u'空条承太郎', stand=StandFactory(name='スタープラチナ'))
>>> print(session.query(Character).all()[0])
本体名:空条承太郎 スタンド名:スタープラチナ
また、 Fuzzy attributes を利用することで、ランダムな値を生成することもできます。
例えば、こう書くと:
# Stand, StandFactory以外のコードは省略
import factory
from factory import fuzzy
from factory.alchemy import SQLAlchemyModelFactory
from sqlalchemy import Column, Integer, Unicode
class Stand(Base):
u"""スタンド"""
__tablename__ = 'stand'
id = Column(Integer(), primary_key=True)
name = Column(Unicode(20))
power = Column(Integer())
speed = Column(Integer())
firing_range = Column(Integer())
enduring = Column(Integer())
accuracy = Column(Integer())
potential = Column(Integer())
class StandFactory(SQLAlchemyModelFactory):
FACTORY_FOR = Stand
FACTORY_SESSION = session
id = factory.Sequence(lambda n: n)
name = factory.Sequence(lambda n: u'スタンド %d' % n)
power = fuzzy.FuzzyInteger(1, 5)
speed = fuzzy.FuzzyInteger(1, 5)
firing_range = fuzzy.FuzzyInteger(1, 5)
enduring = fuzzy.FuzzyInteger(1, 5)
accuracy = fuzzy.FuzzyInteger(1, 5)
potential = fuzzy.FuzzyInteger(1, 5)
power, speed, firing_range, enduring, accuracy, potential に1から5までのランダムな整数値が生成されます:
>>> from jojo import session, Stand, StandFactory
>>> for i in range(3):
... StandFactory()
...
<jojo.Stand object at 0x10b025610>
<jojo.Stand object at 0x10b11db90>
<jojo.Stand object at 0x10b11dcd0>
>>> for stand in session.query(Stand).all():
... print(stand.name, ":", stand.power, stand.speed, stand.firing_range, stand.enduring, stand.accuracy, stand.potential)
...
スタンド 1 : 5 5 2 2 5 1
スタンド 2 : 4 2 1 2 5 5
スタンド 3 : 4 1 5 5 3 4
文字列の場合は:
# Character, Characterfactory以外のコードは省略
import factory
from factory import fuzzy
from factory.alchemy import SQLAlchemyModelFactory
from sqlalchemy import Column, Integer, Unicode, ForeignKey
from sqlalchemy.orm import relationship, backref
class Character(Base):
u""" 登場人物 """
__tablename__ = 'character'
id = Column(Integer(), primary_key=True)
name = Column(Unicode(20))
signature_phrase = Column(Unicode(50))
stand_id = Column(Integer, ForeignKey('stand.id'))
stand = relationship('Stand', backref=backref('character', uselist=False))
def __str__(self):
return "本体名:{0} スタンド名:{1}".format(self.name, self.stand.name)
class CharacterFactory(SQLAlchemyModelFactory):
FACTORY_FOR = Character
FACTORY_SESSION = session # the SQLAlchemy session object
id = factory.Sequence(lambda n: n)
name = factory.Sequence(lambda n: u'登場人物 %d' % n)
signature_phrase = fuzzy.FuzzyChoice(
choices=(
u'このド低能がァーーッ',
u'このクサレ脳ミソがァーーッ',
u'このきたならしい阿呆がァーッ!!',
u'この花京院典明に精神的動揺による操作ミスは決してない!と思っていただこうッ!',
)
)
stand = factory.SubFactory(StandFactory)
choicesで指定した文字列のどれかが選択されます:
>>> from jojo import session, Character, CharacterFactory
>>> for i in range(10):
... CharacterFactory()
...
<jojo.Character object at 0x10ce9e310>
<jojo.Character object at 0x10ce9e990>
<jojo.Character object at 0x10ce9ee50>
<jojo.Character object at 0x10ced2c10>
<jojo.Character object at 0x10ced2c50>
<jojo.Character object at 0x10ced2d90>
<jojo.Character object at 0x10ced2cd0>
<jojo.Character object at 0x10ced2dd0>
<jojo.Character object at 0x10ced8050>
<jojo.Character object at 0x10ced8190>
>>> for character in session.query(Character).all():
... print(character.signature_phrase)
...
このきたならしい阿呆がァーッ!!
このクサレ脳ミソがァーーッ
このきたならしい阿呆がァーッ!!
このきたならしい阿呆がァーッ!!
このド低能がァーーッ
このクサレ脳ミソがァーーッ
このきたならしい阿呆がァーッ!!
このきたならしい阿呆がァーッ!!
このクサレ脳ミソがァーーッ
このド低能がァーーッ
もっと手の込んだデータを作りたいなら、BaseFuzzyAttributeクラスを継承して、 Custom fuzzy fields を作ることもできます。
最後に
factory_boyは実際に私が携わっている仕事で採用しているのですが、fixtureファイルを使っていた時にやっていた、他のテストのfixtureファイルをコピペで持ってきて、ちょっと編集して…のような作業が一切なくなって、かなり作業が楽になりました。
SQLAlchemy以外にも Django , Mogo , MongoEngine に対応しているので、いろいろなプロジェクトで使えるのではないでしょうか? Pythonでテストコードを書く際は、ぜひ使ってみてください。
明日は、 Attsun1031 さんです!