Get a head start on your PHP testing with Codeception

Before we delve into Codeception and PHP, let’s establish a foundational understanding of why testing is crucial in application development. You might wonder if it’s possible to skip testing, at least for this particular project.

It’s true that not everything requires rigorous testing, like a basic homepage or a site with static pages connected by a simple router.

However, testing becomes indispensable when:

  • Your team employs BDD/TDD methodologies.
  • Your Git repository has a substantial commit history.
  • You are a dedicated professional engaged in a serious project.

You might argue that a separate testing department handles these tasks. However, consider the time spent on bug fixing after implementing new features without thorough testing.

Addressing Challenges Through Testing

Let’s define the problems testing can solve. While it won’t eliminate all errors, it establishes expected behavior through test cases. However, errors can exist within test cases, and poorly written code remains problematic even with tests.

Testing ensures that future code changes (bug fixes or new features) align with the defined behavior outlined in the tests. Well-written tests can even serve as documentation by demonstrating typical scenarios and expected outcomes. Testing is a small but vital investment in your project’s future.

Various testing types are available:

Basic unit tests usually aren't enough. They need to be backed by integrational, functional, and acceptance testing.
Basic unit tests usually aren't enough. They need to be backed by integrational, functional, and acceptance testing.
  • Unit tests: These granular tests examine small code segments, such as individual class methods, in isolation.
  • Integration tests: These tests verify how different parts of your application, like several classes or methods related to a single feature, interact.
  • Functional tests: These tests evaluate specific application requests, including browser responses, database interactions, and more.
  • Acceptance tests: These tests typically ensure that the application fulfills all client requirements.

Imagine a building analogy. Unit tests are like testing individual bricks for strength, size, and shape. Integration tests check how well these bricks fit together to form walls. Functional tests examine a wall’s ability to provide weather protection and allow sunlight. Finally, acceptance tests evaluate the entire building’s functionality – opening doors, using lights, accessing different floors.

Introducing Codeception

It’s important to note that these divisions are not always absolute, and sometimes, different test types might overlap.

Many developers rely solely on unit tests. I was once one of them, finding multiple testing systems for different purposes cumbersome. However, I sought a better solution than PHPUnit, something to enhance my testing practices without excessive overhead. That’s when I discovered Codeception. Initially skeptical, I found it to be a remarkably useful and powerful system after some exploration.

Installing Codeception is straightforward:

1
2
$ composer require "codeception/codeception"
$ php vendor/bin/codecept bootstrap

This creates a “tests” folder with subfolders for acceptance, functional, and unit tests. Let’s start by adding a standard “Hello World” acceptance test.

1
$ php vendor/bin/codecept generate:cept acceptance HelloWorld

This generates tests/acceptance/HelloWorldCept.php:

1
2
3
<?php
$I = new AcceptanceTester($scenario);
$I->wantTo('perform actions and see result');

The default $I variable represents the “tester”. It interacts with your website or application, performing actions and reporting the results, highlighting successes and failures. This object, with methods like wantTo(), see(), and amOnPage(), embodies the tester’s role.

Let’s approach testing a page’s functionality like a tester. A basic check involves opening the page and searching for a specific phrase to confirm its accessibility.

This is simple:

1
2
3
<?php
$I->amOnPage('/');
$I->see('Welcome');

To run the test:

1
$ php vendor/bin/codecept run

An error appears. While lengthy at first glance, closer inspection reveals the issue.

Whops! Something went wrong. That's the whole point of testing. Check out the message, identify the error, and learn from your mistakes.
Whops! Something went wrong. That's the whole point of testing. Check out the message, identify the error, and learn from your mistakes.

The Acceptance test encountered an error:

1
2
3
4
5
6
Acceptance Tests (1)
Perform actions and see result (HelloWorldCept)                                                             Error

