Unmanagedモデルを使用しながらテストを適用する方法 (feat. Table XXXが存在しない)

以前、会社でRaw Queryで書かれていたレガシーコードをDjango ORMベースに改編しようとしているとお話ししたことがありましたよね?

以前に書いておいた悩みがある程度解決され、プロジェクト構造やファイルの整理方法も整ったので、実際にモデルを分類しながら起こった出来事を投稿しようと思います。

既存のSQLクエリを分析しながら、各テーブルがどのテーブルと結合されているか、影響度がどの程度かを確認しながらモデル分類まではうまくいっていたと思います。そしてそれがうまく動作するかテストをしようとテストコードを作成しているときに問題が発生しました。開発者にとっては~~嬉しい?~~エラーが発生したのです。

Table my_database.XXX doesn't exist.

なんでテーブルがないの?


Unmanagedモデルとは?

Djangoでは、DBと接続するModelを定義する際にdjango.db.models.Modelモジュールを使用します。DjangoではModelを基にしてmigrationをサポートしているため、もしDjango Native Appで設計した場合、DjangoDB Tableにアクセスして直接管理できるように設定することができます。 migrationについては次回に詳細に投稿しますね!

このような場合、Djangoで定義したModelはデータベースに直接接続し設計図になります。

Djangoで管理されるDB SchemaはModelクラスでデフォルト設定されるのがmanaged = Trueです。実際のコードでは次のように適用されるでしょう。

class MyModel(models.Model):
  ...
  # DBフィールドを定義します
  ...
  class Meta:
    db_table = "real_database_table_name"
    manage = True # ここがポイントです。 

Model内部MetaクラスはDjango Native Appを通じてモデルを定義する際にはわざわざ表記しなくてもDjango Modelでデフォルトで設定されているので、別途設定しなくても大丈夫です。

この時、Metaクラス内部で設定されているmanaged変数がFalseになっている場合、これをUnmanagedモデル、すなわちDjangoで管理しないモデルと呼びます。

Table xxx doesn’t exist

ついに私が直面したエラーについて説明できる時が来ました。 Django Unit Testの場合、テストをするために既存のデータベースを使用しません。

むしろ使用してはいけません。

開発データベースであっても、私のプロジェクトが他のプロジェクトに影響を及ぼす可能性があるか、テストの繰り返しでデータベースが散らかってしまう可能性があります(多くの重複したテストケースによる…汚い

その問題を解決するために、Django Testではmanaged = Trueに定義されているモデルスキーマを基にTest DatabaseをDBに作成し、一時テーブルを作成してその中でテストを実行します。そしてテストが終わると、テストデータベースと共にテーブルを削除します。

この時、app/migrationに定義されているテーブルスキーマが重要です。

実際にはモデルをそのまま作成したとしても、モデルを実際にDBに適用するためには、モデルの変更履歴を通じてDBに適用するために中間段階を経る必要があるので、Djangoでは各アプリ内のmigrationsを通じて変更履歴を追跡し、判断します。

+ Djangoを学ぶ際にpython manage.py makemigrationspython manage.py migrateというコマンドをよく使いますが、その時生成されるコードです!

しかしUnmanagedモデルの場合、migrationを利用してはいけません。他のサービスでも使用される可能性があるDBをDjangoが追跡させてはいけません。そしてテーブル修正の主体はDBにあると考え、Djangoにはないと考えます。したがってmanaged = Falseであるべきです。

DBからDjangoモデルにコードを生成するためにpython manage.py inspectdbコマンドをよく使用しますが、この時生成されるモデルのコードを見るとmanaged = Falseであることがわかります。

しかしTestは状況がだいぶ違いますね?一時的なDBであっても生成されても問題ないのですが、managed = Falseの影響を受けて、Test DBを生成できず、結局テーブルがないというエラーが発生するのです。

Solution, どうすればいいでしょうか?

私の場合は単純ですが、少し強引に解決しました。(結論として成功しましたが、変更する予定であり、お勧めしない方法です。)

いくつかのブログでは、Testを実行するDefault Test Runnerを変更して、テストが実行される前にmodelMeta ClassにアクセスしてmanagedTrueに変更する方法を紹介しています。

私が使用したのも、下の例も、原理的には上記の方法を使用している点では同じです。

Soluton #1: コマンドをフックする(私が使用した方法)

私はコマンドを通じてテストを進行する際、そのコマンドをフックしてコマンド内にtestが含まれている場合、managed = Falseに変更するように設定しました。実際に変更するコードは2行だけで本当に簡単で、よく動作するコードでした。

まずPythonコマンド内にtestが含まれているかを判定するコードを設定ファイルに定義します。

参考にした文献はこちら: https://stackoverflow.com/questions/53289057/how-to-run-django-test-when-managed-false

# settings.py
...
UNDER_TEST = (len(sys.argv) > 1 and sys.argv[1] == 'test')
...

そしてテストモデルクラスのメタクラスに以下のようにmanaged = getattr(settings, 'UNDER_TEST', False)を追加してください。

# models.py
from django.conf import settings # 私が定義したsettings.pyの値を取得できるようにするモジュールです。


class MyModel(models.Model)
  ...
  # 私が定義したフィールド
  ...
  class Meta(object):
      db_table = 'your_db_table'
      managed = getattr(settings, 'UNDER_TEST', False)

Solution #2: カスタムテストランナーを使用する

上記の事例では簡単にテストを行うことができるという利点がありますが、テスト対象であるすべてのモデルのmanaged変数を同じ値に変更しなければならないという短所があります。

もしそうやって進め続ければテーブルが多ければ多くなるほど漏れが生じることがあり、協力しているチームメンバーがテストコードを書くときにこれを別に認識しなければならない短所が大きいでしょう。(多くのルールは開発者を苦しめます)

先ほど申し上げたようにTest Runnerを別途定義して、実行時にそのTest Runnerを使用してテストコードを実行することがDjangoの基本的なプログラミングルールを維持しながら、チームメンバー間で一貫したコードを特定の伝達なしに進めることができるという利点があります。

原理はTest Runnermanaged = Falseのクラスを認識してテスト実行時にすべてのクラスをmanaged = Trueに変更してTestを実行するクラスを定義することです。

ただし、この事例の場合、まだ成功していないコードなので、リンクだけを残しておきます。

おそらく最後に成功した上記の事例でmigrationフォルダを後になって削除したところ、残っていたmigrationフォルダが空だったことが元の原因だったようです。(完全に削除するべきでした)。今後会社で適用してみて可能であれば変更して更新します!

Django 1.9 以上

Django 1.8 以下

結びに

普段よりもきちんとしていなかったですが、一つずつ変えて適用していく中で自信がついてきた気がします。実際の投稿はそれほど長くないですが、この文章を書くために会社ではかなりの時間をかけました… ㅠㅠ

今日の投稿はここまでにし、次回は今日バグを見つける中で学んだDjango Test Runnerや、migrationについてもう少し詳しく投稿したいと思います! 今日も読んでいただきありがとうございます。ご質問やご指摘はいつでも歓迎します。コメントをご利用ください!