38

私はテストが初めてではありませんが、Django でさまざまなレイヤーをテストするための推奨事項の混乱に本当に混乱しました。

Doctestsは保守できないため、モデル内でDoctestsを避けることを推奨する人もいます (そして彼らは正しいです) 。

たとえば、フィクスチャはヘルパー関数よりも柔軟性が低いため、フィクスチャを使用しないと言う人もいます..

また、モックオブジェクトの使用をめぐって争っているグループが 2 つあります。最初のグループは、モックを使用してシステムの残りを分離することを信じていますが、別のグループは、モックを 停止してテストを開始することを好みます..

上で述べたのは、主にモデルのテストに関するものでした。機能テストは別の話です (test.Client() VS webTest VS などを使用)

さまざまなレイヤーをテストするための、保守可能で拡張可能な適切な方法はあります か??

アップデート

PyCon 2012 でのCarl Meyer の講演を知っています。

4

2 に答える 2

45

更新2012年8月7日

私自身の目的のためにかなりうまく機能しているユニットテストの私の実践をあなたに話すことができます、そして私はあなたに私の理由を与えます:

1.-フィクスチャは、テストに必要であるが変更されない情報にのみ使用します。たとえば、テストごとにユーザーが必要になるため、ベースフィクスチャを使用してユーザーを作成します。

2.-ファクトリを使用してオブジェクトを作成します。私は個人的にFactoryBoyが大好きです。(これは、ルビーライブラリであるFactoryGirlからのものです)。これらすべてのオブジェクトを保存するアプリごとに、factories.pyという個別のファイルを作成します。このようにして、必要なすべてのオブジェクトをテストファイルから除外し、読みやすく、保守しやすくします。このアプローチの優れた点は、ファクトリからのオブジェクトに基づいて他の何かをテストする場合に変更できるベースオブジェクトを作成することです。また、djangoに依存しないため、mongodbを使い始めてテストする必要があるときにこれらのオブジェクトを移行したとき、すべてがスムーズでした。工場について読んだ後、「なぜ私はなぜ備品を使いたいのか」と言うのが一般的です。これらのフィクスチャは決して変更されるべきではないので、工場からの余分なグッズはすべて役に立たず、djangoはフィクスチャを箱から出して非常にうまくサポートします。

3.-外部サービスへの呼び出しをモックします。これらの呼び出しはテストを非常に遅くし、コードが正しいか間違っているかとは何の関係もないことに依存しているためです。たとえば、テスト内でツイートする場合は、正しくツイートするようにテストし、応答をコピーしてそのオブジェクトをモックし、実際の呼び出しを行わずに毎回正確な応答を返すようにします。また、問題が発生したときにテストするのが良い場合もあり、モックはそのために最適です。

4.-ステージングサーバーにプッシュするたびにテストを実行する統合サーバー(ここではjenkinsが推奨)を使用し、失敗した場合は電子メールを送信します。前回の変更で何か他のものを壊してしまい、テストを実行するのを忘れてしまったことがよくあるので、これは素晴らしいことです。また、カバレッジレポート、pylint / jslint / pep8検証などの他の機能も提供し、さまざまな統計を設定できるプラグインが多数存在します。

フロントエンドをテストするための質問について、djangoには、これを基本的な方法で処理するためのヘルパー関数がいくつか付属しています。

これは私が個人的に使用しているもので、getを起動したり、投稿したり、ユーザーにログインしたりすることができます。これで十分です。セレンのような完全なフロントエンドテストエンジンを使用する傾向はありません。ビジネスレイヤー以外のものをテストするのはやり過ぎだと感じているからです。いくつかは異なると確信しており、それは常にあなたが取り組んでいることに依存します。

私の意見に加えて、django 1.4には、ブラウザー内フレームワーク用の非常に便利な統合が付属しています。

より理解しやすいように、このプラクティスを適用できるサンプルアプリを設定します。非常に基本的なブログアプリを作成しましょう。

構造

blogger/
    __init__.py
    models.py
    fixtures/base.json
    factories.py
    tests.py

models.py

 from django.db import models

 class Blog(models.Model):
     user = models.ForeignKey(User)
     text = models.TextField()
     created_on = models.DateTimeField(default=datetime.now())

フィクスチャ/base.json

[
{
    "pk": 1,
    "model": "auth.user",
    "fields": {
        "username": "fragilistic_test",
        "first_name": "demo",
        "last_name": "user",
        "is_active": true,
        "is_superuser": true,
        "is_staff": true,
        "last_login": "2011-08-16 15:59:56",
        "groups": [],
        "user_permissions": [],
        "password": "IAmCrypted!",
        "email": "test@email.com",
        "date_joined": "1923-08-16 13:26:03"
    }
}
]

