データベースクエリでオブジェクトを単体テストする方法


153

ユニットテストは「完全に素晴らしい」、「本当にクール」、「あらゆる種類の良いこと」であると聞きましたが、私のファイルの70%以上にデータベースアクセス(一部は読み取り、一部は書き込み)が含まれており、その方法はわかりませんこれらのファイルの単体テストを作成します。

PHPとPythonを使用していますが、データベースアクセスを使用するほとんどすべての言語に当てはまる質問だと思います。

回答:


82

データベースへの呼び出しをモックアウトすることをお勧めします。モックは基本的に、呼び出し側が利用できる同じプロパティ、メソッドなどを持っているという意味で、メソッドを呼び出そうとしているオブジェクトのように見えるオブジェクトです。しかし、特定のメソッドが呼び出されたときにプログラムが行うようにプログラムされているアクションを実行する代わりに、それを完全にスキップして、結果を返します。その結果は通常、事前に定義されます。

オブジェクトをモック用に設定するには、次の疑似コードのように、制御/依存性注入パターンのある種の反転を使用する必要があります。

class Bar
{
    private FooDataProvider _dataProvider;

    public instantiate(FooDataProvider dataProvider) {
        _dataProvider = dataProvider;
    }

    public getAllFoos() {
        // instead of calling Foo.GetAll() here, we are introducing an extra layer of abstraction
        return _dataProvider.GetAllFoos();
    }
}

class FooDataProvider
{
    public Foo[] GetAllFoos() {
        return Foo.GetAll();
    }
}

次に、単体テストでFooDataProviderのモックを作成します。これにより、実際にデータベースにアクセスすることなく、GetAllFoosメソッドを呼び出すことができます。

class BarTests
{
    public TestGetAllFoos() {
        // here we set up our mock FooDataProvider
        mockRepository = MockingFramework.new()
        mockFooDataProvider = mockRepository.CreateMockOfType(FooDataProvider);

        // create a new array of Foo objects
        testFooArray = new Foo[] {Foo.new(), Foo.new(), Foo.new()}

        // the next statement will cause testFooArray to be returned every time we call FooDAtaProvider.GetAllFoos,
        // instead of calling to the database and returning whatever is in there
        // ExpectCallTo and Returns are methods provided by our imaginary mocking framework
        ExpectCallTo(mockFooDataProvider.GetAllFoos).Returns(testFooArray)

        // now begins our actual unit test
        testBar = new Bar(mockFooDataProvider)
        baz = testBar.GetAllFoos()

        // baz should now equal the testFooArray object we created earlier
        Assert.AreEqual(3, baz.length)
    }
}

一言で言えば、一般的なモックのシナリオです。もちろん、実際のデータベース呼び出しも単体テストする必要があるでしょう。そのため、データベースにアクセスする必要があります。


私はこれが古いことを知っていますが、すでにDBにあるテーブルに複製テーブルを作成するのはどうですか。その方法で、DB呼び出しが機能していることを確認できますか?
速報値は

1
私はPHPのPDOをデータベースへの最低レベルのアクセスとして使用しており、それを介してインターフェースを抽出しました。次に、その上にアプリケーション対応のデータベース層を構築しました。これは、すべての生のSQLクエリとその他の情報を保持するレイヤーです。アプリケーションの残りの部分は、このより高レベルのデータベースと対話します。これは単体テストでかなりうまくいくことがわかりました。アプリケーションページとアプリケーションデータベースとの相互作用をテストします。アプリケーションデータベースをPDOとどのように相互作用するかをテストします。PDOはバグなしで動作すると思います。ソースコード:manx.codeplex.com
合法化

1
@bretterer-重複したテーブルを作成することは、統合テストに適しています。単体テストでは通常、データベースに関係なくコードのユニットをテストできるモックオブジェクトを使用します。
BornToCode 2015年

2
単体テストでデータベース呼び出しをモックアウトすることの価値は何ですか?実装を変更して別の結果を返す可能性があるため、これは役に立たないようですが、単体テストは(誤って)成功します。
bmay2 2017

2
@ bmay2あなたは間違っていません。私の元の答えは、多くの人がテスト可能な方法でコードを記述しておらず、テストツールが大幅に不足していた昔(9年!)に書かれました。このアプローチはもうお勧めしません。今日は、テストデータベースをセットアップして、テストに必要なデータを入力するか、データベースをまったく使用せずにできるだけ多くのロジックをテストできるようにコードを設計します。
Doug R

25

理想的には、オブジェクトは永続的に無知である必要があります。たとえば、リクエストを行う「データアクセスレイヤー」があり、オブジェクトを返す必要があります。このようにして、その部分を単体テストから除外することも、単独でテストすることもできます。

オブジェクトがデータレイヤーに密結合されている場合、適切な単体テストを行うことは困難です。ユニットテストの最初の部分は「ユニット」です。すべてのユニットを個別にテストできる必要があります。

