init
This commit is contained in:
473
vendor/codeception/base/docs/05-UnitTests.md
vendored
Normal file
473
vendor/codeception/base/docs/05-UnitTests.md
vendored
Normal file
@@ -0,0 +1,473 @@
|
||||
# Unit & Integration Tests
|
||||
|
||||
Codeception uses PHPUnit as a backend for running its tests. Thus, any PHPUnit test can be added to a Codeception test suite
|
||||
and then executed. If you ever wrote a PHPUnit test then do it just as you did before.
|
||||
Codeception adds some nice helpers to simplify common tasks.
|
||||
|
||||
## Creating a Test
|
||||
|
||||
Create a test using `generate:test` command with a suite and test names as parameters:
|
||||
|
||||
```bash
|
||||
php vendor/bin/codecept generate:test unit Example
|
||||
```
|
||||
|
||||
It creates a new `ExampleTest` file located in the `tests/unit` directory.
|
||||
|
||||
As always, you can run the newly created test with this command:
|
||||
|
||||
```bash
|
||||
php vendor/bin/codecept run unit ExampleTest
|
||||
```
|
||||
|
||||
Or simply run the whole set of unit tests with:
|
||||
|
||||
```bash
|
||||
php vendor/bin/codecept run unit
|
||||
```
|
||||
|
||||
A test created by the `generate:test` command will look like this:
|
||||
|
||||
```php
|
||||
<?php
|
||||
|
||||
class ExampleTest extends \Codeception\Test\Unit
|
||||
{
|
||||
/**
|
||||
* @var \UnitTester
|
||||
*/
|
||||
protected $tester;
|
||||
|
||||
protected function _before()
|
||||
{
|
||||
}
|
||||
|
||||
protected function _after()
|
||||
{
|
||||
}
|
||||
|
||||
// tests
|
||||
public function testMe()
|
||||
{
|
||||
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Inside a class:
|
||||
|
||||
* all public methods with `test` prefix are tests
|
||||
* `_before` method is executed before each test (like `setUp` in PHPUnit)
|
||||
* `_after` method is executed after each test (like `tearDown` in PHPUnit)
|
||||
|
||||
## Unit Testing
|
||||
|
||||
Unit tests are focused around a single component of an application.
|
||||
All external dependencies for components should be replaced with test doubles.
|
||||
|
||||
A typical unit test may look like this:
|
||||
|
||||
```php
|
||||
<?php
|
||||
class UserTest extends \Codeception\Test\Unit
|
||||
{
|
||||
public function testValidation()
|
||||
{
|
||||
$user = new User();
|
||||
|
||||
$user->setName(null);
|
||||
$this->assertFalse($user->validate(['username']));
|
||||
|
||||
$user->setName('toolooooongnaaaaaaameeee');
|
||||
$this->assertFalse($user->validate(['username']));
|
||||
|
||||
$user->setName('davert');
|
||||
$this->assertTrue($user->validate(['username']));
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Assertions
|
||||
|
||||
There are pretty many assertions you can use inside tests. The most common are:
|
||||
|
||||
* `$this->assertEquals()`
|
||||
* `$this->assertContains()`
|
||||
* `$this->assertFalse()`
|
||||
* `$this->assertTrue()`
|
||||
* `$this->assertNull()`
|
||||
* `$this->assertEmpty()`
|
||||
|
||||
Assertion methods come from PHPUnit. [See the complete reference at phpunit.de](https://phpunit.de/manual/current/en/appendixes.assertions.html).
|
||||
|
||||
### Test Doubles
|
||||
|
||||
Codeception provides [Codeception\Stub library](https://github.com/Codeception/Stub) for building mocks and stubs for tests.
|
||||
Under the hood it used PHPUnit's mock builder but with much simplified API.
|
||||
|
||||
Alternatively, [Mockery](https://github.com/Codeception/MockeryModule) can be used inside Codeception.
|
||||
|
||||
#### Stubs
|
||||
|
||||
Stubs can be created with a static methods of `Codeception\Stub`.
|
||||
|
||||
```php
|
||||
<?php
|
||||
$user = \Codeception\Stub::make('User', ['getName' => 'john']);
|
||||
$name = $user->getName(); // 'john'
|
||||
```
|
||||
|
||||
[See complete reference](http://codeception.com/docs/reference/Mock)
|
||||
|
||||
Inside unit tests (`Codeception\Test\Unit`) it is recommended to use alternative API:
|
||||
|
||||
```php
|
||||
<?php
|
||||
// create a stub with find method replaced
|
||||
$userRepository = $this->make(UserRepository::class, ['find' => new User]);
|
||||
$userRepository->find(1); // => User
|
||||
|
||||
// create a dummy
|
||||
$userRepository = $this->makeEmpty(UserRepository::class);
|
||||
|
||||
// create a stub with all methods replaced except one
|
||||
$user = $this->makeEmptyExcept(User::class, 'validate');
|
||||
$user->validate($data);
|
||||
|
||||
// create a stub by calling constructor and replacing a method
|
||||
$user = $this->construct(User::class, ['name' => 'davert'], ['save' => false]);
|
||||
|
||||
// create a stub by calling constructor with empty methods
|
||||
$user = $this->constructEmpty(User::class, ['name' => 'davert']);
|
||||
|
||||
// create a stub by calling constructor with empty methods
|
||||
$user = $this->constructEmptyExcept(User::class, 'getName', ['name' => 'davert']);
|
||||
$user->getName(); // => davert
|
||||
$user->setName('jane'); // => this method is empty
|
||||
```
|
||||
|
||||
[See complete reference](http://codeception.com/docs/reference/Mock)
|
||||
|
||||
Stubs can also be created using static methods from `Codeception\Stub` class.
|
||||
In this
|
||||
|
||||
```php
|
||||
<?php
|
||||
\Codeception\Stub::make(UserRepository::class, ['find' => new User]);
|
||||
```
|
||||
|
||||
See a reference for [static Stub API](http://codeception.com/docs/reference/Stub)
|
||||
|
||||
#### Mocks
|
||||
|
||||
To declare expectations for mocks use `Codeception\Stub\Expected` class:
|
||||
|
||||
```php
|
||||
<?php
|
||||
// create a mock where $user->getName() should never be called
|
||||
$user = $this->make('User', [
|
||||
'getName' => Expected::never(),
|
||||
'someMethod' => function() {}
|
||||
]);
|
||||
$user->someMethod();
|
||||
|
||||
// create a mock where $user->getName() should be called at least once
|
||||
$user = $this->make('User', [
|
||||
'getName' => Expected::atLeastOnce('Davert')
|
||||
]
|
||||
);
|
||||
$user->getName();
|
||||
$userName = $user->getName();
|
||||
$this->assertEquals('Davert', $userName);
|
||||
```
|
||||
|
||||
[See complete reference](http://codeception.com/docs/reference/Mock)
|
||||
|
||||
## Integration Tests
|
||||
|
||||
Unlike unit tests integration tests doesn't require the code to be executed in isolation.
|
||||
That allows us to use database and other components inside a tests.
|
||||
To improve the testing experience modules can be used as in functional testing.
|
||||
|
||||
### Using Modules
|
||||
|
||||
As in scenario-driven functional or acceptance tests you can access Actor class methods.
|
||||
If you write integration tests, it may be useful to include the `Db` module for database testing.
|
||||
|
||||
```yaml
|
||||
# Codeception Test Suite Configuration
|
||||
|
||||
# suite for unit (internal) tests.
|
||||
actor: UnitTester
|
||||
modules:
|
||||
enabled:
|
||||
- Asserts
|
||||
- Db
|
||||
- \Helper\Unit
|
||||
```
|
||||
|
||||
To access UnitTester methods you can use the `UnitTester` property in a test.
|
||||
|
||||
### Testing Database
|
||||
|
||||
Let's see how you can do some database testing:
|
||||
|
||||
```php
|
||||
<?php
|
||||
function testSavingUser()
|
||||
{
|
||||
$user = new User();
|
||||
$user->setName('Miles');
|
||||
$user->setSurname('Davis');
|
||||
$user->save();
|
||||
$this->assertEquals('Miles Davis', $user->getFullName());
|
||||
$this->tester->seeInDatabase('users', ['name' => 'Miles', 'surname' => 'Davis']);
|
||||
}
|
||||
```
|
||||
|
||||
To enable the database functionality in unit tests, make sure the `Db` module is included
|
||||
in the `unit.suite.yml` configuration file.
|
||||
The database will be cleaned and populated after each test, the same way it happens for acceptance and functional tests.
|
||||
If that's not your required behavior, change the settings of the `Db` module for the current suite. See [Db Module](http://codeception.com/docs/modules/Db)
|
||||
|
||||
### Interacting with the Framework
|
||||
|
||||
You should probably not access your database directly if your project already uses ORM for database interactions.
|
||||
Why not use ORM directly inside your tests? Let's try to write a test using Laravel's ORM Eloquent.
|
||||
For this we need to configure the Laravel5 module. We won't need its web interaction methods like `amOnPage` or `see`,
|
||||
so let's enable only the ORM part of it:
|
||||
|
||||
```yaml
|
||||
actor: UnitTester
|
||||
modules:
|
||||
enabled:
|
||||
- Asserts
|
||||
- Laravel5:
|
||||
part: ORM
|
||||
- \Helper\Unit
|
||||
```
|
||||
|
||||
We included the Laravel5 module the same way we did for functional testing.
|
||||
Let's see how we can use it for integration tests:
|
||||
|
||||
```php
|
||||
<?php
|
||||
function testUserNameCanBeChanged()
|
||||
{
|
||||
// create a user from framework, user will be deleted after the test
|
||||
$id = $this->tester->haveRecord('users', ['name' => 'miles']);
|
||||
// access model
|
||||
$user = User::find($id);
|
||||
$user->setName('bill');
|
||||
$user->save();
|
||||
$this->assertEquals('bill', $user->getName());
|
||||
// verify data was saved using framework methods
|
||||
$this->tester->seeRecord('users', ['name' => 'bill']);
|
||||
$this->tester->dontSeeRecord('users', ['name' => 'miles']);
|
||||
}
|
||||
```
|
||||
|
||||
A very similar approach can be used for all frameworks that have an ORM implementing the ActiveRecord pattern.
|
||||
In Yii2 and Phalcon, the methods `haveRecord`, `seeRecord`, `dontSeeRecord` work in the same way.
|
||||
They also should be included by specifying `part: ORM` in order to not use the functional testing actions.
|
||||
|
||||
If you are using Symfony with Doctrine, you don't need to enable Symfony itself but just Doctrine2:
|
||||
|
||||
```yaml
|
||||
actor: UnitTester
|
||||
modules:
|
||||
enabled:
|
||||
- Asserts
|
||||
- Doctrine2:
|
||||
depends: Symfony
|
||||
- \Helper\Unit
|
||||
```
|
||||
|
||||
In this case you can use the methods from the Doctrine2 module, while Doctrine itself uses the Symfony module
|
||||
to establish connections to the database. In this case a test might look like:
|
||||
|
||||
```php
|
||||
<?php
|
||||
function testUserNameCanBeChanged()
|
||||
{
|
||||
// create a user from framework, user will be deleted after the test
|
||||
$id = $this->tester->haveInRepository(User::class, ['name' => 'miles']);
|
||||
// get entity manager by accessing module
|
||||
$em = $this->getModule('Doctrine2')->em;
|
||||
// get real user
|
||||
$user = $em->find(User::class, $id);
|
||||
$user->setName('bill');
|
||||
$em->persist($user);
|
||||
$em->flush();
|
||||
$this->assertEquals('bill', $user->getName());
|
||||
// verify data was saved using framework methods
|
||||
$this->tester->seeInRepository(User::class, ['name' => 'bill']);
|
||||
$this->tester->dontSeeInRepository(User::class, ['name' => 'miles']);
|
||||
}
|
||||
```
|
||||
|
||||
In both examples you should not be worried about the data persistence between tests.
|
||||
The Doctrine2 and Laravel5 modules will clean up the created data at the end of a test.
|
||||
This is done by wrapping each test in a transaction and rolling it back afterwards.
|
||||
|
||||
### Accessing Module
|
||||
|
||||
Codeception allows you to access the properties and methods of all modules defined for this suite.
|
||||
Unlike using the UnitTester class for this purpose, using a module directly grants you access
|
||||
to all public properties of that module.
|
||||
|
||||
We have already demonstrated this in a previous example where we accessed the Entity Manager from a Doctrine2 module:
|
||||
|
||||
```php
|
||||
<?php
|
||||
/** @var Doctrine\ORM\EntityManager */
|
||||
$em = $this->getModule('Doctrine2')->em;
|
||||
```
|
||||
|
||||
If you use the `Symfony` module, here is how you can access the Symfony container:
|
||||
|
||||
```php
|
||||
<?php
|
||||
/** @var Symfony\Component\DependencyInjection\Container */
|
||||
$container = $this->getModule('Symfony')->container;
|
||||
```
|
||||
|
||||
The same can be done for all public properties of an enabled module. Accessible properties are listed in the module reference.
|
||||
|
||||
### Scenario Driven Testing
|
||||
|
||||
[Cest format](http://codeception.com/docs/07-AdvancedUsage#Cest-Classes) can also be used for integration testing.
|
||||
In some cases it makes tests cleaner as it simplifies module access by using common `$I->` syntax:
|
||||
|
||||
```php
|
||||
<?php
|
||||
public function buildShouldHaveSequence(\UnitTester $I)
|
||||
{
|
||||
$build = $I->have(Build::class, ['project_id' => $this->project->id]);
|
||||
$I->assertEquals(1, $build->sequence);
|
||||
$build = $I->have(Build::class, ['project_id' => $this->project->id]);
|
||||
$I->assertEquals(2, $build->sequence);
|
||||
$this->project->refresh();
|
||||
$I->assertEquals(3, $this->project->build_sequence);
|
||||
}
|
||||
```
|
||||
This format can be recommended for testing domain and database interactions.
|
||||
|
||||
In Cest format you don't have native support for test doubles so it's recommended
|
||||
to include a trait `\Codeception\Test\Feature\Stub` to enable mocks inside a test.
|
||||
Alternatively, install and enable [Mockery module](https://github.com/Codeception/MockeryModule).
|
||||
|
||||
## Advanced Tools
|
||||
|
||||
### Specify
|
||||
|
||||
When writing tests you should prepare them for constant changes in your application.
|
||||
Tests should be easy to read and maintain. If a specification of your application is changed,
|
||||
your tests should be updated as well. If you don't have a convention inside your team for documenting tests,
|
||||
you will have issues figuring out what tests will be affected by the introduction of a new feature.
|
||||
|
||||
That's why it's pretty important not just to cover your application with unit tests, but make unit tests self-explanatory.
|
||||
We do this for scenario-driven acceptance and functional tests, and we should do this for unit and integration tests as well.
|
||||
|
||||
For this case we have a stand-alone project [Specify](https://github.com/Codeception/Specify)
|
||||
(which is included in the phar package) for writing specifications inside unit tests:
|
||||
|
||||
```php
|
||||
<?php
|
||||
class UserTest extends \Codeception\Test\Unit
|
||||
{
|
||||
use \Codeception\Specify;
|
||||
|
||||
/** @specify */
|
||||
private $user;
|
||||
|
||||
public function testValidation()
|
||||
{
|
||||
$this->user = User::create();
|
||||
|
||||
$this->specify("username is required", function() {
|
||||
$this->user->username = null;
|
||||
$this->assertFalse($this->user->validate(['username']));
|
||||
});
|
||||
|
||||
$this->specify("username is too long", function() {
|
||||
$this->user->username = 'toolooooongnaaaaaaameeee';
|
||||
$this->assertFalse($this->user->validate(['username']));
|
||||
});
|
||||
|
||||
$this->specify("username is ok", function() {
|
||||
$this->user->username = 'davert';
|
||||
$this->assertTrue($this->user->validate(['username']));
|
||||
});
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
By using `specify` codeblocks, you can describe any piece of a test.
|
||||
This makes tests much cleaner and comprehensible for everyone in your team.
|
||||
|
||||
Code inside `specify` blocks is isolated. In the example above, any changes to `$this->user`
|
||||
will not be reflected in other code blocks as it is marked with `@specify` annotation.
|
||||
|
||||
Also, you may add [Codeception\Verify](https://github.com/Codeception/Verify) for BDD-style assertions.
|
||||
This tiny library adds more readable assertions, which is quite nice, if you are always confused
|
||||
about which argument in `assert` calls is expected and which one is actual:
|
||||
|
||||
```php
|
||||
<?php
|
||||
verify($user->getName())->equals('john');
|
||||
```
|
||||
|
||||
### Domain Assertions
|
||||
|
||||
The more complicated your domain is the more explicit your tests should be. With [DomainAssert](https://github.com/Codeception/DomainAssert)
|
||||
library you can easily create custom assertion methods for unit and integration tests.
|
||||
|
||||
It allows to reuse business rules inside assertion methods:
|
||||
|
||||
```php
|
||||
<?php
|
||||
$user = new User;
|
||||
|
||||
// simple custom assertions below:
|
||||
$this->assertUserIsValid($user);
|
||||
$this->assertUserIsAdmin($user);
|
||||
|
||||
// use combined explicit assertion
|
||||
// to tell what you expect to check
|
||||
$this->assertUserCanPostToBlog($user, $blog);
|
||||
// instead of just calling a bunch of assertions
|
||||
$this->assertNotNull($user);
|
||||
$this->assertNotNull($blog);
|
||||
$this->assertContain($user, $blog->getOwners());
|
||||
```
|
||||
|
||||
With custom assertion methods you can improve readability of your tests and keep them focused around the specification.
|
||||
|
||||
### AspectMock
|
||||
|
||||
[AspectMock](https://github.com/Codeception/AspectMock) is an advanced mocking framework which allows you to replace any methods of any class in a test.
|
||||
Static methods, class methods, date and time functions can be easily replaced with AspectMock.
|
||||
For instance, you can test singletons!
|
||||
|
||||
```php
|
||||
<?php
|
||||
public function testSingleton()
|
||||
{
|
||||
$class = MySingleton::getInstance();
|
||||
$this->assertInstanceOf('MySingleton', $class);
|
||||
test::double('MySingleton', ['getInstance' => new DOMDocument]);
|
||||
$this->assertInstanceOf('DOMDocument', $class);
|
||||
}
|
||||
```
|
||||
|
||||
* [AspectMock on GitHub](https://github.com/Codeception/AspectMock)
|
||||
* [AspectMock in Action](http://codeception.com/07-31-2013/nothing-is-untestable-aspect-mock.html)
|
||||
* [How it Works](http://codeception.com/09-13-2013/understanding-aspectmock.html)
|
||||
|
||||
## Conclusion
|
||||
|
||||
PHPUnit tests are first-class citizens in test suites. Whenever you need to write and execute unit tests,
|
||||
you don't need to install PHPUnit separately, but use Codeception directly to execute them.
|
||||
Some nice features can be added to common unit tests by integrating Codeception modules.
|
||||
For most unit and integration testing, PHPUnit tests are enough. They run fast, and are easy to maintain.
|
||||
Reference in New Issue
Block a user