📜 ⬆️ ⬇️

Unit testing of Yii2 behavior with Codeception

In software development, writing automatic tests is often overshadowed by more pressing issues. So in my case, the code had to be written, but tests for it were not. At the same time, I wanted to try modular testing of my own code for a long time, and then the behavior of Yii2 ManyToMany Behavior , which was already written on Habré, turned up. I first expanded this behavior a bit, and then decided to put together a test suite.

The tests themselves, including those referred to in this article, can be viewed in the repository at the link above. All the commands were executed under Windows with a globally installed composer, but I think that developers using Linux can easily adapt them for themselves.

Next, we look at setting up a Codeception with a module for Yii2 and creating tests for behavior.

Why test?


Are automatic tests worth the time spent on their development? It is difficult to answer unequivocally.
')
When I decided to participate in the development of Yii2 ManyToMany Behavior, the functionality for working with connections of type 1-N was partially implemented and not tested. At a minimum, I had to make sure that the existing code was working. If I had not written automatic tests, I would still have to create some kind of Yii2 application, connect behavior to its models, and then check whether it works on some test data. From this point of view, tests are beneficial, since the costs of writing the tests themselves are a drop in the ocean compared to the preparation of test data and test application. In addition, the behavior being developed is a fairly simple thing, which requires only the standard Yii2 code, which is almost guaranteed to work. This greatly facilitates the preparation of tests.
In my case, the tests automatically justified themselves. As it turned out, by resolving the conflict when merging branches, I ruined something, and the 1-N connections ceased to exist. Thanks to the test, I quickly found the error and corrected it.

What are we testing?


Behavior, which we are considering, while maintaining the model also allows you to save its connection with other models. For example, consider a simple data structure consisting of books ( Book ), authors ( Author ), and reviews of books ( Review ). Books and authors are related as NN, that is, a book can have many authors, and the author has many books. Books and reviews are related as 1-N, that is, a book can have a lot of reviews, but each review can relate to only one book.



During testing, we will save one book with all its links. When saving the model, it is necessary to consider several possible input data options:
  1. A non-empty array of identifiers of the associated model. At the same time, old links should be removed and new ones created.
  2. An empty array, as a result of which the old connections must be removed.
  3. Complete lack of data, corresponding, for example, to the editing form of a book in which there are no fields relating to the authors. In this case, the behavior should not do anything, that is, the existing connections should not change.

It is necessary to check the behavior of behavior in all three cases.

Features Associated with Yii2


Since the behavior is intended to work with Yii2, there is no point in testing it without the rest of the framework. For testing, we will actually create a console application Yii2, and in it we will operate with models. We consider the model from the database, pass on the necessary parameters to it, save it, read it again from the database, and check if it is saved correctly.

Of course, for testing we need a database. Fortunately, for our task it is not necessary to have a separate database server. It will be enough to use the SQLite DBMS, which is supported by Yii2 and stores the database in a file. The test data themselves will be stored as a dump, which is loaded before each test.

Configure Codeception


First, with the help of composer, let's execute a global codeception installation:

composer global require codeception/codeception 

Now we will prepare everything necessary to test our behavior. In the behavior directory there is already a file composer.json , which describes the behavior and its dependencies. Add the yii2-codeception library to it:

 composer require --dev yiisoft/yii2-codeception 

Then, we initialize the codeception environment in the behavior directory:

 codecept bootstrap --customize 

The name of the actor ( actor ) can be left as default ( Tester ), and we need only one set of tests ( suite ) - unit .

The tests directory and the codeception.yml file will appear , in which we will set the necessary parameters. The default settings are fine with us, except for connecting to the database.

 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 

Now you need to configure the unit test suite in the files / unit.suite.yml file :

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

Module UnitHelper , which was enabled by default, we do not need, but we added Asserts and Db . Now we will build the environment taking into account the selected modules:

 codecept build 

Finally, you need to configure the Yii2 autoloader in the tests / _bootstrap.php file :
 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'); 

Before writing tests, you need to prepare a database dump and create model classes.

Database dump preparation


To create a database structure, it is convenient to use a visual tool, such as DB Browser for SQLite .

Create book , author , review and book_has_author tables , fill them with test data. Then we do a dump and save it to tests / _data / dump.sql .

My dump looks like this:

 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; 


Configuring the application


Since our behavior will be tested within the console application, you need to prepare a configuration for it. Create the file 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' => '', ] ] ]; 

Creating models


We create model class files in the tests / _data directory and give them the namespace data . In order not to do this manually, I in a different directory unfolded the basic application template, connected it to the database and created classes using gii .

It is important that the desired relationship be declared in the Book model:

 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']); } 

We also add the following behavior:

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

Be sure to specify the validator for the attributes that are created by the behavior:

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

Now you can write the tests themselves.

Creating tests


In codeception, test cases are drawn up as classes. To work with Yii2 objects, you need to create a class inherited from yii \ codeception \ TestCase . The class name and file name must end with Test .

In the tests / unit / BehaviorTest.php file, we will create a BehaviorTest test case, and in it the testSaveManyToMany method, which checks whether the correct data set is stored for the NN connection:

 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'); } ... 

We perform actions that are usually associated with the preservation of the form Certain data comes from the request ( $ post variable). The load () method is used to write this data to model attributes. The model is then saved using the save () method.

After our manipulations, the book should have three authors with keys 7, 8 and 9, which is verified.

Other tests are described similarly, for example, saving an empty data set for a 1-N connection:

 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'); } 

If you run the codecept run , the system will run all the available tests and report on their results:

 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) 

findings


Having tried unit testing in business, I see how useful it has been when developing various add-ons and add-ons. In other words, it is convenient to test behavior in this way, but I wouldn’t cover all the code of my project with unit tests.

One of the problems of unit testing that I encountered is the need to invent the conditions under which the code is checked. When you look at what you wrote yourself, it’s hard to imagine where it might break. It seems to me that an outside view would help here.

In any case, I can say with confidence that when writing code, which will then be reused in other projects, unit testing solves a lot of problems and definitely pays for the time spent on its preparation.

Source: https://habr.com/ru/post/254509/


All Articles