Advertisement
  1. Code
  2. PHP
  3. Yii

Programming With Yii2: Automated Testing With Codeception

Scroll to top
Read Time: 14 min
This post is part of a series called How to Program With Yii2.
Programming With Yii2: Security
How to Program With Yii2: ActiveRecord
Final product imageFinal product imageFinal product image
What You'll Be Creating

If you're asking, "What's Yii?" check out my earlier tutorial, Introduction to the Yii Framework, which reviews the benefits of Yii and includes an overview of what's new in Yii 2.0, released in October 2014.

In this Programming With Yii2 series, I'm guiding readers in use of the Yii2 Framework for PHP. In this tutorial, I'll explore automated testing using Codeception, which is integrated with the Yii2 development framework.

Admittedly, my experience writing tests with my code is scarce. I've often been part of solo or small projects with limited resources. During my time at Microsoft, we had distinct test teams that did this. But frankly, this is likely typical of you too, right? Programmers like to code, they don't write tests—at least old school programmers didn't.

Codeception is an innovative library that literally aims to make writing tests fun and easy. And, I'd say they've succeeded to a reasonable degree. As I dipped my toe into the water of "Lake Codeception", it was mostly easy and fun. However, as I began to go deeper, I ran into configuration issues with Yii and the specific modules used in this series. There were definitely challenges. Overall, though, I'm impressed and see the benefit of learning more.

Put simply, Codeception and its integration with Yii make me want to write more tests, a first for me. I suspect you'll have a similar experience. 

A little reminder before we get started, I do participate in the comment threads below. I'm especially interested if you have additional thoughts or want to suggest topics for future tutorials. If you have a question or topic suggestion, please post below. You can also reach me on Twitter @reifman directly.

Getting Started

Installing Codeception

To guide me, I used Yii's Testing Environment Setup documentation. I began with a global install of codeception so I could use it from any project.

1
$ composer global require "codeception/codeception=2.1.*"
2
Changed current directory to /Users/Jeff/.composer
3
./composer.json has been updated
4
Loading composer repositories with package information
5
Updating dependencies (including require-dev)
6
  - Installing symfony/yaml (v3.1.1)
7
    Loading from cache
8
...
9
codeception/codeception suggests installing symfony/phpunit-bridge (For phpunit-bridge support)
10
Writing lock file
11
Generating autoload files

You also need to require codeception/specify:

1
$ composer global require "codeception/specify=*"
2
Changed current directory to /Users/Jeff/.composer
3
./composer.json has been updated
4
Loading composer repositories with package information
5
Updating dependencies (including require-dev)
6
composer require "codeception/verify=*"
7
  - Installing codeception/specify (0.4.3)
8
    Downloading: 100%         
9
10
Writing lock file
11
Generating autoload files

And codeception/verify:

1
$ composer require "codeception/verify=*"
2
./composer.json has been updated
3
Loading composer repositories with package information
4
Updating dependencies (including require-dev)
5
  - Installing codeception/verify (0.3.0)                  
6
    Downloading: 100%         
7
8
Writing lock file
9
Generating autoload files

Next, it helps to set up an alias for codecept using your global composer directory:

1
$ composer global status
2
Changed current directory to /Users/Jeff/.composer
3
No local changes

This sets the alias:

1
$ alias codecept="/Users/Jeff/.composer/vendor/bin/codecept"

Yii also requires you install Faker, which generates fake testing data for your application:

1
$ composer require --dev yiisoft/yii2-faker:*
2
./composer.json has been updated
3
Loading composer repositories with package information
4
Updating dependencies (including require-dev)
5
Nothing to install or update                               
6
Generating autoload files

Setting Up Codeception With Your Application

Codecept bootstrap initializes codeception for your Yii application, creating a variety of configuration files for building and running tests against your application. We're using the Hello application from this series for this tutorial. See the GitHub link on this page to get the code.

1
$ codecept bootstrap
2
 Initializing Codeception in /Users/Jeff/Sites/hello 
3
4
File codeception.yml created       <- global configuration
5
tests/unit created                 <- unit tests
6
tests/unit.suite.yml written       <- unit tests suite configuration
7
tests/functional created           <- functional tests
8
tests/functional.suite.yml written <- functional tests suite configuration
9
tests/acceptance created           <- acceptance tests
10
tests/acceptance.suite.yml written <- acceptance tests suite configuration
11
tests/_output was added to .gitignore
12
 --- 
