Codeception is new BDD-style testing framework for PHP. It makes testing easier than it was before. Yep, really. If you are not a fan of testing, that might as well be because you havenāt used the proper tools. Weāve already showed you how simple it is to automate testing for any web application by writing acceptance tests. Today we will dig deeper into the code and show you how it can be tested.
With BDD approach in Codeception, any test, even the unit test, is written as a scenario. By this scenario you declare what you are doing and what results you expect to see. In traditional xUnit scheme your test is just a piece of code that uses the method being tested. This piece of code becomes a mess when you test complex units depending on other classes or when you need to check data in a database, etc. Codeception always keeps your unit tests simple and readable.
I always start with a model example in the MVC pattern. I am not using any of existing PHP ORMs in the sample code, and this will make the code look a little bit weird. Iām doing this just to demonstrate testing process.
Here weāve got a sample model class.
<?php
class User extends AbstractModel {
public function create()
{
if (!$this->isNew) throw new ModelException("User already created");
if (!$this->role) $this->role = 'member';
if (!$this->validate()) throw new ValidationException("User is invalid");
$this->save();
}
}
?>Quite a complex method of ORM class, but its usage is really simple:
<?php
$user = new User;
$user->setName('davert');
$user->create();
?>How is this method tested with Codeception? First of all, we wonāt be testing any inherited methods like validate or save. They belong to AbstractModel class and are to be tested there. The ācreateā method is to be tested in full isolation. For this we will not use the actual User class, but its Stub, i.e. a class with some methods replaced by their dummies.
<?php
use Codeception\Util\Stub;
class UserCest {
public $class = 'User';
public function create(CodeGuy $I)
{
$I->wantTo('create new user by name');
$I->haveStub($user = Stub::makeEmptyExcept('User', 'create'));
$user->setName('davert');
$I->executeTestedMethodOn($user);
$I->expect('user is validated and saved')
->seeMethodInvoked($user, 'validate')
->seeMethodInvoked($user, 'save');
}
}
?>Here we have tested that the āvalidateā and āsaveā methods were actually invoked. We assume that āvalidateā and āsaveā are themselves tested; thus, they will work as expected. And if the test fails, we know the source of problem is the ācreateā method itself.
However, the test doesnāt cover exceptions that may be thrown. Thus letās improve it by making the validate method simulate exceptions.
<?php
use Codeception\Util\Stub;
class UserCest {
public $class = 'User';
public function create(CodeGuy $I)
{
$I->wantTo('create new user by name');
$I->haveStub($user = Stub::makeEmptyExcept('User', 'create'));
$I->haveStub($invalid_user = Stub::makeEmptyExcept('User', 'create', array(
'validate' => function () { return false; }
)));
$user->setName('davert');
$I->executeTestedMethodOn($user);
$I->expect('user is validated and saved')
->seeMethodInvoked($user, 'validate')
->seeMethodInvoked($user, 'save');
$I->expect('exception is thrown for invalid user')
->executeTestedMethodOn($invalid_user)
->seeExceptionThrown('ValidationException','User is invalid');
$I->expect('exception is thrown while trying to create not new user')
->changeProperty($user,'isNew', false)
->executeTestedMethodOn($user)
->seeExceptionThrown('ModelException', "User already created");
}
}
?>The only thing we havenāt cover in the test is userās default role assertion. In case we store all column values as public variables, we can use the āseePropertyEqualsā method.
<?php
use Codeception\Util\Stub;
class UserCest {
public $class = 'User';
public function create(CodeGuy $I)
{
$I->wantTo('create new user by name');
$I->haveStub($user = Stub::makeEmptyExcept('User', 'create'));
$I->haveStub($invalid_user = Stub::makeEmptyExcept('User', 'create', array(
'validate' => function () { throw new Exception("invalid"); }
)));
$user->setName('davert');
$I->executeTestedMethodOn($user);
$I->expect('user is validated and saved')
->seePropertyEquals($user, 'role', 'member')
->seeMethodInvoked($user, 'validate')
->seeMethodInvoked($user, 'save');
$I->expect('exception is thrown for invalid user')
->executeTestedMethodOn($invalid_user)
->seeExceptionThrown('ValidationException','User is invalid');
$I->expect('exception is thrown while trying to create not new user')
->changeProperty($user,'isNew', false)
->executeTestedMethodOn($user)
->seeExceptionThrown('ModelException', "User already created");
}
}
?>By this test we have 100% covered the ācreateā method with test and isolated its environment. As a bonus, we can improve our documentation by the text of this scenario. If we use DocBlox, we can set up Codeception plugin and generate documentation for User class ācreateā method.
With this method I can create new users by name.
Declared Variables:
* $user1 (User)
* $user2 (User)
If I execute $user1->create()
I expect user is validated and saved
I will see property equals $user1, 'role', 'member'
I will see method invoked $user1, 'validate'
I will see method invoked $user1, 'save'
I expect exception is thrown for invalid user
If I execute $user2->create()
I will see exception thrown 'ValidationException', 'invalid'
I expect exception is thrown while trying to create not new user
I change property $user1, 'isNew', false
If I execute $user1->create()
I will see exeception thrown 'ModelException', 'User already created'We can say that the ācreateā method is fully described by this text.
Conclusion
What weāve got by writing the test for the create method of user class? Weāve made sure that by using this method the user is always validated and saved when created. Weāve also made sure the default role is āmemberā. Well, thatās all. But thatās all that ācreateā function is doing.
For further reading on Codeception unit tests see our documentation.
In the next post we will simplify the model test by breaking some isolation rules. Subscribe to our RSS channel to stay in touch.


