Jumpstart Your PHP Testing with Codeception
Would you like to test your PHP code like a boss? Do you feel that basic unit tests and PHPUnit just don’t cut it anymore? If your answer to both questions is yes, you might want to try Codeception, a mature and well-documented testing framework designed to outperform PHPUnit and Behat.
In this post, Toptal Freelance Software Engineer Vasily Koval describes how he came to take the plunge and start using Codeception, and he explains why you should check out Codeception for your PHP testing needs.
Would you like to test your PHP code like a boss? Do you feel that basic unit tests and PHPUnit just don’t cut it anymore? If your answer to both questions is yes, you might want to try Codeception, a mature and well-documented testing framework designed to outperform PHPUnit and Behat.
In this post, Toptal Freelance Software Engineer Vasily Koval describes how he came to take the plunge and start using Codeception, and he explains why you should check out Codeception for your PHP testing needs.
Vasily is a web developer with more than nine years of extensive experience developing, optimizing, and supporting web applications.
Expertise
Before moving on to Codeception and PHP, we should cover the basics and start by explaining why we need testing in applications in the first place. Perhaps we could complete a project without wasting time on tests, at least this time?
Sure, you don’t need tests for everything; for example, when you want to build yet another homepage. You probably don’t need tests when your project contains static pages linked by one router.
However, you definitely do need testing when:
- Your team uses BDD/TDD.
- Your Git repo contains more than a couple commits.
- You are a proper professional, working on a serious project.
You can excuse yourself by saying that you already have a dedicated test department, a group of people who conduct tests and write new ones when needed. But, can you imagine how long bug fixing will take after you add new functionality to your project?
What Does Testing Solve?
First, let’s decide what sort of problems may be solved through testing. You can’t get rid of all your errors with testing, but you can describe expected behavior in test cases. The errors might be inside your test cases. Spaghetti code remains spaghetti code even when you use test cases.
However, you can be sure that your code will be changed afterward (by fixing errors, or adding new features), so your code still will be free of errors described in the test. Besides, even well-written tests may sometimes be used in documentation because there you can see how typical scenarios unfold and check expected behavior. We can say that testing is a small but crucial investment in the future.
So what sort of tests can we employ?
- Unit tests: Low-level tests that check small pieces of your code - your class’ methods isolated from other code.
- Integrational testing: Integrational tests check a part of your application, they may contain several classes or methods, but should be restricted to one feature. This test should also check how different classes are interacting.
- Functional testing: Tests specific requests to your application: browser response, database changes and so on.
- Acceptance testing: In most cases acceptance testing means checking if the application meets all client requirements.
To clarify, let’s say we illustrate the process with something tangible, such as a building. A building is composed of small blocks that form walls. Each brick has to meet specified requirements; it has to withstand the required load, have a specific volume and shape, and so on. These are unit tests. The idea of integrational tests is to check how tightly and accurately the bricks adhere to each other, how they’re integrated into a certain element of the building. Functional tests can be likened to tests on a single wall of the building, to check whether or not the interior is protected from the elements, and whether or not it possible to see the sun through the window. Acceptance testing involves testing the entire building as a complete product: Open the door, go inside, shut the door, switch on the light, climb to the second floor and take a look at the garden outside the building.
Meet Codeception
However, this division is conditional and sometimes it is difficult to resist the temptation of mixing different kinds of tests.
Many developers use unit tests and claim that’s enough. I used to be one such developer; I found using different systems for different types of tests too difficult and time-consuming. A while ago, I decided to find something more useful than PHPUnit; I wanted to be better at testing my code, but I didn’t want to read and learn tons of documentation and look for pitfalls. That’s how I discovered Codeception. At first, I was skeptical, as we often are when it comes to something new (this project is five years old, so technically, it can’t be considered “new”), but after playing around with it for a couple of days, I concluded Codeception is a very useful and powerful system.
So how do you install Codeception? It is as simple as it gets:
$ composer require "codeception/codeception"
$ php vendor/bin/codecept bootstrap
After installing, you will find a new folder named tests in your project, and there will be some subfolders named acceptance, functional and unit. It looks like we can start writing our tests. Cool, but what’s next?
Now, try to add a standard acceptance Hello World test.
$ php vendor/bin/codecept generate:cept acceptance HelloWorld
Now, we get an acceptance test file tests/acceptance/HelloWorldCept.php, with the following content:
<?php
$I = new AcceptanceTester($scenario);
$I->wantTo('perform actions and see result');
The default variable, named $I
, is not just a letter; it is a character. What conducts the tests? The tester, obviously. This tester opens your website’s page or class, does something with it, and shows you the end-result of its actions. You will see what worked and what went wrong. That’s why this object is named $I
and why it contains methods called wantTo()
, see()
or amOnPage()
.
So, let’s think like a tester would about ways of checking a page’s operability. The first approach is to open the page and search for a phrase. It proves that the page is available to visitors.
This should be easy:
<?php
$I->amOnPage('/');
$I->see('Welcome');
We can use this command to run Codeception’s tests:
$ php vendor/bin/codecept run
We see immediately that something is wrong. At first glance, it appears that the message is too long and unclear, but when we look more closely, everything becomes obvious.
We had one test, Acceptance, and it detected an error:
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)
This is the culprit: localhost is not available.
And here are the scenario steps of our test:
1. $I->amOnPage("/")
Ok, let’s open tests/acceptance.suite.yml and change url: http://localhost/
to something that is actually available. In my case, it is my local test host, url: https://local.codeception-article.com/
Run the test again and this is what you should end up with:
Acceptance Tests (1) ---------------------------------------------------------------------------------------
Perform actions and result (HelloWorldCept) Ok
Hooray! Our first successful test!
Of course, amOnPage()
is not the only testing method available, we merely singled it out for our example. All Codeception test methods can be divided into the following groups:
- Interaction with page:
fillField()
,selectOption()
,submitForm()
,click()
- Assertions.
see()
,dontSee()
,seeElement()
,seeInCurrentUrl()
,seeCheckboxIsChecked()
,seeInField()
,seeLink()
. To all these methods you can add a suffix and use it when you need a method that will not interrupt the test scenario when something cannot be found. - Cookie methods:
setCookie()
,grabCookie()
,seeCookie()
- Comment and description of test scenarios:
amGoingTo()
,wantTo()
,expect()
. Use these methods to get well commented and described tests, which will help you remember the goals of the test.
So, if we were to test the password-reset email page, we could do it this way:
<?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');
It looks like this should do it, but what if there are some Ajax-loaded parts on the page? Can we test a page like that? The answer is that Codeception uses PhpBrowser, based on Symfony BrowserKit and Guzzle, by default. It is simple, fast and you need only curl to use it.
You could also use Selenium and test pages with real browsers. Yes, it will be slower, but you will be able to test JavaScript, as well.
First, you need to install the Selenium driver, change acceptance.suite.yml and rebuild the AcceptanceTester class. After this, you can use methods wait()
and waitForElement()
. And, more interestingly, you will be able to save your time and resources by using methods saveSessionSnapshot()
and loadSessionSnapshot()
. This method allows you to store the session state and start new tests in earlier sessions. This is useful in some situations, for instance, in the test authorization process.
So, we end up with a simple, yet powerful, ability to test many functions.
Functional Testing
OK, time to move on to functional testing.
$ php vendor/bin/codecept generate:cept functional HelloWorld
And this is what we get:
<?php
$I = new FunctionalTester($scenario);
$I->amOnPage('/');
$I->see('Welcome');
Wait, what?
No, it is not a mistake. Functional tests should be written in the same way as integrational tests. The difference is that functional tests are interacting directly with your application. That means you don’t need a webserver to run functional test, and you have more capacity for testing different parts of your application.
It does mean that support for all frameworks is lacking, but the list of supported frameworks is extensive: Symfony, Silex, Phalcon, Yii, Zend Framework, Lumen, Laravel. This should be sufficient for most cases and most developers. Please consult Codeception’s module documentation to get a list of available functions and then just switch it on in functional.suite.yml
.
Before we proceed to unit testing, allow me to make a small digression. As you may have noticed, we created our tests with key cept:
$ php vendor/bin/codecept generate: cept acceptance HelloWorld
This is not the only way of creating tests. There are also cest tests. The difference is that you can structure multiple related scenarios in one class:
$ 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');
}
}
In this example, methods _before()
and _after()
are run before and after each test. An instance of AcceptanceTester
class is passed to each test, so you can use it in the same way as in cest tests. This style of tests can be useful in certain situations, so it is worth keeping in mind.
Unit Testing
Time for some unit testing.
Codeception is based on PHPUnit, so you can use tests written for PHPUnit. To add new PHPUnit tests, use the following approach:
$ php vendor/bin/codecept generate:phpunit unit HelloWorld
Or just inherit your tests on \PHPUnit_Framework_TestCase
.
But if you want something more, you should try Codeception’s unit tests:
$ 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());
}
}
Nothing unusual for now. Methods _before()
and _after()
are setUp()
and tearDown()
analogues and will run before and after each test.
The main advantage of this test is in its ability to extend your testing process by including modules that can be switched on in unit.suite.yml
:
- Access to memcache and databases to track changes (MySQL, SQLite, PostgreSQL, MongoDB are supported)
- Testing of REST/SOAP applications
- Queues
Each module has its own features, so it is best to check the documentation and collect the necessary information for each module before proceeding to actual tests.
Plus, you can use the Codeception/Specify package (which needs to be added to composer.json
) and write a description like so:
<?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 inside these closure functions is isolated, so changes inside will not affect the rest of your code. Descriptions will help you to make the test more readable and make it easier to identify failed tests.
As an optional extra, you can use package Codeception\Verify
for BDD-like syntax:
<?php
public function testUserEmailSave()
{
verify($map->getEmail())->equals('correct@email.com');
}
And of course you can use stubs:
<?php
public function testUserEmailSave()
{
$user = Stub::make('User', ['getEmail' => 'correct@email.com']);
$this->assertEquals('correct@email.com', $user->getEmail());
}
Verdict: Codeception Saves Time and Effort
So what should you expect from Codeception? Who’s it for? Are there any caveats?
In my opinion, this testing framework is suitable for all sorts of different teams: Large and small, beginners and battle-hardened PHP professionals, those who are using a popular framework, and those who aren’t using any framework.
In any case, it all boils down to this: Codeception is ready for prime time.
It is a mature and well-documented framework that may easily be extended by numerous modules. Codeception is modern, but based on the time-tested PHPUnit, which should reassure developers who don’t want to experiment too much.
It performs well, which means it’s fast and doesn’t require too much time and effort. Better yet, it is relatively easy to master, and abundant documentation should assist a hassle-free learning process.
Codeception is also easy to install and configure, yet it boasts a lot of advanced options. While most users won’t need all (or indeed, most) of them, it all depends on what you intend to do with it. You can start with the basics, and the extra features will come in handy sooner or later.
Kyiv, Ukraine
Member since November 15, 2015
About the author
Vasily is a web developer with more than nine years of extensive experience developing, optimizing, and supporting web applications.