13
tests/_bootstrap.php written <- global bootstrap file
14
Building initial Tester classes
15
Building Actor classes for suites: acceptance, functional, unit
16
 -> AcceptanceTesterActions.php generated successfully. 0 methods added
17
\AcceptanceTester includes modules: PhpBrowser, \Helper\Acceptance
18
AcceptanceTester.php created.
19
 -> FunctionalTesterActions.php generated successfully. 0 methods added
20
\FunctionalTester includes modules: \Helper\Functional
21
FunctionalTester.php created.
22
 -> UnitTesterActions.php generated successfully. 0 methods added
23
\UnitTester includes modules: Asserts, \Helper\Unit
24
UnitTester.php created.
25
26
Bootstrap is done. Check out /Users/Jeff/Sites/hello/tests directory

For some reason, I also ended up with duplicate testing directories in hello/tests; just deleting hello/tests/functional, hello/tests/acceptance, and hello/tests/unit cleared things up. All the tests live in hello/tests/codeception/*.

The Different Kinds of Tests

Codeception is focused on three kinds of tests:

  1. Unit testing verifies that specific units are working, such as an exhaustive test of all your model's methods.
  2. Functional testing verifies common application scenarios as if a user was acting them out, but using web browser emulation.
  3. Acceptance testing is identical to functional testing but actually runs the tests through a web browser.

And it supports three different kinds of test formats for your testing code:

  1. Cept: it's the simplest single scenario test file
  2. Cest: an object oriented format for running multiple tests within a single file
  3. Test: tests written on PHPUnit, a PHP testing framework

Let's begin with an example of acceptance tests using cept format:

Acceptance Testing

We'll use Codeception's Welcome test example first.

1
$ codecept generate:cept acceptance Welcome
2
Test was created in /Users/Jeff/Sites/hello/tests/acceptance/WelcomeCept.php

This generates tests/acceptance/WelcomeCept.php, which we'll edit below.

Since acceptance tests require the browser, we have to edit /tests/acceptance.suite.yml in our project to provide our development URL, http://localhost:8888/hello:

1
# Codeception Test Suite Configuration
2
#
3
# Suite for acceptance tests.
4
# Perform tests in browser using the WebDriver or PhpBrowser.
5
# If you need both WebDriver and PHPBrowser tests - create a separate suite.
6
7
class_name: AcceptanceTester
8
modules:
9
    enabled:
10
        - PhpBrowser:
11
            url: http://localhost:8888/hello/
12
        - \Helper\Acceptance

Now, we're ready to modify the initial test in tests/acceptance/WelcomeCept.php. I'm writing a test that loads the front page to make sure that it works as expected. 

Codeception tests have the concept of an actor, in this case, $I = new AcceptanceTester().

Here's how it describes actors in the Codeception documentation:

We have a UnitTester, who executes functions and tests the code. We also have a FunctionalTester, a qualified tester, who tests the application as a whole, with knowledge of its internals. And an AcceptanceTester, a user that works with our application through an interface that we provide.

You can comment your tests with code, such as $I->wantTo('perform a certain test') or 'ensure that the frontpage works'.

In my test, I want to see $I->see text for 'Congratulations!' and 'Yii-powered':

1
<?php
2
$I = new AcceptanceTester($scenario);
3
$I->wantTo('ensure that frontpage works');
4
$I->amOnPage('/');
5
$I->see('Congratulations!');
6
$I->see('Yii-powered');

Here's the current Hello home page:

Programming with Yii - The Hello Home PageProgramming with Yii - The Hello Home PageProgramming with Yii - The Hello Home Page

Next, let's run the test, simply codecept run:

1
$ codecept run
2
Codeception PHP Testing Framework v2.1.11
3
Powered by PHPUnit 5.3.5 by Sebastian Bergmann and contributors.
4
Acceptance Tests (1) ---------------------------------------------------------------------------------------
5
Ensure that frontpage works (WelcomeCept)                                                             Ok
6
------------------------------------------------------------------------------------------------------------
7
Functional Tests (0) ------------------------
8
---------------------------------------------
9
Unit Tests (0) ------------------------------
10
---------------------------------------------
11
Time: 554 ms, Memory: 10.25MB
12
OK (1 test, 2 assertions)

As you can see, our test passed, and the code to verify this functionality was quite readable and simple.

Notes on Yii's Default Tests

To go further, I began using Yii's default tests. At this point, I ran into a number of configuration issues—most due to my use of the custom yii2-user module in this series. Others were due to small bugs with Yii, which its team has been quick to respond to and fix when reported on GitHub; in some cases, issues had been fixed in later releases of the yii2-basic tree.

Also, because I'd updated the yii2-basic tree for this series, I had to make small changes to some of the default tests.

Here's an example of the output for running the default acceptance tests once I'd made some minor adjustments:

1
$ codecept run
2
Codeception PHP Testing Framework v2.1.11
3
Powered by PHPUnit 5.3.5 by Sebastian Bergmann and contributors.
4
5
Acceptance Tests (4) -------------------------------------------------------------------------------------------------
6
Ensure that about works (AboutCept)                                                                             Ok
7
Ensure that contact works (ContactCept)                                                                         Ok
8
Ensure that home page works (HomeCept)                                                                          Ok
9
Ensure that login works (LoginCept)                                                                             Ok
10
----------------------------------------------------------------------------------------------------------------------

Functional Testing

To get functional tests to work, I needed to run an instance of Yii's built-in server. I hadn't known about this component until Yii's Alex Makarov mentioned it in our GitHub exchange.

1
$ ./yii serve

I made small changes to the functional tests in /tests/codeception/functional, mostly to look for my specific updated text strings, i.e. "Invalid login or password" in place of Yii's default. Here's a look at LoginCept.php:

1
<?php
2
use tests\codeception\_pages\LoginPage;
3
$I = new FunctionalTester($scenario);
4
5
$I->wantTo('ensure that login works');
6
7
$loginPage = LoginPage::openBy($I);
8
$I->see('Login');
9
$I->amGoingTo('try to login with empty credentials');
10
$loginPage->login('', '');
11
$I->expectTo('see validations errors');
12
$I->see('Login cannot be blank.');
13
$I->see('Password cannot be blank.');
14
15
$I->amGoingTo('try to login with wrong credentials');
16
$loginPage->login('admin', 'wrong');
17
$I->expectTo('see validations errors');
18
$I->see('Invalid login or password');
19
20
$I->amGoingTo('try to login with correct credentials');
21
$loginPage->login('admin', 'admin11');
22
$I->expectTo('see user info');
23
$I->see('Logout');

Basically, the code accesses the LoginForm model and tests its various methods using Yii serve.

Here's the /tests/codeception_pages/LoginPage.php testing code it's leveraging (I also had to modify it for changes we've made to the series):

1
class LoginPage extends BasePage
2
{
3
    public $route = 'user/login';
4
5
    /**

6
     * @param string $username

7
     * @param string $password

8
     */