factorys.py

import factory
from blog.models import User, Blog

class BlogFactory(factory.Factory):
    FACTORY_FOR = Blog

    user__id = 1
    text = "My test text blog of fun"

tests.py

class BlogTest(TestCase):
    fixtures = ['base']  # loads fixture

    def setUp(self):
        self.blog = BlogFactory()
        self.blog2 = BlogFactory(text="Another test based on the last one")

    def test_blog_text(self):
        self.assertEqual(Blog.objects.filter(user__id=1).count(), 2)

    def test_post_blog(self):
        # Lets suppose we did some views
        self.client.login(username='user', password='IAmCrypted!')
        response = self.client.post('/blogs', {'text': "test text", user='1'})

        self.assertEqual(response.status, 200)
        self.assertEqual(Blog.objects.filter(text='test text').count(), 1)

    def test_mocker(self):
        # We will mock the datetime so the blog post was created on the date
        # we want it to
        mocker = Mock()
        co = mocker.replace('datetime.datetime')
        co.now()
        mocker.result(datetime.datetime(2012, 6, 12))

        with mocker:
            res = Blog.objects.create(user__id=1, text='test')

        self.assertEqual(res.created_on, datetime.datetime(2012, 6, 12))

    def tearDown(self):
        # Django takes care of this but to be strict I'll add it
        Blog.objects.all().delete()

例のために特定のテクノロジーを使用していることに注意してください(これはまだテストされていません)。

私は主張しなければなりません、これは標準的なベストプラクティスではないかもしれませんが(私はそれが存在するとは思えません)、それは私にとってはかなりうまく機能しています。

于 2012-07-18T16:37:05.597 に答える
20

私は@Hassekからの提案が本当に好きで、私たち全員がさまざまな懸念を持ってフレームワークにアプローチしているため、テストだけでなく、Djangoの多くの側面に当てはまる標準的な慣行の明らかな欠如について彼が述べている優れた点を強調したいと思います.また、アプリケーションの設計に関して非常に高い柔軟性を備えているため、同じ問題に適用できる大幅に異なるソリューションが得られることがよくあります。

とはいえ、私たちのほとんどは、アプリケーションをテストするときに、主に次のような多くの同じ目標に向かって努力しています。

  • テスト モジュールをきちんと整理する
  • 再利用可能なアサーション メソッドとヘルパー メソッド、テスト メソッドの LOC を削減するヘルパー関数を作成して、よりコンパクトで読みやすいものにする
  • アプリケーション コンポーネントのテスト方法に対する明確で体系的なアプローチがあることを示す

@Hassekのように、これらはあなたが適用している可能性のある慣行と直接矛盾する可能性のある私の好みですが、私たちの場合に限り、機能することを証明したことを共有するのはいいことだと思います.

テスト ケース フィクスチャなし

アプリケーション フィクスチャは、データベースに存在することを保証したい特定の一定のモデル データがある場合、たとえば町の名前と郵便局番号のコレクションなど、うまく機能します。

ただし、これはテスト ケース データを提供するための柔軟性のないソリューションだと思います。テスト フィクスチャは非常に冗長です。モデルのミューテーションにより、フィクスチャ データを再現するための長いプロセスを経るか、退屈な手動変更を実行する必要があり、参照整合性を手動で維持することは困難です。

さらに、モデルだけでなく、多くの種類のフィクスチャをテストで使用する可能性が高くなります。API リクエストからの応答本文を保存したり、NoSQL データベース バックエンドをターゲットとするフィクスチャを作成したり、使用されるフィクスチャを記述したりします。フォームデータの入力など

最終的に、API を使用してデータを作成することは簡潔で読みやすく、関係を見つけるのがはるかに簡単になるため、私たちのほとんどは、動的にフィクスチャを作成するためにファクトリを使用することに頼っています。

工場を広く活用する

ファクトリ関数とメソッドは、テスト データを踏みにじるよりも望ましいものです。ヘルパー ファクトリ モジュール レベル関数またはテスト ケース メソッドを作成して、アプリケーション テスト間またはプロジェクト全体で再利用することができます。特に、factory_boy@Hassek が言及している は、フィクスチャ データを継承/拡張し、自動シーケンスを実行する機能を提供します。

ファクトリを利用する最終的な目標は、コードの重複を減らし、テスト データの作成方法を合理化することです。正確な指標を提供することはできませんが、テスト メソッドを目の肥えた目で見れば、テスト コードの大部分が主に、テストを実行するために必要なデータを準備していることに気付くでしょう。

