Codeceptionを使用したYii2の動作の単体テスト

ソフトウェア開発では、自動化されたテストの作成は、より差し迫った問題によってバックグラウンドに追いやられることがよくあります。 したがって、私の場合、コードを書く必要がありましたが、テストはありませんでした。 同時に、私は長い間自分のコードの単体テストを試してみたかったのですが、ここではすでにHabréで記述されたYii2 ManyToMany Behaviorの動作がアームによって検出されました。 最初にこの動作を少し拡張してから、一連のテストを作成することにしました。



この記事で説明したテストを含むテスト自体は、上記のリンクのリポジトリで表示できます。 すべてのコマンドは、ComposerがグローバルにインストールされたWindowsで実行されましたが、Linuxを使用している開発者は、コマンドを自分で簡単に調整できると思います。



次に、Yii2のモジュールでCodeceptionを設定し、動作のテストを作成する方法を見ていきます。



テストする理由



自動テストは、それらの開発に費やした時間の価値がありますか? 明確に答えることは困難です。



Yii2 ManyToMany Behaviorの開発に参加することにしたとき、1-Nタイプの通信機能は部分的に実装され、テストされていませんでした。 少なくとも、既存のコードが機能していることを確認する必要がありました。 自動テストを作成していない場合でも、何らかのYii2アプリケーションを作成し、ビヘイビアーをモデルに接続して、テストデータが機能するかどうかを確認する必要があります。 この観点からすると、テスト自体を作成するコストは、テストデータとテストアプリケーションを準備する場合と比べて、バケットの低下につながるため、テストは有益です。 さらに、開発された動作は非常に単純なものであり、その操作には標準のYii2コードのみが必要であり、機能することがほぼ保証されています。 これにより、テストの準備が非常に容易になります。

私の場合、テストは自動的に成果を上げました。 ブランチをマージするときに競合を解決することが判明したため、何かを台無しにして、1-N接続の保存を停止しました。 テストのおかげで、私はすぐにエラーを見つけて修正しました。



何をテストしていますか?



検討している動作により、モデルを保存するときに他のモデルとの関係を保持できます。 たとえば、書籍( Book )、著者( Author )、書籍のレビューReview )で構成される単純なデータ構造を考えてみましょう。 書籍と著者はNNとして関連付けられています。つまり、本には多くの著者を含めることができ、著者には多くの書籍があります。 書籍とレビューは1-Nとしてリンクされます。つまり、書籍には多くのレビューを含めることができますが、各レビューは1つの書籍にのみ関連付けることができます。







テスト中に、すべての接続を含む1冊の本を保存します。 モデルを保存するとき、いくつかの可能な入力オプションを考慮する必要があります。

  1. 関連モデルの識別子の空でない配列。 この場合、古い接続を削除し、新しい接続を作成する必要があります。
  2. 空の配列。その結果、古い接続を削除する必要があります。
  3. たとえば、著者に関連するフィールドがない本の編集フォームに対応するデータの完全な欠如。 この場合、振る舞いは何もしないはずです。つまり、既存の関係は変わらないはずです。


3つのケースすべてで、動作のパフォーマンスを確認する必要があります。



Yii2に関連する機能



この動作はYii2で動作することを目的としているため、フレームワークの残りなしでテストすることは意味がありません。 テストのために、実際にYii2コン​​ソールアプリケーションを作成し、その中でモデルを操作します。 データベースからモデルを読み取り、必要なパラメーターを転送して保存し、データベースから再読み取りして、モデルが正しく保存されたかどうかを確認します。



もちろん、テストにはデータベースが必要です。 幸いなことに、タスクに別のデータベースサーバーを用意する必要はありません。 Yii2でサポートされ、データベースをファイルに保存するSQLite DBMSを使用すれば十分です。 テストデータ自体はダンプの形式で保存され、各テストの前にロードされます。



Codeceptionの設定



開始するには、composerを使用してcodeceptionのグローバルインストールを実行します。



composer global require codeception/codeception
      
      





次に、動作をテストするために必要なすべてを準備します。 動作ディレクトリにはすでにcomposer.jsonファイルが含まれており、このファイルには動作とその依存関係が記述されています。 yii2-codeceptionライブラリを追加します:



 composer require --dev yiisoft/yii2-codeception
      
      





次に、behaviorディレクトリでcodeception環境を初期化します。



 codecept bootstrap --customize
      
      





アクターの名前( actor )はデフォルトで残すことができ( Tester )、必要なのは1セットのテスト( suite )-unitだけです。