9
    public function login($username, $password)
10
    {
11
        $this->actor->fillField('input[name="login-form[login]"]', $username);
12
        $this->actor->fillField('input[name="login-form[password]"]', $password);
13
        $this->actor->click('button[type=submit]');
14
    }
15
}

You can see that we're coding the actor to fillFields and click buttons for our updated form fields. 

While troubleshooting my Codeception integration with Yii, I found it helpful to run these tests in verbose mode:

1
$ codecept run -vvv

Here's the verbose output from the Login functional tests—in MacOS Terminal, PASSED and FAILED are color coded red or pink and inverted for visibility:

1
Functional Tests (4) -------------------------------------------------------------------------------------------------
2
Modules: Filesystem, Yii2
3
...
4
----------------------------------------------------------------------------------------------------------------------
5
Ensure that login works (LoginCept)
6
Scenario:
7
* I am on page "/index-test.php/user/login"
8
9
  [Page] /index-test.php/user/login
10
  [Response] 200
11
  [Request Cookies] []
12
  [Response Headers] {"content-type":["text/html; charset=UTF-8"]}
13
* I see "Login"
14
* I am going to try to login with empty credentials
15
* I fill field "input[name="login-form[login]"]",""
16
* I fill field "input[name="login-form[password]"]",""
17
* I click "button[type=submit]"
18
  [Uri] http://localhost/index-test.php/user/login
19
  [Method] POST
20
  [Parameters] {"_csrf":"VEpvcmk3bVgFH1Y9AVsmYWQQDEouTSggYXMFGStdKBEnCyQfBxo8Bw==","login-form[login]":"","login-form[password]":""}
21
  [Page] http://localhost/index-test.php/user/login
22
  [Response] 200
