ユニットテストでコードを保護する

重要

このチュートリアルは、 :doc:の server_framework_101 チュートリアルの拡張です。 完了したことを確認し、このチュートリアルの演習のベースとして構築した`estate`モジュールを使用してください。

参照: Odooのテストフレームワーク: ベストプラクティス (Odoo Experience 2020) をYouTubeで学びましょう。

複数の理由でテストを書く必要があります。以下は非網羅的なリストです:

  • 将来コードが壊れないことを確認する

  • コードの範囲を明確にします。

  • ユースケースの例を示します。

  • コードを技術的に文書化する一つの方法です。

  • 目標を定義することで、コーディングを助けることができます。

テストの実行

テストの書き方を知る前に、テストの実行方法を知っておく必要があります。

$ odoo-bin -h
Usage: odoo-bin [options]

Options:
--version             show program's version number and exit
-h, --help            show this help message and exit

[...]

Testing Configuration:
  --test-file=TEST_FILE
                      Launch a python test file.
  --test-enable       Enable unit tests.
  --test-tags=TEST_TAGS
                      Comma-separated list of specs to filter which tests to
                      execute. Enable unit tests if set. A filter spec has
                      the format: [-][tag][/module][:class][.method] The '-'
                      specifies if we want to include or exclude tests
                      matching this spec. The tag will match tags added on a
                      class with a @tagged decorator (all Test classes have
                      'standard' and 'at_install' tags until explicitly
                      removed, see the decorator documentation). '*' will
                      match all tags. If tag is omitted on include mode, its
                      value is 'standard'. If tag is omitted on exclude
                      mode, its value is '*'. The module, class, and method
                      will respectively match the module name, test class
                      name and test method name. Example: --test-tags
                      :TestClass.test_func,/test_module,external  Filtering
                      and executing the tests happens twice: right after
                      each module installation/update and at the end of the
                      modules loading. At each stage tests are filtered by
                      --test-tags specs and additionally by dynamic specs
                      'at_install' and 'post_install' correspondingly.
  --screencasts=DIR   Screencasts will go in DIR/{db_name}/screencasts.
  --screenshots=DIR   Screenshots will go in DIR/{db_name}/screenshots.
                      Defaults to /tmp/odoo_tests.

$ # run all the tests of account, and modules installed by account
$ # the dependencies already installed are not tested
$ # this takes some time because you need to install the modules, but at_install
$ # and post_install are respected
$ odoo-bin -i account --test-enable
$ # run all the tests in this file
$ odoo-bin --test-file=addons/account/tests/test_account_move_entry.py
$ # test tags can help you filter quite easily
$ odoo-bin --test-tags=/account:TestAccountMove.test_custom_currency_on_account_1

Botの統合

注釈

このセクションは Odoo の従業員と github.com/odoo に貢献している人だけです。そうでなければ独自の CI を持つことを強くお勧めします。

テストが書き込まれるときは、変更がソースコードに適用されるときに常にパスすることが重要です。 このタスクを自動化するために、継続的インテグレーション(CI)という開発プラクティスを使用します。 これが、いくつかのボットが異なる瞬間にすべてのテストを実行している理由です。 Odooで作業しているかどうかに関わらず、odoo/odooodoo/enterpriseodoo/upgrade、またはodooで何かをマージしようとしている場合。 h、CIを通過する必要があります。別のプロジェクトに取り組んでいる場合は、独自のCIを追加することを考える必要があります。

Runbot

参照 : このトピックに関連するドキュメントは Runbot FAQ にあります。

ほとんどのテストは、GitHubにコミットがプッシュされるたびに`Runbot <https://runbot.odoo.com>`__ で実行されます。

ランボットダッシュボードでフィルタリングすることで、コミット/ブランチの状態を確認できます。

各ブランチごとに**バンドル**が作成されます。バンドルは構成とバッチで構成されています。

バッチ は、バンドルのパラメータに応じてビルドのセットです。 バッチは、すべてのビルドが緑色の場合(つまりテストに合格する)です。