testsディレクトリとcodeception.ymlファイルが表示され、必要なパラメーターを設定します。 データベースへの接続を除き、デフォルトの設定で問題ありません。



 actor: Tester paths: tests: tests log: tests/_output data: tests/_data helpers: tests/_support settings: bootstrap: _bootstrap.php colors: false memory_limit: 1024M modules: config: Db: dsn: 'sqlite:tests/_output/temp.db' user: '' password: '' dump: tests/_data/dump.sql
      
      





ここで、 tests / unit.suite.ymlファイルユニットテストスイートを設定する必要があります



 class_name: UnitTester modules: enabled: [Asserts, Db]
      
      





デフォルトで有効になっているUnitHelperモジュールは必要ありませんが、 AssertsDbを追加しました。 選択したモジュールを考慮して環境を構築しましょう。



 codecept build
      
      





最後に、 tests / _bootstrap.phpファイルでYii2オートローダーを設定する必要があります

 defined('YII_DEBUG') or define('YII_DEBUG', true); defined('YII_ENV') or define('YII_ENV', 'dev'); require_once __DIR__ . implode(DIRECTORY_SEPARATOR, ['', '..', 'vendor', 'autoload.php']); require_once __DIR__ . implode(DIRECTORY_SEPARATOR, ['', '..', 'vendor', 'yiisoft', 'yii2', 'Yii.php']); Yii::setAlias('@tests', __DIR__); Yii::setAlias('@data', __DIR__ . DIRECTORY_SEPARATOR . '_data');
      
      





テストを作成する前に、データベースダンプを準備し、モデルクラスを作成する必要があります。



データベースダンプの準備



SQLiteのDB Browserなどの視覚的なツールを使用して、データベース構造を作成すると便利です。



bookauthorreview 、およびbook_has_authorのテーブルを作成し、それらにテストデータを入力します。 次に、それをtests / _data / dump.sqlにダンプして保存します



私のダンプは次のようになります。



 BEGIN TRANSACTION; CREATE TABLE "review" ( `id` INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, `book_id` INTEGER, `comment` VARCHAR(150) NOT NULL, `rating` INTEGER NOT NULL ); INSERT INTO `review` VALUES (1,3,' ,   .',5); INSERT INTO `review` VALUES (2,3,'!',5); INSERT INTO `review` VALUES (3,3,'.',4); INSERT INTO `review` VALUES (4,5,'!',2); CREATE TABLE "book_has_author" ( `book_id` INTEGER NOT NULL, `author_id` INTEGER NOT NULL ); INSERT INTO `book_has_author` VALUES (1,1); INSERT INTO `book_has_author` VALUES (1,2); INSERT INTO `book_has_author` VALUES (2,1); INSERT INTO `book_has_author` VALUES (2,3); INSERT INTO `book_has_author` VALUES (3,4); INSERT INTO `book_has_author` VALUES (4,5); INSERT INTO `book_has_author` VALUES (4,6); INSERT INTO `book_has_author` VALUES (5,9); CREATE TABLE "book" ( `id` INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, `name` VARCHAR(150) NOT NULL, `year` INTEGER NOT NULL ); INSERT INTO `book` VALUES (1,'   .',2004); INSERT INTO `book` VALUES (2,':   /.',2005); INSERT INTO `book` VALUES (3,'   .',1964); INSERT INTO `book` VALUES (4,'   .',1979); INSERT INTO `book` VALUES (5,'.     .',2004); CREATE TABLE "author" ( `id` INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, `name` VARCHAR(150) NOT NULL ); INSERT INTO `author` VALUES (1,' ..'); INSERT INTO `author` VALUES (2,' ..'); INSERT INTO `author` VALUES (3,' ..'); INSERT INTO `author` VALUES (4,' ..'); INSERT INTO `author` VALUES (5,' ..'); INSERT INTO `author` VALUES (6,' ..'); INSERT INTO `author` VALUES (7,' ..'); INSERT INTO `author` VALUES (8,' ..'); INSERT INTO `author` VALUES (9,' ..'); COMMIT;
      
      







アプリケーション構成



動作はコンソールアプリケーションの一部としてテストされるため、そのための構成を準備する必要があります。 tests / unit / _config.phpファイルを作成します



 <?php return [ 'id' => 'app-console', 'class' => 'yii\console\Application', 'basePath' => \Yii::getAlias('@tests'), 'runtimePath' => \Yii::getAlias('@tests/_output'), 'bootstrap' => [], 'components' => [ 'db' => [ 'class' => '\yii\db\Connection', 'dsn' => 'sqlite:'.\Yii::getAlias('@tests/_output/temp.db'), 'username' => '', 'password' => '', ] ] ];
      
      