23
  [Request Cookies] {"_csrf":"dd395a9e5e3c08cfb1615dae5fc7b5ba0a2025c003e430ba0139b300f4a917ada:2:{i:0;s:5:"_csrf";i:1;s:32:"QU9OhlK90Zc8GzEx59jkBjEIsAKmn-Q_";}"}
24
  [Response Headers] {"content-type":["text/html; charset=UTF-8"]}
25
* I expect to see validations errors
26
* I see "Login cannot be blank."
27
* I see "Password cannot be blank."
28
* I am going to try to login with wrong credentials
29
* I fill field "input[name="login-form[login]"]","admin"
30
* I fill field "input[name="login-form[password]"]","wrong"
31
* I click "button[type=submit]"
32
  [Uri] http://localhost/index-test.php/user/login
33
  [Method] POST
34
  [Parameters] {"_csrf":"QjFBRl9hMjMTZHgJNw15CnJrIn4YG3dLdwgrLR0Ld3oxcAorMUxjbA==","login-form[login]":"admin","login-form[password]":"wrong"}
35
  [Page] http://localhost/index-test.php/user/login
36
  [Response] 200
37
  [Request Cookies] {"_csrf":"dd395a9e5e3c08cfb1615dae5fc7b5ba0a2025c003e430ba0139b300f4a917ada:2:{i:0;s:5:"_csrf";i:1;s:32:"QU9OhlK90Zc8GzEx59jkBjEIsAKmn-Q_";}"}
38
  [Response Headers] {"content-type":["text/html; charset=UTF-8"]}
39
* I expect to see validations errors
40
* I see "Invalid login or password"
41
* I am going to try to login with correct credentials
42
* I fill field "input[name="login-form[login]"]","admin"
43
* I fill field "input[name="login-form[password]"]","admin11"
44
* I click "button[type=submit]"
45
  [Uri] http://localhost/index-test.php/user/login
46
  [Method] POST
47
  [Parameters] {"_csrf":"bG8uMXdPYk49Ohd.HyMpd1w1TQkwNSc2WVZEWjUlJwcfLmVcGWIzEQ==","login-form[login]":"admin","login-form[password]":"admin11"}
48
  [Headers] {"location":["http://localhost/index-test.php"],"content-type":["text/html; charset=UTF-8"]}
49
  [Page] http://localhost/index-test.php/user/login
50
  [Response] 302
51
  [Request Cookies] {"_csrf":"dd395a9e5e3c08cfb1615dae5fc7b5ba0a2025c003e430ba0139b300f4a917ada:2:{i:0;s:5:"_csrf";i:1;s:32:"QU9OhlK90Zc8GzEx59jkBjEIsAKmn-Q_";}"}
52
  [Response Headers] {"location":["http://localhost/index-test.php"],"content-type":["text/html; charset=UTF-8"]}
53
  [Redirecting to] http://localhost/index-test.php
54
  [Page] http://localhost/index-test.php
55
  [Response] 200
56
  [Request Cookies] {"_csrf":"dd395a9e5e3c08cfb1615dae5fc7b5ba0a2025c003e430ba0139b300f4a917ada:2:{i:0;s:5:"_csrf";i:1;s:32:"QU9OhlK90Zc8GzEx59jkBjEIsAKmn-Q_";}"}
57
  [Response Headers] {"content-type":["text/html; charset=UTF-8"]}
58
* I expect to see user info
59
* I see "Logout"
60
 PASSED 

Overall, there's a bit to learn to get started with Codeception and properly code your tests. But the results are impressive and helpful.

Unit Testing

Basically, unit tests are programmatic testing of our infrastructure and models. Ideally, we would write tests for every method and usage variation of our models.

Unfortunately, I was not able to get unit tests to work within our tree because of either small Yii bugs yet to be released or configuration issues between Codeception and yii2-user which we integrated in How to Program With Yii2: Integrating User Registration

1
Unit Tests (3) ----------------------------------------------------------------------------------------------------
2
Modules: 
3
-------------------------------------------------------------------------------------------------------------------
4
Trying to test login no user (tests\codeception\unit\models\LoginFormTest::testLoginNoUser)... 
5
<pre>PHP Fatal Error &#039;yii\base\ErrorException&#039; with message &#039;Call to undefined function

6
tests\codeception\unit\models\expect()&#039; 

I'll address unit testing again in our Startup series which does not use yii2-user but instead uses the Yii Advanced tree's built-in user integration.