私のc#プロジェクトでは、NHibernateと完全に別のデータレイヤーを使用しています。私のオブジェクトはコアドメインモデルにあり、アプリケーション層からアクセスされます。アプリケーション層は、データ層とドメインモデル層の両方と通信します。

アプリケーション層は、「ビジネス層」とも呼ばれます。

あなたがPHPを使用している場合、のクラスの特定のセットを作成ONLYデータアクセス。オブジェクトがどのように永続化されているかをオブジェクトが把握していないことを確認し、アプリケーションクラスで2つを関連付けます。

別のオプションは、モック/スタブを使用することです。


私はこれに常に同意しましたが、実際には締め切りのため、「今日は午後2時までに機能を1つだけ追加します」ので、これは達成するのが最も難しい作業の1つです。この種のものはリファクタリングの主なターゲットですが、上司がまったく新しいビジネスロジックとテーブルを必要とする50の新しい緊急の問題を考えていなかったと決心した場合。
ダレンリンガー2015

3
オブジェクトがデータレイヤーに密結合されている場合、適切な単体テストを行うことは困難です。ユニットテストの最初の部分は「ユニット」です。すべてのユニットを個別にテストできる必要があります。素敵な、について説明
阿弥陀

11

データベースアクセスでオブジェクトを単体テストする最も簡単な方法は、トランザクションスコープを使用することです。

例えば:

    [Test]
    [ExpectedException(typeof(NotFoundException))]
    public void DeleteAttendee() {

        using(TransactionScope scope = new TransactionScope()) {
            Attendee anAttendee = Attendee.Get(3);
            anAttendee.Delete();
            anAttendee.Save();

            //Try reloading. Instance should have been deleted.
            Attendee deletedAttendee = Attendee.Get(3);
        }
    }

これにより、データベースの状態が元に戻ります。基本的にはトランザクションのロールバックのように、副作用なしに何度でもテストを実行できます。このアプローチは、大規模なプロジェクトで成功裏に使用されています。ビルドの実行には少し時間がかかります(15分)が、1800の単体テストを行うのは恐ろしいことではありません。また、ビルド時間が問題になる場合は、ビルドプロセスを変更して複数のビルドを作成できます。1つはsrcのビルド用で、もう1つはユニットテスト、コード分析、パッケージ化などを処理するために後で起動します。


1
+1データアクセス層を単体テストするときに時間を大幅に節約します。TSはしばしばMSDTCを必要とすることに注意してください(アプリがMSDTCを必要とするかどうかによって異なります)
StuartLC

元の質問はPHPに関するものでしたが、この例はC#のようです。環境は大きく異なります。
2012年

2
質問の著者は、それはDBと関係があるすべての言語に適用される一般的な質問であると述べました。
Vedran 2013

9
この親愛なる友人は、統合テストと呼ばれます
AA。

10

大量の "ビジネスロジック" SQL操作を含む中間層プロセスの単体テストを検討し始めたとき、おそらく私たちの経験を味わうことができます。

最初に、適切なデータベース接続を「組み込む」ことができる抽象化レイヤーを作成しました(この場合は、単一のODBCタイプの接続をサポートしただけです)。

これが整うと、コードで次のようなことができるようになります(C ++で作業しますが、きっとあなたはそのアイデアを理解できます)。

GetDatabase()。ExecuteSQL( "INSERT INTO foo(blah、blah)")

通常の実行時に、GetDatabase()は、ODBCを介してデータベースに直接、すべてのSQL(クエリを含む)を供給したオブジェクトを返します。