build はサーバーを起動するときです。サブビルドに分けることができます。 通常、コミュニティバージョン用のビルドがあります。 エンタープライズバージョン(エンタープライズブランチがあるが、ビルドを強制できる場合のみ)とブランチの移行。 ビルドは、すべてのサブビルドが緑の場合は緑です。

サブビルド は、完全なビルドが行うことの一部のみを実行します。CI プロセスをスピードアップするために使用します。 一般的に、インストール後のテストを4つのパラレルインスタンスに分割するために使用されます。 サブビルドは、すべてのテストが合格し、エラーや警告が記録されていない場合は緑色になります。

注釈

  • すべてのテストは、行われた変更に関係なく実行されます。 エラーメッセージのタイプミスを修正したり、モジュール全体をリファクタリングすると、同じテストがトリガーされます。すべてのモジュールも同様にインストールされます。 これは、Runbot が緑色であっても動作しない可能性があることを意味します。 変更内容は変更内容に依存しないモジュールによって異なります。

  • ローカリゼーションモジュール (つまり、国固有のモジュール) は Runbot にはインストールされません (ジェネリックモジュールを除く)。 外部依存性を持つモジュールも除外できます。

  • モジュールの操作、ローカライズ、単一のモジュールのインストール、非決定的なバグのためのマルチビルドなど、追加テストを実行する毎晩ビルドがあります。 これらは、実行時間を短縮するために標準的なCIには保持されません。

Runbotによってビルドされたビルドにログインすることもできます。 利用可能なユーザは3人います: admin, demo, portal。パスワードはログインと同じです。 これは、ローカルでビルドすることなく、異なるバージョンで素早くテストするのに便利です。 完全なログも利用できます。これらは監視に使用されます。

Robodoo

robodooを召喚する権利を持つ前に、あなたはほとんどの場合、もう少し多くの経験を得る必要があります。 しかしここではいくつかの発言があります

Robodooは、あなたのPRにタグとしてCIのステータスをスパムしている男です。 でも彼は親切にあなたのコミットを メインリポジトリに統合した男でもあります

最後のバッチが緑色の場合、レビューアはあなたのPRをマージするようrobo-dooに依頼することができます(`merge`よりも`rebase`です)。 それはそれからmergebotに行きます。

Mergebot

Mergebot は、PR をマージする前の最後のテストフェーズです。

それはまだターゲットに存在しないあなたのブランチのコミットを取る、それをステージし、もう一度テストを再実行します。 コミュニティで何かを変えているだけでもエンタープライズ版を含めてね

このステップは ステージングに失敗しました エラーメッセージで失敗する可能性があります。

  • ターゲットにすでに確定的でないバグがあります。Odoo の従業員であれば、こちらから確認できます: https://runbot.odoo.com/runbot/errors

  • 導入されたがCIでは検出されなかった非決定論的なバグ。

  • 直前にマージされた別のコミットとマージしようとしているものとの互換性がありません

  • コミュニティリポジトリに変更があった場合のみエンタープライズリポジトリとの互換性がありません

マージボットに再試行を求める前に問題が発生していないことを常に確認してください。ターゲットにブランチをリベースし、テストをローカルに再試行します。

モジュール

Odooはモジュール式なので、テストもモジュール式である必要があります。 これは、追加する機能を追加するテストをモジュール内で定義することを意味します。 そして、モジュールが依存しないモジュールの機能にテストは依存できません。

参照: このトピックに関連するドキュメントは、 Special Tags にあります。

from odoo.tests.common import TransactionCase
from odoo.tests import tagged

# The CI will run these tests after all the modules are installed,
# not right after installing the one defining it.
@tagged('post_install', '-at_install')  # add `post_install` and remove `at_install`
class PostInstallTestCase(TransactionCase):
    def test_01(self):
        ...

@tagged('at_install')  # this is the default
class AtInstallTestCase(TransactionCase):
    def test_01(self):
        ...

