This is an old revision of the document!
Yii 2 Testing Setup
Setup Environment
Install Local Codeception
- Remove
“yiisoft/yii2-codeception”: “*”
from sectionrequire-dev
inapp/composer.json
. - If performing Acceptance tests with Selenium, you need the full Codeception package (not just Codeception
base
):- Remove
base
:$ composer remove --dev codeception/base
- Add support for full Codeception package:
$ composer requires --dev codeception/codeception
- Or edit section
require-dev
in@app/composer.json
:"require-dev": { "codeception/base": "^2.2.3", "codeception/codeception": "2.3.*", "codeception/specify": "*", "codeception/verify": "*", ... }
- Run
$ composer update
.
Install Global Codeception
Run the following commands to install Codeception globally (when sharing the same installation among several projects):
%composer global require "codeception/codeception=2.3.*" %composer global require "codeception/specify=*" %composer global require "codeception/verify=*"
Selenium
- Download Selenium browser automation server (standalone): http://www.seleniumhq.org/download
- Run Selenium server:
svr$ java -jar ~/selenium-server-standalone-x.xx.x.jar
- However, for Selenium Server 3.0, when using any of these browsers, launch Selenium using the appropriate custom driver command:
- Mozilla Firefox browser since v48, download GeckoDriver, then:
svr$ java -jar -Dwebdriver.gecko.driver=~/geckodriver ~/selenium-server-standalone-3.xx.x.jar C:\> java -jar -Dwebdriver.gecko.driver=C:\apps\Selenium\geckodriver C:\apps\Selenium\selenium-server-standalone-3.xx.x.jar
- Google Chrome browser since v53, download ChromeDriver, then:
svr$ java -jar -Dwebdriver.chrome.driver=~/chromedriver ~/selenium-server-standalone-3.xx.x.jar C:\> java -jar -Dwebdriver.chrome.driver=C:\apps\Selenium\chromedriver C:\apps\Selenium\selenium-server-standalone-3.xx.x.jar
Run Default Tests
You can run the Codeception base tests:
$ cd ~/proj/myapp/ $ ./vendor/bin/codecept run
If this fails, check or generate the configuration files for the tests.
Configure Tests
NOTE: Yii 2.0.12 already comes with the tests in [app]/tests
folder without the need to bootstrap or build these. However, if wanting to generate test structure from scratch (not the actual tests), then follow the next set of instructions.
Run Codeception bootstrap to generate configuration:
svr$ cd ~/proj/myapp/ svr$ ./vendor/bin/codecept bootstrap C:\> cd C:\proj\myapp C:\> .\vendor\bin\codecept bootstrap
This generates the required files:
C:\wamp\www\myapp>codecept bootstrap Initializing Codeception in C:\wamp\www\myapp File codeception.yml created <- global configuration tests/unit created <- unit tests tests/unit.suite.yml written <- unit tests suite configuration tests/functional created <- functional tests tests/functional.suite.yml written <- functional tests suite configuration tests/acceptance created <- acceptance tests tests/acceptance.suite.yml written <- acceptance tests suite configuration --- tests/_bootstrap.php written <- global bootstrap file Building initial Tester classes Building Actor classes for suites: acceptance, functional, unit -> AcceptanceTesterActions.php generated successfully. 0 methods added \AcceptanceTester includes modules: PhpBrowser, \Helper\Acceptance AcceptanceTester.php created. -> FunctionalTesterActions.php generated successfully. 0 methods added \FunctionalTester includes modules: \Helper\Functional FunctionalTester.php created. -> UnitTesterActions.php generated successfully. 0 methods added \UnitTester includes modules: Asserts, \Helper\Unit UnitTester.php created. Bootstrap is done. Check out C:\wamp\www\myapp/tests directory
It should generate the following structure:
myapp/tests/
myapp/tests/functional/
myapp/tests/acceptance/
myapp/tests/unit/
myapp/codeception.yml
(main configuration)myapp/tests/unit.suite.yml
myapp/tests/functional.suite.yml
myapp/tests/acceptance.suite.yml
This generates config files such as: [app]\codeception.yml
:
#actor: Tester #coverage: # #c3_url: http://localhost:8080/index-test.php/ # enabled: true # #remote: true # #remote_config: '../tests/codeception.yml' # white_list: # include: # - ../models/* # - ../controllers/* # - ../commands/* # - ../mail/* # blacklist: # include: # - ../assets/* # - ../config/* # - ../runtime/* # - ../vendor/* # - ../views/* # - ../web/* # - ../tests/* actor: Tester paths: tests: tests log: tests/_output data: tests/_data helpers: tests/_support envs: tests/_envs settings: bootstrap: _bootstrap.php suite_class: \PHPUnit_Framework_TestSuite memory_limit: 1024M log: true colors: false extensions: enabled: - Codeception\Extension\RunFailed config: # the entry script URL (with host info) for functional and acceptance tests # PLEASE ADJUST IT TO THE ACTUAL ENTRY SCRIPT URL test_entry_url: http://localhost:8080/myapp/web/index-test.php
[app]\tests\acceptance.suite.yml
:
# Codeception Test Suite Configuration # suite for acceptance tests. # perform tests in browser using the Selenium-like tools. # powered by Mink (http://mink.behat.org). # (tip: that's what your customer will see). # (tip: test your ajax and javascript by one of Mink drivers). # RUN `build` COMMAND AFTER ADDING/REMOVING MODULES. class_name: AcceptanceTester modules: enabled: #- PhpBrowser # url: 'http://localhost:8080/myapp/web' # You can use WebDriver instead of PhpBrowser to test javascript and ajax. # This will require you to install selenium. See http://codeception.com/docs/04-AcceptanceTests#Selenium # "restart" option is used by the WebDriver to start each time per test-file new session and cookies, # it is useful if you want to login in your app in each test. - WebDriver url: 'http://localhost:8080/myapp/web' # optional browser: firefox # optional config: #PhpBrowser: # url: 'http://localhost:8080/myapp/web' WebDriver: url: 'http://localhost:8080/myapp/web' browser: firefox restart: true
[app]\tests\functions.suite.yml
:
# Codeception Test Suite Configuration # suite for functional (integration) tests. # emulate web requests and make application process them. # (tip: better to use with frameworks). # RUN `build` COMMAND AFTER ADDING/REMOVING MODULES. #basic/web/index.php class_name: FunctionalTester modules: enabled: - Filesystem - Yii2
[app]\tests\unit.suite.yml
:
# Codeception Test Suite Configuration # suite for unit (internal) tests. # RUN `build` COMMAND AFTER ADDING/REMOVING MODULES. class_name: UnitTester modules: enabled: - Asserts - Db: dsn: "mysql:host=localhost;dbname=acme_test" user: "root" password: "" - Yii2: part: [orm, email]
Database Config
Verify database configuration in [app]/config/test_db.php
:
<?php $db = require(__DIR__ . '/db.php'); // test database! Important not to run tests on production or development databases //$db['dsn'] = 'mysql:host=localhost;dbname=yii2_basic_tests'; $db['dsn'] = 'mysql:host=localhost;dbname=acme_myapp_test'; return $db;
Perform database migration if necessary:
C:\> yii migrate/to m170227_101010_create_tables
or simply:
C:\> yii migrate
Build Tests
Build test suite:
svr$ ./vendor/bin/codecept build C:\> cd C:\proj\myapp\tests C:\> codecept build
NOTE: Run build
command after adding/removing modules in files:
[app]/tests/unit.suite.yml
[app]/tests/functional.suite.yml
[app]/tests/acceptance.suite.yml
This command generates:
C:\wamp\www\myapp\tests>codecept build Building Actor classes for suites: acceptance, functional, unit -> AcceptanceTesterActions.php generated successfully. 0 methods added \AcceptanceTester includes modules: PhpBrowser, \Helper\Acceptance -> FunctionalTesterActions.php generated successfully. 0 methods added \FunctionalTester includes modules: \Helper\Functional -> UnitTesterActions.php generated successfully. 0 methods added \UnitTester includes modules: Asserts, \Helper\Unit
Some support files can be found in [app]/tests/_support/_generated
. They include the commands available to be used in testing.
Run Tests
To execute unit and functional tests:
# run only unit and functional tests svr$ cd ~/proj/myapp svr$ ./vendor/bin/codecept run unit,functional C:\> cd C:\proj\myapp C:\> .\vendor\bin\codecept run unit,functional
To execute all tests:
# run all available tests svr$ ./vendor/bin/codecept run # run tests with verbose output svr$ ./vendor/bin/codecept run --debug --fail-fast # run tests with code coverage svr$ ./vendor/bin/codecept run --coverage-html --coverage-xml
To execute functional tests:
# run acceptance tests svr$ ./vendor/bin/codecept run functional
To execute acceptance tests:
# run acceptance tests svr$ ./vendor/bin/codecept run acceptance # run only acceptance test LoginCept svr$ ./vendor/bin/codecept run acceptance LoginCept
Acceptance testing requires a webserver. Run a built-in PHP webserver (accessible on port 88 for any interface):
# Alternative 1 # run built-in PHP webserver # see: http://php.net/manual/en/features.commandline.webserver.php $php -S 0.0.0.0:88 -t [app]/web/ # Alternative 2 # run built-in Yii webserver # edit default port in [app]\vendor\yiisoft\yii2\console\controllers\ServeController.php # run default port: svr$ ./yii serve svr$ ./yii serve localhost:88
Generate Tests
Acceptance Tests
Generate CEPT acceptance test (for use with single test):
svr$ ./vendor/bin/codecept generate:cept acceptance Welcome Test was created in C:/proj/myapp/tests/acceptance/WelcomeCept.php
Generated code:
<?php $I = new AcceptanceTester($scenario); $I->wantTo('perform actions and see result'); // Add something $I->amOnPage('/'); $I->see('Welcome');
Generate CEST acceptance test (for use with multiple tests):
svr$ ./vendor/bin/codecept generate:cest acceptance HelloWorld
Test:
<?php class HelloWorldCest { public function _before(AcceptanceTester $I) { $I->amOnPage('/forgotten') } public function _after(AcceptanceTester $I) { } // tests public function testEmailField(AcceptanceTester $I) { $I->see('Enter email'); } public function testIncorrectEmail(AcceptanceTester $I) { $I->fillField('email', 'incorrect@email.com'); $I->click('Continue'); $I->see('Email is incorrect, try again'); } public function testCorrectEmail(AcceptanceTester $I) { $I->fillField('email', 'correct@email.com'); $I->click('Continue'); $I->see('Please check your email for next instructions'); } }
Functional Tests
Generate CEPT functional test (for use with single test):
svr$ ./vendor/bin/codecept generate:cept functional HelloWorld
Generated code:
<?php $I = new FunctionalTester($scenario); $I->amOnPage('/'); $I->see('Welcome');
Generate CEST functional test (for use with multiple tests):
svr$ ./vendor/bin/codecept generate:cest functional HelloWorld
Unit Tests
Generate unit test:
# To generate PHPUnit test inside [app]/test/unit dir svr$ vendor/bin/codecept generate:phpunit unit HelloWorld # To generate PHPUnit test inside [app]/test/unit/models dir svr$ ./vendor/bin/codecept generate:phpunit unit models/HelloWorld
Or simply inherit your tests on \PHPUnit_Framework_TestCase
Alternatively, generate Codeception unit tests:
# To generate Codeception unit test inside [app]/test/unit dir svr$ ./vendor/bin/codecept generate:test unit HelloWorld
Generated code:
<?php class AddressTest extends \Codeception\Test\Unit { /** * @var \UnitTester */ protected $tester; protected function _before() { } protected function _after() { } // tests public function testSomeFeature() { } }
You might need to add the correct namespace for tests and models:
<?php namespace test\models; use app\models\User; // to access User model use app\models\Address; // to access Address model //...
Troubleshooting
When running a unit test, you get “Fatal error: Class 'tests\codeception\unit\models\TestCase' not found in C:\wamp\www\myapp\tests\codeception\unit\models\ContactFormTest.php on line 11”, you are probably running an out of date version of tests suite and/or Codeception.
You need to upgrade to yii-basic-template
2.0.12 or higher, and Codeception to 2.3.6 or higher. Follow these steps:
- Upgrade Codeception. Edit
[app]/composer.json
to be:Add support for codeception/codeception to section require-dev in app/composer.json: "require-dev": { "codeception/base": "^2.2.3", "codeception/codeception": "2.3.*", "codeception/specify": "*", "codeception/verify": "*", ... }
- Delete
[app]/tests
folder. - Add a copy of
[app]/tests
folder from new download. - Add a copy of
[app]/codeception.yml
file from form new download. - Add a copy of
[app]/config/test.php
and[app]/config/test_db.php
files from form new download. - Edit file
[app]/web/index-test.php
to look as follows:<?php // NOTE: Make sure this file is not accessible when deployed to production if (!in_array(@$_SERVER['REMOTE_ADDR'], ['127.0.0.1', '::1'])) { die('You are not allowed to access this file.'); } defined('YII_DEBUG') or define('YII_DEBUG', true); defined('YII_ENV') or define('YII_ENV', 'test'); require(__DIR__ . '/../vendor/autoload.php'); require(__DIR__ . '/../vendor/yiisoft/yii2/Yii.php'); // Remove: //$config = require(__DIR__ . '/../tests/codeception/config/acceptance.php'); // Add: $config = require(__DIR__ . '/../config/test.php'); (new yii\web\Application($config))->run();
Debugging
Output messages:
// Statement to output message to Codeception debugger codecept_debug("debug message");
Run codecept in debug mode. Eg:
svr$ ./vendor/bin/codecept run unit --debug
Test Specification
Traditional Testing
Assertions
Uses some of these most common asserts:
$this->assertEquals() $this->assertContains() $this->assertFalse() $this->assertTrue() $this->assertNull() $this->assertEmpty()
Specify (BDD style)
For BDD (Behavior Driven Development), add the specifications first, then build the tests into each spec.
<?php // this is just a PHPUnit's testcase class PostTest extends PHPUnit_Framework_TestCase { use Codeception\Specify; // just a regular test declaration public function testPublication() { $this->specify('post can be published'); $this->specify('post should contain a title'); $this->specify('post should contain a body'); $this->specify('author of post should not be banned'); } } ?>
Add asserts under each spec. Group specs under a single behavior test (following BDD convention).
<?php // this is just a PHPUnit's testcase class PostTest extends PHPUnit_Framework_TestCase { use Codeception\Specify; // just a regular test declaration public function testPublication() { $this->post = new Post; $this->post->setAuthor(new User()); $this->specify('post can be published', function() { $this->post->setTitle('Testing is Fun!'); $this->post->setBody('Thats for sure'); $this->assertTrue($this->post->publish()); }); $this->specify('post should contain a title', function() { $this->assertFalse($this->post->publish()); $this->assertArrayHasKey('title', $this->post->errors()); }); $this->specify('post should contain a body', function() { $this->assertFalse($this->post->publish()); $this->assertArrayHasKey('body', $this->post->errors()); }); $this->specify('author of post should not be banned', function() { $this->post->getAuthor()->setIsBanned(true); $this->post->setTitle('Testing is Fun!'); $this->post->setBody('Thats for sure'); $this->assertFalse($this->post->publish()); $this->assertArrayHasKey('author', $this->post->errors()); }); } } ?>
Verify (BDD Style)
Change the asserts to follow the expect(XX)→toBe(YY)
style, more in line with BDD.
<?php // this is just a PHPUnit's testcase class PostTest extends PHPUnit_Framework_TestCase { use Codeception\Specify; // just a regular test declaration public function testPublication() { $this->post = new Post; $this->post->setAuthor(new User()); $this->specify('post can be published', function() { $this->post->setTitle('Testing is Fun!'); $this->post->setBody('Thats for sure'); expect_that($this->post->publish()); }); $this->specify('post should contain a title', function() { expect_not($this->post->publish()); expect($this->post->errors())->hasKey('title'); }); $this->specify('post should contain a body', function() { expect_not($this->post->publish()); expect($this->post->errors())->hasKey('body'); }); $this->specify('author of post should not be banned', function() { $this->post->getAuthor()->setIsBanned(true); $this->post->setTitle('Testing is Fun!'); $this->post->setBody('Thats for sure'); expect_not($this->post->publish()); expect($this->post->errors())->hasKey('author'); }); } } ?>
What Test Style to Use?
What style to use depends on what needs testing. For features, use BDD features scenarios (i.e. anything that bring business value). For the rest (such as regression tests or negative scenario tests) which have no direct business value, use Cept/Cest/Test formats.
Fixtures
Fixtures help generate data models that can be used for testing. Create a fixture and place it in @app/tests/fixtures/
. Eg: File @app/tests/fixtures/PriceFixture.php
:
<?php namespace app\tests\fixtures; use yii\test\ActiveFixture; class PriceFixture extends ActiveFixture { public $modelClass = 'app\models\Price'; }
Add fixture data file. The data file should return an array of data rows to be inserted into the respective table. Eg: @tests/fixtures/data/price.php
(or @tests/_data/price.php
if using Codeception). Notice the optional alias for each row, i.e. price1
, price2
, etc.
<?php return [ 'price1' => [ 'item_code' => 'TABLE_BLACK', 'description' => 'Black Table', 'cost' => '120', ], 'price2' => [ 'item_code' => 'TABLE_BEIGE', 'description' => 'Beige Table', 'cost' => '110', ], ];
Include fixtures in Codeception unit test. Eg: File @app/tests/unit/models/PriceTest.php
:
<?php namespace models; use app\models\Price; use app\tests\fixtures\PriceFixture; class PriceTest extends \Codeception\Test\Unit { /** * @var \UnitTester */ protected $tester; public function _fixtures() { return [ 'prices' => [ // <-- alias for this data 'class' => PriceFixture::className(), // Codeception fixture data located in @tests/_data/price.php // Default is usually @tests/unit/fixtures/data/price.php 'dataFile' => codecept_data_dir() . 'price.php' ], ]; } //... } ?>
Source: Yii 2 Fixtures
Fixtures: Generating Data with Faker
Install yii2-faker
extension (comes preinstalled with Yii2). Add the following to the @app/config/console.php
file:
'controllerMap' => [ 'fixture' => [ 'class' => 'yii\faker\FixtureController', 'templatePath' => '@app/tests/unit/templates/fixtures', //'fixtureDataPath' => '@app/tests/fixtures/data', 'fixtureDataPath' => '@app/tests/_data', ], ],
Usage
In the unit test (or any part of the application), you can use faker like this:
// use the factory to create a Faker\Generator instance $faker = \Faker\Factory::create(); // generate data by accessing properties echo $faker->name; echo $faker->address; echo $faker->text;
Get more generators from: https://github.com/fzaninotto/Faker
Template for Fixtures
If you need unit tests to load faker data using fixtures, create a fixture template under @tests/unit/templates/fixtures
. Eg: File users.php
:
<?php // users.php file under the template path (by default @tests/unit/templates/fixtures) /** * @var $faker \Faker\Generator * @var $index integer */ return [ 'name' => $faker->firstName, 'phone' => $faker->phoneNumber, 'city' => $faker->city, 'password' => Yii::$app->getSecurity()->generatePasswordHash('password_' . $index), 'auth_key' => Yii::$app->getSecurity()->generateRandomString(), 'intro' => $faker->sentence(7, true), // generate a sentence with 7 words ];
Call fixture from unit test:
public function _fixtures() { return [ 'profiles' => [ // alias for this data 'class' => UserFixture::className(), // Codeception fixture data located in @tests/_data/users.php 'dataFile' => codecept_data_dir() . 'users.php' ], ]; }
Generate fixture data
With a template file, generate fixtures using any of the following commands.
Generate fixtures from users fixture template
$ yii fixture/generate users
Generate several fixture data files. Eg: users
is a template name. The command generates a new file with the same template name under the fixture path (@tests/unit/fixtures
folder).
$ yii fixture/generate users profiles teams
This command will generate fixtures for all template files that are stored under the template path and store fixtures under the fixtures path with file names same as templates names.
$ yii fixture/generate-all
Generate N number of fixtures per file
$ yii fixture/generate-all --count=3
Generate fixtures in russian language
$ yii fixture/generate users --count=5 --language="ru_RU"
Read templates from the other path
$ yii fixture/generate-all --templatePath='@app/path/to/my/custom/templates'
Generate fixtures into other directory.
$ yii fixture/generate-all --fixtureDataPath='@tests/acceptance/fixtures/data'
Test Doubles
These are the common test doubles (Source: https://martinfowler.com/articles/mocksArentStubs.html):
- Dummy objects are passed around but never actually used. Usually they are just used to fill parameter lists.
- Fake objects actually have working implementations, but usually take some shortcut which makes them not suitable for production (an in memory database is a good example).
- Stubs provide canned answers to calls made during the test, usually not responding at all to anything outside what's programmed in for the test.
- Spies are stubs that also record some information based on how they were called. One form of this might be an email service that records how many messages it was sent.
- Mocks are what we are talking about here: objects pre-programmed with expectations which form a specification of the calls they are expected to receive.
Check
Test Examples
Comments
$I->wantTo('query the customer info using his phone number'); $I->expectTo('see query result');
Users
// Login $I->amLoggedInAs(\app\models\User::findByUsername('admin')); $I->amOnPage(['site/index']); $I->see('Logout'); // Logout \Yii::$app->user->logout(); $I->amOnPage(['site/index']); $I->see('Signup'); $I->see('Login');