その後、インメモリデータベースの調査を開始しました。長い目で見れば、SQLiteが最良のようです。(http://www.sqlite.org/index.html)。設定と使用は非常に簡単で、GetDatabase()をサブクラス化してオーバーライドすることで、SQLをメモリ内のデータベースに転送し、実行されたすべてのテストで作成および破棄できます。

これはまだ初期段階ですが、これまでのところ順調です。ただし、必要なテーブルを作成し、テストデータを入力する必要があります。ただし、ここでは、ヘルパー関数の一般的なセットであり、これらすべてを私たちのために行うことができます。

全体的に、それは私たちのTDDプロセスに非常に役立ちました。なぜなら、特定のバグを修正するためにまったく無害な変更のように見えるものを行うと、システムの他の(検出が困難な)領域に非常に奇妙な影響を与える可能性があるためです-sql /データベースの性質そのものが原因です。

明らかに、私たちの経験はC ++開発環境を中心に扱っていますが、PHP / Pythonでも同様の機能が得られると思います。

お役に立てれば。


9

クラスを単体テストする場合は、データベースアクセスをモックする必要があります。結局のところ、単体テストでデータベースをテストする必要はありません。それは統合テストになります。

呼び出しを抽象化し、期待されるデータを返すだけのモックを挿入します。クラスがクエリの実行以上のことを行わない場合、それらをテストする価値はないかもしれませんが...


6

xUnit Test Patternsは、データベースにヒットするユニットテストコードを処理するいくつかの方法を説明しています。IMO遅いのでやりたくないと言っている他の人たちにも同意します。より高いレベルのものをテストするためにdb接続をモックアウトすることは良い考えですが、実際のデータベースを操作するためにできることについての提案については、この本をチェックしてください。


4

あなたが持っているオプション:

  • 単体テストを開始する前にデータベースを一掃するスクリプトを記述し、事前定義されたデータセットをdbに入力してテストを実行します。すべてのテストの前にそれを行うこともできます–遅くなりますが、エラーが起こりにくくなります。
  • データベースを注入します。(疑似Javaの例ですが、すべてのオブジェクト指向言語に適用されます)

    クラスデータベース{
     public Result query(String query){...ここに実際のデータベース...}
    }

    MockDatabaseクラスはDatabase { public Result query(String query){ 「模擬結果」を返す; } }

    クラスObjectThatUsesDB { public ObjectThatUsesDB(Database db){ this.database = db; } }

    現在、本番環境では通常のデータベースを使用し、すべてのテストでアドホックに作成できるモックデータベースを挿入するだけです。

  • ほとんどすべてのコードでDBを使用しないでください(とにかくそれは悪い習慣です)。結果を返すのではなく通常のオブジェクトを返す(つまりUser、タプルの代わりに返す{name: "marcin", password: "blah"})「データベース」オブジェクトを作成します。アドホックに構築された実際のオブジェクトですべてのテストを記述し、この変換を確実にするデータベースに依存する1つの大きなテストを記述します。正常に動作します。

もちろん、これらのアプローチは相互に排他的ではなく、必要に応じてそれらを組み合わせて組み合わせることができます。


3

プロジェクトが全体的に高い凝集度と疎結合を持っている場合、データベースアクセスの単体テストは十分に簡単です。このようにして、一度にすべてをテストしなくても、特定の各クラスが行うことだけをテストできます。

たとえば、ユーザーインターフェイスクラスをユニットテストする場合、作成するテストでは、UI内のロジックが期待どおりに機能することを確認するだけで、その機能の背後にあるビジネスロジックやデータベースアクションは確認しないでください。

実際のデータベースアクセスを単体テストしたい場合は、ネットワークスタックとデータベースサーバーに依存するため、実際にはより多くの統合テストが行​​われますが、SQLコードが要求どおりに機能することを確認できます。行う。

私にとってユニットテストの隠された力は、それがない場合よりもはるかに優れた方法でアプリケーションを設計しなければならないことです。これは、「この機能ですべてをやるべきだ」という考え方から離れることができたからです。

申し訳ありませんが、PHP / Pythonの特定のコード例はありませんが、.NETの例を確認したい場合は、これと同じテストを行うために使用した手法を説明する投稿があります。


2

私は通常、オブジェクト(およびORM(存在する場合))のテストとデータベースのテストの間にテストを分割しようとします。私はデータアクセス呼び出しをモックすることによってオブジェクト側のことをテストしますが、私の経験では、通常はかなり制限されているdbとのオブジェクトの相互作用をテストすることによって、データベース側をテストします。

私は、データアクセス部分のモックを開始するまで単体テストの作成に不満を感じていたため、テストデータベースを作成したり、その場でテストデータを生成したりする必要はありませんでした。データをモックすることで、実行時にすべてを生成し、オブジェクトが既知の入力で正しく動作することを確認できます。


2

私はこれをPHPで行ったことはなく、Pythonを使用したこともありませんが、データベースへの呼び出しをモックアウトすることをお勧めします。これを行うには、サードパーティツールまたは自分で管理するIoCを実装できます。次に、偽の呼び出しの結果を制御するデータベース呼び出し元のモックバージョンを実装できます。

インターフェイスにコーディングするだけで、IoCの単純な形式を実行できます。これには、コードで何らかのオブジェクト指向を実行する必要があるため、あなたの行動に適用されない可能性があります(私が続ける必要があるのは、PHPとPythonについての言及だけであるためです)

それが役に立てば幸いです。他に何もない場合は、今検索する用語がいくつかあります。


2

私は最初の投稿に同意します-データベースアクセスは、インターフェイスを実装するDAOレイヤーに取り除かれるべきです。次に、DAOレイヤーのスタブ実装に対してロジックをテストできます。


2

モッキングフレームワークを使用て、データベースエンジンを抽象化できます。PHP / Pythonにいくつかあるかどうかはわかりませんが、型付き言語(C#、Javaなど)にはたくさんの選択肢があります

以前の投稿で述べたように、一部の設計は他のものよりユニットテストが容易なため、データベースアクセスコードの設計方法にも依存します。


2

単体テストのテストデータを設定するのは難しい場合があります。

Javaの場合、Spring APIを使用してユニットテストを行うと、ユニットレベルでトランザクションを制御できます。つまり、データベースの更新/挿入/削除を含む単体テストを実行し、変更をロールバックできます。実行の最後に、実行を開始する前の状態にすべてをデータベースに残します。私にとって、それは可能な限り良いことです。

弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.