Let's look at a couple of examples from the Yii2-app-basic tree.

Testing Contact Form Emails

The hello/tests/codeception/unit/models/ContactFormTest.php tests sending an email through programmatic use of models:

1
<?php
2
3
namespace tests\codeception\unit\models;
4
5
use Yii;
6
use yii\codeception\TestCase;
7
use Codeception\Specify;
8
9
class ContactFormTest extends TestCase
10
{
11
    use Specify;
12
13
    protected function setUp()
14
    {
15
        parent::setUp();
16
        Yii::$app->mailer->fileTransportCallback = function ($mailer, $message) {
17
            return 'testing_message.eml';
18
        };
19
    }
20
21
    protected function tearDown()
22
    {
23
        unlink($this->getMessageFile());
24
        parent::tearDown();
25
    }
26
27
    public function testContact()
28
    {
29
        $model = $this->createMock('app\models\ContactForm', ['validate']);
30
        $model->expects($this->once())->method('validate')->will($this->returnValue(true));
31
32
        $model->attributes = [
33
            'name' => 'Tester',
34
            'email' => 'tester@example.com',
35
            'subject' => 'very important letter subject',
36
            'body' => 'body of current message',
37
        ];
38
39
        $model->contact('admin@example.com');
40
        
41
        $this->specify('email should be send', function () {
42
            expect('email file should exist', file_exists($this->getMessageFile()))->true();
43
        });
44
45
        $this->specify('message should contain correct data', function () use ($model) {
46
            $emailMessage = file_get_contents($this->getMessageFile());
47
48
            expect('email should contain user name', $emailMessage)->contains($model->name);
49
            expect('email should contain sender email', $emailMessage)->contains($model->email);
50
            expect('email should contain subject', $emailMessage)->contains($model->subject);
51
            expect('email should contain body', $emailMessage)->contains($model->body);
52
        });
53
    }
54
55
    private function getMessageFile()
56
    {
57
        return Yii::getAlias(Yii::$app->mailer->fileTransportPath) . '/testing_message.eml';
58
    }
59
60
}

I was unable to successfully get this test to pass because of a small bug in Yii which hasn't been updated yet (or at least I couldn't find the updated code). My drop of the Yii codebase was naming outbound email with date stamps and the code above was looking for a fixed filename. Thus, it always failed. Still, it's useful to see how programmatic testing can use models to generate a file and then look for that file and validate its contents to verify that code is working.

Testing Login

Let's look at hello/tests/codeception/unit/models/LoginFormTest.php. Again, my use of yii2-user made it overly difficult to integrate at the time of writing this tutorial; however, we can look at the conceptual approach to unit testing user model functions.

Here's testLoginCorrect(), which looks to see if login succeeds with a correct password:

1
public function testLoginCorrect()
2
    {
3
        $model = new LoginForm([
4
            'username' => 'admin',
5
            'password' => 'admin11',
6
        ]);
7
8
        $this->specify('user should be able to login with correct credentials', function () use ($model) {
9
            expect('model should login user', $model->login())->true();
10
            expect('error message should not be set', $model->errors)->hasntKey('password');
11
            expect('user should be logged in', Yii::$app->user->isGuest)->false();
12
        });
13
    }

It uses the LoginForm model to programmatically log in the user, and then it programmatically looks to see if Yii's current user is now no longer a guest.

1
expect('user should be logged in', Yii::$app->user->isGuest)->false();

What's Next?

I hope that you've enjoyed learning about Codeception and its integration with Yii, despite some of the roadblocks I ran into. Default installation of yii2-basic today should perform better.

If you'd like to read more about deciding when and what to test and why, I recommend reading Yii's Testing Overview. There's certainly more to learn about Codeception and writing more complete tests.

Watch for upcoming tutorials in our Programming With Yii2 series as we continue diving into different aspects of the framework. If you'd like to know when the next Yii2 tutorial arrives, follow me @reifman on Twitter or check my instructor page

You may also want to check out our Building Your Startup With PHP series, which is using Yii2's advanced template as we build a real-world application. In fact, you can try out the startup application, Meeting Planner, today.

Related Links

Advertisement
Did you find this post useful?
Want a weekly email summary?
Subscribe below and we’ll send you a weekly email summary of all new Code tutorials. Never miss out on learning about the next big thing.
Advertisement
Looking for something to help kick start your next project?
Envato Market has a range of items for sale to help get you started.