これが正しく行われないと、テストの読み取りと保守が大変な作業になります。これは、データの変更が全面的にそれほど明白ではないテストの失敗につながる場合にエスカレートする傾向があり、その時点で体系的なリファクタリング作業を適用できなくなります。

この問題に対する私の個人的なアプローチは、モデルのメソッドmyproject.factoryへのアクセスしやすい参照を作成するモジュールとQuerySet.create、ほとんどのアプリケーション テストで定期的に使用する可能性のあるオブジェクトへの参照を作成することから始めることです。

from django.contrib.auth.models import User, AnonymousUser
from django.test import RequestFactory

from myproject.cars.models import Manufacturer, Car
from myproject.stores.models import Store


create_user = User.objects.create_user
    create_manufacturer = Manufacturer.objects.create
create_car = Car.objects.create
create_store = Store.objects.create

_factory = RequestFactory()


def get(path='/', data={}, user=AnonymousUser(), **extra):
    request = _factory.get(path, data, **extra)
    request.user = user

    return request


def post(path='/', data={}, user=AnonymousUser(), **extra):
    request = _factory.post(path, data, **extra)
    request.user = user

    return request

これにより、次のようなことができます。

from myproject import factory as f  # Terse alias

# A verbose, albeit readable approach to creating instances
manufacturer = f.create_manufacturer(name='Foomobiles')
car1 = f.create_car(manufacturer=manufacturer, name='Foo')
car2 = f.create_car(manufacturer=manufacturer, name='Bar')

# Reduce the crud for creating some common objects
manufacturer = f.create_manufacturer(name='Foomobiles')
data = {name: 'Foo', manufacturer: manufacturer.id)
request = f.post(data=data)
view = CarCreateView()

response = view.post(request)

ほとんどの人はコードの重複を減らすことに厳格ですが、実際には、テストの包括性に貢献していると感じたときに、意図的に導入しています。繰り返しになりますが、工場に対してどのようなアプローチを取る場合でも、目標は、各テスト メソッドのヘッダーに導入するブレインファックの量を最小限に抑えることです。

モックを使用しますが、賢く使用してください

私は のファンですmock。なぜなら、彼が対処したかった問題であると私が信じるものに対する著者の解決策に感謝するようになったからです。パッケージが提供するツールを使用すると、期待される結果を注入してテスト アサーションを作成できます。

# Creating mocks to simplify tests
factory = RequestFactory()
request = factory.get()
request.user = Mock(is_authenticated=lamda: True)  # A mock of an authenticated user
view = DispatchForAuthenticatedOnlyView().as_view()

response = view(request)


# Patching objects to return expected data
@patch.object(CurrencyApi, 'get_currency_list', return_value="{'foo': 1.00, 'bar': 15.00}")
def test_converts_between_two_currencies(self, currency_list_mock):
    converter = Converter()  # Uses CurrencyApi under the hood

    result = converter.convert(from='bar', to='foo', ammount=45)
    self.assertEqual(4, result)

ご覧のとおり、モックは非常に便利ですが、厄介な副作用があります。モックは、アプリケーションがどのように動作するかについての仮定を明確に示し、カップリングを導入します。Converter以外のものを使用するようにリファクタリングされた場合CurrencyApi、誰かがテスト メソッドが突然失敗する理由を明らかに理解できない可能性があります。

したがって、大きな力には大きな責任が伴います。賢く、モックを使用して根深いテストの障害を回避する場合、テストの失敗の本質を完全に難読化する可能性があります。

何よりも、一貫性を保つこと。非常に一貫性がある

これは最も重要なポイントです。絶対にすべてに一貫性を持たせてください:

  • 各テスト モジュールでコードを整理する方法
  • アプリケーション コンポーネントのテスト ケースを導入する方法
  • それらのコンポーネントの動作をアサートするためのテスト メソッドをどのように導入するか
  • テストメソッドをどのように構築するか
  • 共通コンポーネント (クラスベースのビュー、モデル、フォームなど) をテストする方法
  • 再利用の適用方法

ほとんどのプロジェクトでは、協力してテストにどのようにアプローチするかということは見過ごされがちです。アプリケーション コード自体は完璧に見えますが (スタイル ガイドの遵守、Python のイディオムの使用、関連する問題を解決するための Django 独自のアプローチの再適用、フレームワーク コンポーネントの教科書の使用など)。テスト コードを有効で便利なコミュニケーション ツールに変えることができますが、テスト コードの明確なガイドラインだけで十分だとしたら、それはもったいないことです。

于 2012-08-07T18:31:41.410 に答える