モデル作成



tests / _dataディレクトリにモデルクラスファイルを作成し、 名前空間データを与えます 。 これを手動で行わないために、別のディレクトリに基本的なアプリケーションテンプレートをデプロイし、データベースに接続して、 giiを使用してクラスを作成しました。



Bookモデルで必要な関係を宣言することが重要です。



 public function getAuthors() { return $this->hasMany(Author::className(), ['id' => 'book_id']) ->viaTable('book_has_author', ['author_id' => 'id']); } public function getReviews() { return $this->hasMany(Review::className(), ['book_id' => 'id']); }
      
      





ここにも動作を追加します。



 public function behaviors() { return [ [ 'class' => \voskobovich\behaviors\ManyToManyBehavior::className(), 'relations' => [ 'author_list' => ['authors'], 'review_list' => ['reviews'], ] ] ]; }
      
      





動作によって作成される属性のバリデーターを必ず指定してください。



 public function rules() { return [ [['author_list', 'review_list'], 'safe'], ...
      
      





これで、テスト自体を作成できます。



テスト作成



codeceptionでは、テストケースはクラスとしてフォーマットされます。 Yii2オブジェクトを操作するには、 yii \ codeception \ TestCaseから継承したクラスを作成する必要があります。 クラス名とファイル名はTestで終わる必要があります。



tests / unit / BehaviorTest.phpファイルで、 BehaviorTestテストケースを作成し、その中にtestSaveManyToManyメソッドを作成します。 このメソッドは、NN通信用に正しいデータセットが保存されているかどうかを確認します。



 class BehaviorTest extends \yii\codeception\TestCase { public $appConfig = '@tests/unit/_config.php'; public function testSaveManyToMany() { //load $book = Book::findOne(5); //simulate form input $post = [ 'Book' => [ 'author_list' => [7, 9, 8] ] ]; $this->assertTrue($book->load($post), 'Load POST data'); $this->assertTrue($book->save(), 'Save model'); //reload $book = Book::findOne(5); //must have three authors $this->assertEquals(3, count($book->authors), 'Author count after save'); //must have authors 7, 8, and 9 $author_keys = array_keys($book->getAuthors()->indexBy('id')->all()); $this->assertContains(7, $author_keys, 'Saved author exists'); $this->assertContains(8, $author_keys, 'Saved author exists'); $this->assertContains(9, $author_keys, 'Saved author exists'); } ...
      
      





通常、フォームの維持に関連するアクションを実行します。 特定のデータはリクエストから取得されます( $ post変数)。 load()メソッドは、このデータをモデル属性に書き込むために使用されます。 次に、 save()メソッドを使用してモデルが保存されます。



操作後、本にはチェックされているキー7、8、9を持つ3人の著者がいるはずです。



他のテストも同様に説明されます。たとえば、1-N通信用に空のデータセットを保存します。



 public function testResetOneToMany() { //load $book = Book::findOne(3); //simulate form input $post = [ 'Book' => [ 'review_list' => [] ] ]; $this->assertTrue($book->load($post), 'Load POST data'); $this->assertTrue($book->save(), 'Save model'); //reload $book = Book::findOne(3); //must have zero reviews $this->assertEquals(0, count($book->reviews), 'Review count after save'); }
      
      





codecept runを実行すると、システムは利用可能なすべてのテストを実施し、結果を報告します。



 Codeception PHP Testing Framework v2.0.11 Powered by PHPUnit 4.5.0 by Sebastian Bergmann and contributors. Unit Tests (2) -------------------------------------------------------------------------------------- Test save many to many (BehaviorTest::testSaveManyToMany) Ok Test reset one to many (BehaviorTest::testResetOneToMany) Ok ----------------------------------------------------------------------------------------------------- Time: 390 ms, Memory: 9.00Mb OK (2 tests, 9 assertions)
      
      





結論



実際に単体テストを試してみたところ、さまざまなアドオンやアドオンの開発にどれほど役立つかがわかりました。 つまり、この方法で動作をテストするのは便利ですが、プロジェクトのコード全体を単体テストでカバーすることは始めません。



私が遭遇したユニットテストの問題の1つは、コードがテストされる条件を発明する必要があることです。 自分で書いたものを見ると、どこが壊れるか想像するのは難しいです。 私には外の景色がここで役立つと思われます。



いずれにせよ、他のプロジェクトで再利用されるコードを書くとき、ユニットテストは多くの問題を解決し、その準備に費やした時間を確実に回収できると自信を持って言えます。



All Articles