Yii 2 Testing Setup
Setup Environment

Install Local Codeception

  • Remove “yiisoft/yii2-codeception”: “*” from section require-dev in app/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
           #part: [init, orm, email, fixtures]
           configFile: 'config/test.php'
           entryScript: index-test.php
      - Db:
          dsn: "mysql:host=localhost;dbname=mydatabase"
          user: "dbuser"
          password: "dbsecret"
      #- REST
      #    depends: PhpBrowser
      #    url: http://localhost:8080/yii/basic-userdb/web/

[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
      - Yii2:
            part: [orm, email]
            configFile: 'config/test.php'
            entryScript: index-test.php
      #- Db:
      #    dsn: "mysql:host=localhost;dbname=mydatabase"
      #    user: "dbuser"
      #    password: "dbsecret"
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');
		});		
	}
}
?>

Source: New Fashioned Classics: BDD Specs in PHPUnit

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
References