Django で unittest を高速化する(主にDBの話し)

yuheiさんが書き残してくれたありがたい記事を参考にゴニョゴニョしてて気になって事があったので、調べてみた時のメモ

この記事には以下の対応が書かれていました。

  • 対応2 sqlite3のin-memory databaseにしてみる
  • 対応3 southでテスト時にmigrateを使わず、syncdbでテーブル作成を行わせる

この記事の結果を見た時に、あれ?これってそんなに効果ないんだっけ?て気になったのでちょっと調べてみた次第。

前提&環境

  • Djangoは1.4系
  • South(DBマイグレーションツール)を利用
  • Migrationファイルは79ファイルある
  • テストランナーはカスタマイズして、Django本体&サードパーティライブラリのテストは動かないようにしている。
  • MySQL5.6.13を使用(homebrewでインスコ)..my.cnfとか特にチューニングしたりしてない。
  • SQLite3.7.12を使用(homebrewでインスコ)
  • テーブル数は70位
  • 使用マシン
    • OS: Mac OSX 10.8.5
    • CPU: 3.2GHz Core i3
    • メモリ: 4GB
    • HDD: 1TB

やりたい事

  • ローカルでテスト全体を走らせた時に速く終らせたい(コミット&プッシュ前とかに良くするので)
  • 主に利用するDBを変えてみる事でどれくらい実行時間が変わるか知りたい
  • Southのマイグレーションをテスト時にオフする事で実行時間が短縮されるか知りたい

1. MySQL(Migration有り)

  • まずは素のMySQLでテストを走らせる
  • manage.py testで出力されるテスト実行時間と体感時間にかなり差があるのでtimeコマンド付きで実行する。
 $ time python manage.py test --settings=hoge.settings.test

結果

# manage.py testが吐き出す結果
Ran 265 tests in 29.224s

# timeコマンドで計測した結果
real    3m10.899s
user    0m17.511s
sys     0m1.450s
  • テスト実行は29秒で終ってんのに、実際は3分もかかってやがる。
  • Djangoはテスト時にtest_で始まるテスト用のDB作成/破棄してる。多分その時間はmanage.py testで吐き出される時間にはインクルードされてない。

2. MySQL(Migration無し)

  • 次にSouthのマイグレーションをOFFにしてやってみる
  • 設定ファイル(settings/test.py)に以下の設定を書くだけ
SOUTH_TESTS_MIGRATE=False

結果

# manage.py test 
Ran 265 tests in 18.015s

# time
real    1m57.275s
user    0m10.954s
sys     0m1.049s
  • テストの実行自体も早くなったが、コマンドの実行時間自体も1分以上早くなった。体感的に全然ちがう。

3. MySQL on ramdisk (Migration有り)

 $ hdid -nomount ram://10670000
 $ newfs_hfs /dev/disk2 # -> 大体6GBのヤツできる
 $ mkdir /tmp/mnt
 $ mount -t hfs /dev/disk2 /tmp/mnt
 $ cp -pr /usr/local/var/mysql /tmp/mnt/

 $ mysql.server stop
 $ mysql.server start --datadir=/tmp/mnt/mysql

結果

# manage.py test 
Ran 265 tests in 17.435s

# time
real    0m34.779s
user    0m19.037s
sys     0m1.342s
  • やだー速いじゃないですかーやだーもー。
  • テスト実行自体は大して速くなってないけど、コマンドの実行時間は大分短くなってストレスなくなってきた。

4. MySQL on ramdisk (Migration無し)

  • さっきと同じようにMigrationをオフってみる

結果

# manage.py test 
Ran 265 tests in 16.330s

# time
real    0m28.271s
user    0m13.639s
sys     0m1.067s
  • 僅かではあるがMigration有りの時より速くなってる。

5. SQLite(Migration有り)

  • こんどは単純にSQLiteにしてやってみる
  • 設定ファイル(settings/test.py)でDATABASEの設定をSQLiteにするだけ
DATABASES = {
    'default': {
        'ENGINE': 'django.db.backends.sqlite3',
        'NAME': 'unittest.db',
    },
}

結果

# manage.py test 
Ran 265 tests in 12.166s

# time
real    0m22.653s
user    0m14.788s
sys     0m0.810s
  • MigrationがあってもMySQLより速くなった

6. SQLite(Migration無し)

  • 例によってMigrationをOFFる

結果

# manage.py test 
Ran 265 tests in 11.209s

# time
real    0m17.247s
user    0m14.788s
sys     0m0.810s
  • 今までと同様にMigration無い方が実行時間は短くなった

7. SQLite in-memory (Migration有り)

  • 最終形態。SQLiteをin-memoryで利用する
  • さっきのDATABASE設定のNAMEを":memory:"にするだけ
DATABASES = {
    'default': {
        'ENGINE': 'django.db.backends.sqlite3',
        'NAME': ':memory:',
    },
}

結果

# manage.py test 
Ran 265 tests in 10.511s

# time
real    0m16.613s
user    0m14.579s
sys     0m0.647s
  • SQLiteのMigration無しと大差ない結果だった。

8. SQLite in-memory (Migration無し)

  • 最後にMigration無しの試す

結果

# manage.py test 
Ran 265 tests in 10.208s

# time
real    0m11.963s
user    0m10.202s
sys     0m0.456s
  • テスト実行時間自体は大して変わらんけど、コマンド実行時間は10秒前後にまで縮まって、ほぼテスト実行時間とイコールになった。

まとめ

8パターンの計測を行った、最後にそれぞれのパターンを複数回(10回程度)実行して、その中央値を表にしてみた。

f:id:tell-k:20131010154753p:plain

  • mange.py test が出してるテスト実行時間には、テストDBの作成/破棄の時間は含まれない
  • 同様にMigrationしてる間の時間も含まれないのでMigrationをOFFにした方が実際の時間は短縮される
  • テーブル数、Migration数が多いほど、SQLite in-memoryでテスト実行時間が短くなってるのが実感できるのでオヌヌメ
  • ストレスフリーになってよかったな > 俺

余談

  • MySQLSQLiteに変更する事でテストが失敗するケースがあった。
    • Modelのテストで自動で日付を挿入するようなDatetime型のカラムをorder byしてselectしようとした。
    • その時テストデータ作成時に連続して createしたりすると、MySQLだと全く同じ時間になってしまって、結果の順序がSQLiteと違ってたりした。
    • テストで順序を保証したかったりしたら工夫する必要がある。
  • 実環境に近い所でテストがしたかったら、mysqld_multiとかでテスト用インスタンスramdiskに作るとかでもいいかもね。

おわり。