データベースと対話するメソッドを自動的にテストするためにシステムがどのように開発されたのか、プロジェクト環境でシステムを開発および実装するプロセスで遭遇する落とし穴の詳細な説明。
導入語
新しいプロジェクトに取り組み始めたときのアイデアは、 TDDアプローチを使用し、それに応じて、使用する各コンポーネントの単体テストを作成することでした。 なぜなら プロジェクト全体がデータベースと密接にやり取りするので、もちろん、データベースに直接接続されたコンポーネントをどうするか、そしてそれらをどのようにテストするかという疑問が提起されました。 この問題をすばやく解決する方法が見つからなかったため、この問題を後ほど延期することにしました。 しかし、彼は長い間私を手放すことを止めませんでした。ある晴れた日、私はデータベースとの相互作用をすばやくテストできる作業ドラフトの一部としてフレームワークを開発することができました。
テストシステムは、作業プロジェクトのコンテキストおよびコンテキスト内で開発され、独自のORMデータベースシステムを使用します。ORMデータベースシステムは、パラメーター化されたSQLクエリを受け入れ、受信したテーブルデータを出力のオブジェクトに個別に分解します。 テストシステムを開発する場合、いくつかの制限があります。
私は開発中のテストをモジュラーと呼んでいるとすぐに言いたいのですが、実際にはそれらはユニットテストのすべての便利さと利点だけを持つ統合テストです。
やる気
なぜユニットテストを書くように強制するのですか? 開発したアプリケーションの品質を向上させるため。 開発中のプロジェクトが大きいほど、特にリファクタリング中に何かを壊す可能性が高くなります。 単体テストを使用すると、システムコンポーネントの正しい動作をすばやく確認できます。 ただし、データベースとやり取りするメソッドの単体テストを作成するのは、それほど簡単な作業ではありません。 テストが正しく機能するには、カスタマイズされた環境が必要です。より正確には、次のとおりです。
- 構成されたデータベースサーバー
- 正しく構成されたデータベース接続文字列
- 必要なすべてのテーブルを含むデータベースをテストします
- テストデータベースに正しく入力されたテーブル
そして、これはかなり複雑に見えるので、ほとんどの開発者はそれについて考えさえしません。 したがって、私の目標は、プロジェクトの他のコンポーネントの通常の単体テストと変わらない程度にテストの記述を簡素化するシステムを開発することでした。
問題の説明
開発についての話を始める前に、ほとんどの場合、そのようなことをどのようにテストし、何につながるのかを説明したいと思います。
1.データベースでクエリをテストする
開発者は、データベースとやり取りするメソッドを作成する前に、パラメーターを使用して通常のSQLクエリを作成し、データベースのローカルコピーに対する操作を確認します。 これは、エラーリクエストをチェックするための最速かつ最も簡単な方法です。 ただし、このアプローチを使用すると、リクエストをプロジェクトコードまたはこのリクエストを実行するコードに転送する際にミスを犯す可能性があることに注意してください。 例として、いくつかのパラメーターの初期化を誤ってスキップすることがあります。これにより、リクエストが正しく動作しなくなります。
2.直接メソッド呼び出しによるテスト
別の簡単な方法-アプリケーションで常に実行されるコードのセクション、たとえばMainを開き、データベースと対話するメソッドを呼び出し、ブレークポイントを設定して結果を表示するだけです...これは、画面に情報メッセージを表示するデバッグと非常に似ています。
3.アプリケーションのユーザーインターフェイスを使用したテスト
書かれた方法をテストする別の非常に興味深い非常に人気のある方法。 アプリケーションを取得し、データベースと対話するメソッドの呼び出しにつながる一連のアクションを実行します。 メソッドは非常に複雑であり、リクエストが実際に満たされたことを保証しません。 アプリケーションを実行する代わりに、設定のどこかに示されているため、キャッシュにアクセスしたり、別の操作を実行したりできます。
そして、これらすべての困難は、かなり単純なことをテストするのに多くの時間を費やさなければならないという事実につながります。 そして、ご存知のように、これを長時間チェックすると、開発者がテストするのが面倒で、エラーにつながる可能性が高いです。 統計によると、アイドルデータベースクエリに関連するエラーの約5%がバグトラッカーに記録されます。 しかし、これらのエラーの大部分は口頭で十分に説明されていることが多く、残念ながら、これらのエラーはこれらの統計では考慮できないことに注意してください。
開発開始
このシステムの必要性を確信した後、その作成プロセスを説明することができます。
自動テストシステムを開発する方法は?
私が最初に疑問に思ったのは、そのようなシステムをどのように開発するか、どのような要件を満たすべきかということでした。 したがって、私は常に単体テストを書いているという事実にもかかわらず、主題分野の研究に時間を費やす必要がありました。 テストシステムの開発は、まったく異なる分野です。 この研究の枠組みの中で、私は既存の解決策を見つけ、可能な実装方法を見つけようとしました。 しかし、最初に、ユニットテストの開発の原則を思い出さなければなりませんでした。
- 1つのテストが1つのシナリオに適合する
- テストは時間とさまざまなランダム変数に依存するべきではありません
- テストはアトミックでなければなりません
- テスト実行時間は短くする必要があります
その後、これらの要件を満たすテストプロセスを開発する必要がありました。
どのテストプロセスが適切ですか?
長い反射の結果、単純な3段階のプロセスが開発されました。
- 初期化 -この段階で、データベースサーバーへの接続が行われ、新しいデータベースを初期化するためのスクリプトが読み込まれ、その分析も行われました。 その後、新しい空のデータベースが作成され、テストメソッドの実行に使用されます。 そしてもちろん、必要な構造での初期化。
- 実行は、 AAAアプローチを満たすテストメソッドです。
- 完了 -この時点で、使用済みリソースが解放されます。使用済みデータベースの削除、データベースサーバーへのオープン接続の完了。
したがって、このプロセスのおかげで、単体テストが満たさなければならない要件を満たすことができました。 なぜなら 各テストメソッドは専用のデータベースで実行されるため、これらはアトミックであり、したがって、並行して実行できると言えます。
開発したシステムの分析
性能
おそらく今では、そのようなアプローチはビッグデータではすぐに機能しないと多くの人が考えていたでしょうし、正しいでしょう。 プロジェクトが大きく、データベースが大きく、多くの異なるテーブルと初期データがそれぞれ含まれているため、新しいデータベースの初期化時間が非常に長くなります。 次の表は、1つのテストの結果を示しています。
初期化フェーズ | 時間、ミリ秒 | 共有する |
---|---|---|
ファイルのアップロード | 6 | <1% |
スクリプトの準備 | 19 | <1% |
スクリプト解析 | 211 | 1% |
スクリプトコマンドの実行 | 14660 | 98% |
合計 | 14660 |
ご覧のとおり、1つのテストデータベースを初期化するには約15秒かかります。 しかし、プロジェクトは明らかに複数のテストを作成します。 プロジェクトに約100個のテストが書き込まれていると仮定すると、それらの合計実行時間は30分以上になります。 そのようなテストは基本的な原則、つまり短いリードタイムを満たしていないことがすぐに明らかになりました。
システムのパフォーマンスを分析するために座っていなければなりませんでした。 最適化の対象となる可能性のあるテスト初期化の4つの主要なセクションを特定しました。 その結果、上記の表を入手できました。表からわかるように、98%の時間はテストデータベース初期化スクリプトへのコマンド送信に関係する段階に進みます。 この状況を修正するのに役立つ2つの主なアイデアがありました。トランザクションの使用と、テストされたデータベースから必要なテーブルのみの使用です。
最初のオプションは、一連のメソッドをテストするために、事前に作成された1つのデータベースを使用することでした。 さらに、テストデータの挿入を含む各メソッドを実行するために、トランザクションに囲まれていました。 テストメソッドの実行後、トランザクションはロールバックされたため、データベースは純粋な形のままでした...純粋な形のままであるはずです。 しかし、判明したように、プロジェクトで使用されるテーブルのほとんどはMyISAMエンジンを使用しています。 ご存じのとおり、これはトランザクションをサポートしていません。 しかし、これがこのオプションが削除された唯一の理由ではありません。 前述したように、テストを並行して実行するには、テストが完全にアトミックでなければなりません。 そして以来 このアプローチは1つの共通データベースの使用に依存するため、これはテストの原子性に完全に違反し、上記の要件を満たしません。
2番目のオプションは、テストを記述するときに、開発者自身がこのテストを完了するために必要なテーブルを示すことです。 システムは、データベース初期化スクリプトでこれらのテーブル、および初期化データやセカンダリキーに関連付けられた追加のテーブルなどの必要なすべての依存関係を自動的に検出しました。 このアプローチにより、各テストの原子性が確保されました。 既に書かれたテストに新しいメカニズムが実装された後、テストの初期化の平均時間も測定しました。結果は以下の表に示されています。
初期化フェーズ | 時間、ミリ秒 | 共有する |
---|---|---|
ファイルのアップロード | 6 | 1% |
スクリプトの準備 | 22 | 5% |
スクリプト解析 | 254 | 62% |
スクリプトコマンドの実行 | 134 | 32% |
合計 | 416 |
ご覧のとおり、このような最適化の後、97%の時間を節約できました! データベースクエリをテストするためのクイックテストに向けた良いステップ。 この表から、最適化の機会がまだあることがわかりますが、現時点では、このようなテスト実行時間はニーズと要件を完全に満たしています。
自動データ生成システムの開発
システムのパフォーマンスはすべて順調でしたが、何らかの理由で、プロジェクトのほとんどの開発者はこのシステムを避けました。 これが起こる理由を理解しなければなりませんでした。 結局のところ、このようなシステムのテストを作成するには、必要なすべてのデータを入力するのに多くの時間と器用さが必要でした。 このコードを見てください:
[TestMethod] public void GetInboxMessages_ShouldReturnInboxMessages() { const int validRecipient = 1; const int wrongRecipient = 2; var recipients = new [] { validRecipient }; // , var message = new MessageEntity(); HelperDataProvider.Insert(message); // var validInboxMessage = new InboxMessageEntity() { MessageId = message.MessageId, RecipientId = validRecipient }; var wrongInboxMessage = new InboxMessageEntity() { MessageId = message.MessageId, RecipientId = wrongRecipient }; // HelperDataProvider.Insert(validInboxMessage); HelperDataProvider.Insert(wrongInboxMessage); // var collection = _target.GetInboxMessages(recipients); Assert.AreEqual(1, collection.Count); Assert.IsNotNull(collection.FirstOrDefault(x => x.Id == validInboxMessage.Id)); Assert.IsNull(collection.FirstOrDefault(x => x.Id == wrongInboxMessage.Id)); }
テストはAAAアプローチを使用して記述されます。最初の部分は、新しいエンティティを作成し、それらをリンクして、データベースに挿入することです。 次に、テストメソッドが呼び出され、結果がチェックされます。 また、これは、1つのテーブルのみでセカンダリキーバインディングがあり、入力されたフィールドに追加の要件がない場合の別の単純なケースです。 次に、同じ例を見てみましょう。ただし、エンティティの自動生成システムを使用します。
[TestMethod] public void GetInboxMessages_ShouldReturnInboxMessages() { const int validRecipient = 1; const int wrongRecipient = 2; const int recipientsCount = 2; const int messagesCount = 3; var recipients = new [] { validRecipient }; // DataFactory.CreateBuilder<InboxMessageEntity>() // , .UseForeignKeyRule(InboxMessageEntity inboxEntity => inboxEntity.MessageId, MessageEntity messageEntity => messageEntity.MessageId) // .UseEnumerableRule(inboxEntity => inboxEntity.RecipientId, new[] { validRecipient, wrongRecipient }) // , N:N, .SetDefaultGroup(new FixedGroupProvider(recipientsCount)) // - .CreateMany(messagesCount * recipientsCount) .InsertAll(); // var collection = _target.GetInboxMessages(recipients); Assert.AreEqual(messagesCount, collection.Count); Assert.IsTrue(collection.All(inboxMessage => inboxMessage.RecipientId == validRecipient)); }
ほんの数行のジェネレーター設定、および出力では、すべての必要なデータをテストするためのデータベースを完全に準備できます。 このシステムは、エンティティのルールと、これらのルールのグループ化に基づいて構築されています。 このアプローチにより、N:NまたはN:1の形式のエンティティ間の関係を構成できます。 このシステムには次のルールがあります。
- DataSetterRule-エンティティフィールドの1つに特定の値を設定できます
- EnumerableDataRule-さまざまなエンティティに代わる値のリストを指定できます。 たとえば、最初に作成されたエンティティでは、リストの最初の値が設定され、2番目では-2番目などが設定されます。 サイクリングを使用して
- RandomDataRule-使用可能なリストからランダムな値を生成します。複雑なクエリのパフォーマンスをテストするためにビッグデータを生成するために使用すると非常に便利です
- UniqueDataRule-特定のエンティティフィールドに対してランダムな一意の値を生成します。 このルールは、テーブルの列が一意性によって制約されるエンティティのセットを作成する場合に適しています
- ForeignKeyRule-最も有用なルールで、2つのエンティティを接続できます。 このルールのエンティティのグループ化を設定すると、結果としてフォームN:NまたはN:1のエンティティ間の関係を取得できます。
これらのルールを操作することにより、さまざまなデータセットを作成できます。 CreateManyまたはCreateSingleメソッドが呼び出されてエンティティが作成された後、ビルダーは必要なすべてのルールに従い、エンティティを埋めてから、別の内部バッファーに保存します。 そして、 InsertAllメソッドが呼び出された後にのみ、ビルダーはすべてのエンティティをデータベースに送信します。 作業のスキームを以下に示します。
プロジェクト環境でのシステムの実装
もちろん、展開プロセスでこのシステムを導入することは必須でした。 手動でテストを実行する開発者はほとんどいません。 この問題を解決するには、アプリケーションをデプロイするたびに実行するのが最適な方法です。
残念ながら、このシステムはアセンブリプロセスに統合することはお勧めしません。 アセンブリサーバーはテストデータベースサーバーにアクセスできず、テストプロセス自体がリソースを消費するため、統合テストを起動するプロセスをテスト環境に移行することが決定されました。 このために、テストエージェントを自動的に起動し、その作業の結果を分析する一連のスクリプトを使用して、テストを実行するための個別の展開手順が作成されました。 テストを実行するために、Microsoftの標準テストエージェント-MSTestAgentを使用しました 。 分析用のスクリプトの記述は、テスト結果ファイルがXML形式で記述されているため、結果の分析全体がいくつかの単純なXQueryクエリに削減されたという事実によって促進されました。 受信したデータに基づいてレポートが作成され、その後、開発者にメールで送信されるか、必要に応じてチャットチームに送信されました。
おわりに
開発中に、このシステムのさらなる使用を放棄せざるを得ない2つの重大な問題を解決する必要がありました。長いテスト実行時間とテストデータの初期化の難しさです。
データベースへのクエリの自動テストシステムが開発された後、データベースへのクエリに関連付けられたコードの記述とテストがはるかに簡単になりました。 TDDアプローチは、「クラシック」コンポーネントだけでなく、データベースと密接にやり取りするコンポーネントにも簡単に適用できます。 このシステムをプロジェクト環境に統合すると、プロジェクトの品質の監視がはるかに簡単になりました。 コンポーネントの不適切な動作はプロジェクトのビルド中に検出され、すべての開発者にすぐに表示されます。
最後に、データへのアクセスレベルがプロジェクトでどのようにテストされているかを知りたいですか?