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:
- A non-empty array of identifiers of the associated model. At the same time, old links should be removed and new ones created.
- An empty array, as a result of which the old connections must be removed.
- 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() {
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() {
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.