テストしたい動作は、別のモジュールのインストールによって変更できます。 at_install タグが設定されていることを確認してください。 それ以外の場合は、post_install タグを使ってCIをスピードアップし、そうでない場合は変更されないようにします。

テストの書き方

リファレンス: Python unittest と :ref:`Testing Odoo<reference/testing> ` にこのトピックに関連するドキュメントがあります。

テストを書く前に考慮すべき事項は次のとおりです。

  • テストは、現在データベースにあるデータ(デモデータを含む)とは独立して行う必要があります。

  • テストは残留データを残す/変更することによってデータベースに影響を与えるべきではありません。これは通常、ロールバックを行うことによってテストフレームワークによって行われます。 したがって、テスト内で cr.commit を呼び出さないでください。

  • バグ修正の場合、修正を適用する前にテストが失敗し、その後に合格するようにします。

  • すでに他でテストされているものをテストしてはいけません。ORMを信頼してください。ビジネス・モジュールのテストのほとんどは、ビジネス・フローをテストするだけです。

  • データベースにデータを書き込む必要はありません。

注釈

onchange はフォームビューでのみ適用され、pythonで属性を変更しても適用されないことを覚えておいてください。これはテストでも同じです。フォームビューをエミュレートしたい場合は、 odoo.tests.common.Form を使うことができます。

The tests should be in a tests folder at the root of your module. Each test file name should start with test_ and be imported in the __init__.py of the test folder. You shouldn't import the test folder/module in the __init__.py of the module.

estate
├── models
│   ├── *.py
│   └── __init__.py
├── tests
│   ├── test_*.py
│   └── __init__.py
├── __init__.py
└── __manifest__.py

すべてのテストは odoo.tests.common.TransactionCase を拡張する必要があります。通常は setUpClass とテストを定義します。 `setUpClass`を書いた後、クラスに`env`があり、ORMとやり取りを始めることができます。

これらのテストクラスは unittest という python モジュールの上に構築されています。

from odoo.tests.common import TransactionCase
from odoo.exceptions import UserError
from odoo.tests import tagged

# The CI will run these tests after all the modules are installed,
# not right after installing the one defining it.
@tagged('post_install', '-at_install')
class EstateTestCase(TransactionCase):

    @classmethod
    def setUpClass(cls):
        # add env on cls and many other things
        super(EstateTestCase, cls).setUpClass()

        # create the data for each tests. By doing it in the setUpClass instead
        # of in a setUp or in each test case, we reduce the testing time and
        # the duplication of code.
        cls.properties = cls.env['estate.property'].create([...])

    def test_creation_area(self):
        """Test that the total_area is computed like it should."""
        self.properties.living_area = 20
        self.assertRecordValues(self.properties, [
           {'name': ..., 'total_area': ...},
           {'name': ..., 'total_area': ...},
        ])


    def test_action_sell(self):
        """Test that everything behaves like it should when selling a property."""
        self.properties.action_sold()
        self.assertRecordValues(self.properties, [
           {'name': ..., 'state': ...},
           {'name': ..., 'state': ...},
        ])

        with self.assertRaises(UserError):
            self.properties.forbidden_action_on_sold_property()

注釈

読みやすさを向上させるために、テストの範囲に応じてテストを複数のファイルに分割します。 また、テストのほとんどが継承すべき共通クラスを持つこともできます。 この共通のクラスはモジュールのセットアップ全体を定義できます 例えば、 account です。

Exercise

誰にもできないようにコードを更新してください:

  • 売却された不動産のオファーを作成する

  • 承認されていないオファーを持つプロパティを販売

両方の症例のテストを作成します さらに、売却可能なプロパティを売却することは、売却後に正しく売却としてマークされていることを確認してください。

Exercise

Gardenのチェックボックスをオフにすると、誰かが庭の面積と方角のリセットを破ってしまいます。このようなことが起こらないようにしてください。

ちなみに

ヒント: 少し前の Form についての注意を思い出してください。