----------
1) Failed to perform actions and see result in HelloWorldCept (tests/acceptance/HelloWorldCept .php)
[GuzzleHttp\Exception\ConnectException] cURL error 6: Could not resolve host: localhost (see http://curl.haxx.se/libcurl/c/libcurl-errors.html) 

The problem lies in the unavailable “localhost”.

Here’s the test scenario breakdown:

1
 1. $I->amOnPage("/")

In tests/acceptance.suite.yml, replace url: http://localhost/towith an accessible URL, like your local test host:url: https://local.codeception-article.com/`.

Rerun the test:

1
2
Acceptance Tests (1) ---------------------------------------------------------------------------------------
Perform actions and   result (HelloWorldCept)                                                             Ok

Success! Our first test passes.

While this example focuses on amOnPage(), Codeception offers various testing methods:

  • Page interaction: fillField(), selectOption(), submitForm(), click()
  • Assertions: see(), dontSee(), seeElement(), seeInCurrentUrl(), seeCheckboxIsChecked(), seeInField(), seeLink(). These methods have counterparts with suffixes that don’t halt the test if an element isn’t found.
  • Cookie handling: setCookie(), grabCookie(), seeCookie()
  • Test scenario commenting/description: amGoingTo(), wantTo(), expect(). These enhance test readability and clarity.

Testing a password reset email page could look like this:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
<?php
$I = new AcceptanceTester($scenario);
$I->wantTo('Test forgotten password functionality');
$I->amOnPage('/forgotten')
$I->see('Enter email');
$I->fillField('email', 'incorrect@email.com');
$I->click('Continue');
$I->expect('Reset password link not sent for incorrect email');
$I->see('Email is incorrect, try again');
$I->amGoingTo('Fill correct email and get link');
$I->see('Enter email');
$I->fillField('email', 'correct@email.com');
$I->click('Continue');
$I->expect('Reset password link sent for correct email');
$I->see('Please check your email for next instructions');

This seems sufficient, but what about Ajax-loaded content? Codeception uses PhpBrowser, built on Symfony BrowserKit and Guzzle, by default. It’s simple, fast, and only requires curl.

However, you can opt for Selenium and real browser testing for JavaScript interactions. While slower, it provides more comprehensive testing.

After installing the Selenium driver, modifying acceptance.suite.yml, and rebuilding the AcceptanceTester class, you can leverage methods like wait() and waitForElement(). Additionally, you can optimize testing by saving and loading session states using saveSessionSnapshot() and loadSessionSnapshot(). This is particularly useful for scenarios like testing authorization flows.

This demonstrates Codeception’s simple yet powerful testing capabilities.

Functional Testing

Let’s move on to functional testing.

1
$ php vendor/bin/codecept generate:cept functional HelloWorld

This yields:

1
2
3
4
<?php
$I = new FunctionalTester($scenario);
$I->amOnPage('/');
$I->see('Welcome');

Functional tests resemble integration tests but directly interact with your application. This eliminates the need for a webserver during testing and provides greater flexibility in testing different application parts.

While framework support isn’t universal, Codeception supports major frameworks like Symfony, Silex, Phalcon, Yii, Zend Framework, Lumen, and Laravel, catering to most needs. Consult Codeception’s module documentation for available functions and activate them in functional.suite.yml.

Codeception supports major frameworks: Symfony, Silex, Phalcon, Yii, Zend Framework, Lumen, Laravel.
Codeception supports major frameworks: Symfony, Silex, Phalcon, Yii, Zend Framework, Lumen, Laravel.

Before delving into unit testing, let’s briefly discuss test creation. Our examples used the “cept” format:

1
$ php vendor/bin/codecept generate: cept acceptance HelloWorld

Alternatively, “cest” tests allow structuring related scenarios within a class:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
$ php vendor/bin/codecept generate:cest acceptance HelloWorld
<?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');
    }
}

Here, _before() and _after() methods run before and after each test. Each test receives an AcceptanceTester instance for use. This style proves beneficial in specific situations.

Unit Testing

Let’s explore unit testing.

Built on PHPUnit, Codeception supports PHPUnit tests. Add new PHPUnit tests like this:

1
$ php vendor/bin/codecept generate:phpunit unit HelloWorld

Alternatively, inherit from \PHPUnit_Framework_TestCase.

For more advanced features, use Codeception’s unit tests:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
$ php vendor/bin/codecept generate:test unit HelloWorld
<?php

class HelloWorldTest extends \Codeception\TestCase\Test
{
    /**
     * @var \UnitTester
     */
    protected $tester;

    protected function _before()
    {
    }

    protected function _after()
    {
    }

    // tests
    public function testUserSave()
    {
	$user = User::find(1);
	$user->setEmail('correct@email.com');
	$user->save();
	$user = User::find(1);
	$this->assertEquals('correct@email.com', $user->getEmail());
    }
}

The familiar _before() and _after() methods act as setUp() and tearDown() counterparts.

The key advantage lies in extending testing with modules enabled in unit.suite.yml:

  • Access to memcache and databases (MySQL, SQLite, PostgreSQL, MongoDB) for tracking changes
  • REST/SOAP application testing
  • Queue testing

Each module has unique features, so consult the documentation beforehand.

Furthermore, the Codeception/Specify package (added to composer.json) enables descriptive test writing:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
<?php 
class HelloWorldTest extends \Codeception\TestCase\Test
{
    use \Codeception\Specify;
    private $user;
    protected function _before()
   {
	$this->user = User::find(1);
   }
    public function testUserEmailSave()
    {
	$this->specify("email can be stored", function() {
            	$this->user->setEmail('correct@email.com');
		$this->user->save();
		$user = User::find(1);
            	$this->assertEquals('correct@email.com', $user->getEmail());
        	});
    }
}

PHP code within these closures is isolated, preventing interference. Descriptions enhance test readability and facilitate identifying failed tests.

Optionally, use Codeception\Verify for BDD-style syntax:

1
2
3
4
5
<?php
public function testUserEmailSave()
{
	verify($map->getEmail())->equals('correct@email.com');
}

Stubbing is also supported:

1
2
3
4
5
6
<?php
public function testUserEmailSave()
{
        $user = Stub::make('User', ['getEmail' => 'correct@email.com']);
        $this->assertEquals('correct@email.com', $user->getEmail());
}

Conclusion: Codeception for Efficient Testing

What does Codeception offer, and who benefits? Are there any drawbacks?

Codeception can be employed by developers with vastly different PHP profficiency levels and teams of all sizes.
Codeception can be employed by developers with vastly different PHP profficiency levels and teams of all sizes.

Codeception suits diverse teams, from small to large, beginners to experts, and those using popular frameworks or none at all.

Ultimately, Codeception is production-ready.

It’s a mature, well-documented framework, easily extensible through modules. It’s modern yet built on the reliable PHPUnit, appealing to developers seeking stability.

It’s efficient, requiring minimal time and effort. It’s relatively easy to learn, aided by comprehensive documentation.

Installation and configuration are straightforward, yet it offers advanced options for those who need them. Start with the basics, and explore advanced features as needed.

Licensed under CC BY-NC-SA 4.0