Единые и интеграционные тесты
Codeception использует PHPUnit в качестве бэкенда для выполнения тестов. Таким образом, любой тест PHPUnit может быть добавлен в набор тестов Codeception и затем выполнен. Если вы когда-либо писали тест PHPUnit, сделайте это так же, как и раньше. Codeception добавляет несколько полезных вспомогательных функций для упрощения общих задач.
Создание теста
Создайте тест, используя команду generate:test с именем набора и именем теста в качестве параметров:
php vendor/bin/codecept generate:test unit Example
Она создает новый файл ExampleTest в каталоге tests/unit.
Как всегда, вы можете запустить только что созданный тест с помощью этой команды:
php vendor/bin/codecept run unit ExampleTest
Или просто запустить весь набор модульных тестов с помощью:
php vendor/bin/codecept run unit
Тест, созданный командой generate:test, будет выглядеть следующим образом:
<?php
class ExampleTest extends \Codeception\Test\Unit
{
/**
* @var \UnitTester
*/
protected $tester;
protected function _before()
{
}
protected function _after()
{
}
// tests
public function testMe()
{
}
}Внутри класса:
- все публичные методы с префиксом
testявляются тестами -
метод
_beforeвыполняется перед каждым тестом (какsetUpв PHPUnit) -
метод
_afterвыполняется после каждого теста (какtearDownв PHPUnit)
Модульное тестирование
Модульные тесты сосредоточены вокруг одного компонента приложения. Все внешние зависимости для компонентов должны быть заменены тестовыми дубликатами.
Типичный модульный тест может выглядеть так:
<?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']));
}
}Утверждения
Существует довольно много утверждений, которые можно использовать внутри тестов. Наиболее распространенными являются:
$this->assertEquals()$this->assertContains()$this->assertFalse()$this->assertTrue()$this->assertNull()$this->assertEmpty()
Методы утверждений взяты из PHPUnit. Полную справку см. на phpunit.de.
Тестовые дубликаты
Codeception предоставляет библиотеку Codeception\Stub для создания моков и стабов для тестов. Под капотом используется конструктор моков PHPUnit, но с значительно упрощенным API.
В качестве альтернативы, Mockery может быть использован внутри Codeception.
Стабы
Стабы могут быть созданы с помощью статических методов Codeception\Stub.
<?php
$user = \Codeception\Stub::make('User', ['getName' => 'john']);
$name = $user->getName(); // 'john'Внутри модульных тестов (Codeception\Test\Unit) рекомендуется использовать альтернативный API:
<?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Стабы также могут быть созданы с помощью статических методов класса Codeception\Stub. В этом
<?php \Codeception\Stub::make(UserRepository::class, ['find' => new User]);
См. справку по статическому API стабов
Моки
Для объявления ожиданий для моков используйте класс Codeception\Stub\Expected:
<?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);Интеграционные тесты
В отличие от модульных тестов, интеграционные тесты не требуют, чтобы код выполнялся изолированно. Это позволяет нам использовать базу данных и другие компоненты внутри тестов. Для улучшения опыта тестирования можно использовать модули, как и в функциональном тестировании.
Использование модулей
Как и в сценарийно-ориентированных функциональных или приемочных тестах, вы можете получить доступ к методам класса Actor. Если вы пишете интеграционные тесты, может быть полезно включить модуль Db для тестирования базы данных.
# Codeception Test Suite Configuration
# suite for unit (internal) tests.
actor: UnitTester
modules:
enabled:
- Asserts
- Db
- \Helper\UnitДля доступа к методам UnitTester вы можете использовать свойство UnitTester в тесте.
Тестирование базы данных
Давайте посмотрим, как можно выполнить тестирование базы данных:
<?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']);
}Чтобы включить функциональность базы данных в модульных тестах, убедитесь, что модуль Db включен в файл конфигурации unit.suite.yml. База данных будет очищаться и заполняться после каждого теста, так же, как это происходит для приемочных и функциональных тестов. Если это не нужное поведение, измените настройки модуля Db для текущего набора тестов. См. Модуль БД
Взаимодействие с фреймворком
Вероятно, вам не следует напрямую обращаться к базе данных, если ваш проект уже использует ORM для взаимодействия с базой данных. Почему бы не использовать ORM непосредственно внутри ваших тестов? Попробуем написать тест, используя ORM Eloquent Laravel. Для этого нам нужно настроить модуль Laravel5. Нам не понадобятся его методы взаимодействия с веб-приложением, такие как amOnPage или see, поэтому давайте включим только часть ORM:
actor: UnitTester
modules:
enabled:
- Asserts
- Laravel5:
part: ORM
- \Helper\UnitМы включили модуль Laravel5 так же, как и для функционального тестирования. Давайте посмотрим, как мы можем использовать его для интеграционных тестов:
<?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']);
}Очень похожий подход можно использовать для всех фреймворков, которые имеют ORM, реализующий шаблон ActiveRecord. В Yii2 и Phalcon методы haveRecord, seeRecord, dontSeeRecord работают аналогичным образом. Они также должны быть включены путем указания part: ORM для того, чтобы не использовать действия функционального тестирования.
Если вы используете Symfony с Doctrine, вам не нужно включать сам Symfony, а только Doctrine2:
actor: UnitTester
modules:
enabled:
- Asserts
- Doctrine2:
depends: Symfony
- \Helper\UnitВ этом случае вы можете использовать методы из модуля Doctrine2, в то время как сам Doctrine использует модуль Symfony для установления соединений с базой данных. В этом случае тест может выглядеть так:
<?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']);
}В обоих примерах вам не нужно беспокоиться о сохранении данных между тестами. Модули Doctrine2 и Laravel5 очистят созданные данные в конце теста. Это делается путем обертывания каждого теста в транзакцию и отката ее после этого.
Доступ к модулю
Codeception позволяет получить доступ к свойствам и методам всех модулей, определенных для данного набора тестов. В отличие от использования класса UnitTester для этой цели, прямой доступ к модулю предоставляет вам доступ ко всем общедоступным свойствам этого модуля.
Мы уже продемонстрировали это в предыдущем примере, где мы получили доступ к Entity Manager из модуля Doctrine2:
<?php
/** @var Doctrine\ORM\EntityManager */
$em = $this->getModule('Doctrine2')->em;Если вы используете модуль Symfony, вот как вы можете получить доступ к контейнеру Symfony:
<?php
/** @var Symfony\Component\DependencyInjection\Container */
$container = $this->getModule('Symfony')->container;То же самое можно сделать для всех общедоступных свойств включенного модуля. Доступные свойства перечислены в справке по модулю.
Сценарийно-ориентированное тестирование
Формат Cest также может быть использован для интеграционного тестирования. В некоторых случаях это делает тесты более чистыми, так как упрощает доступ к модулям, используя общую синтаксическую конструкцию $I->:
<?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);
}Этот формат может быть рекомендован для тестирования взаимодействия с доменом и базой данных.
В формате Cest нет встроенной поддержки тестовых дубликатов, поэтому рекомендуется включить трейт \Codeception\Test\Feature\Stub для включения моков в тесте. В качестве альтернативы можно установить и включить модуль Mockery.
Расширенные инструменты
Спецификация
При написании тестов следует подготовить их к постоянным изменениям в вашем приложении. Тесты должны быть легко читаемыми и поддерживаемыми. Если спецификация вашего приложения изменяется, ваши тесты также должны быть обновлены. Если у вашей команды нет соглашения о документировании тестов, у вас возникнут проблемы с определением, какие тесты будут затронуты внедрением новой функции.
Вот почему очень важно не только покрыть ваше приложение модульными тестами, но и сделать модульные тесты понятными. Мы делаем это для сценарийно-ориентированных приемочных и функциональных тестов, и мы должны делать это для модульных и интеграционных тестов тоже.
Для этого у нас есть автономный проект Specify (который включен в пакет phar) для написания спецификаций внутри модульных тестов:
<?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']));
});
}
}Используя блоки кода specify вы можете описать любую часть теста. Это делает тесты более чистыми и понятными для всех членов вашей команды.
Код внутри блоков specify изолирован. В приведенном выше примере любые изменения в $this->user не будут отражены в других блоках кода, так как он помечен аннотацией @specify.
Кроме того, вы можете добавить Codeception\Verify для утверждений в стиле BDD. Эта небольшая библиотека добавляет более читаемые утверждения, что довольно удобно, если вы всегда не уверены, какой аргумент в вызовах assert ожидается, а какой является фактическим:
<?php
verify($user->getName())->equals('john');Утверждения домена
Чем сложнее ваш домен, тем более явными должны быть ваши тесты. С помощью библиотеки DomainAssert вы можете легко создавать пользовательские методы утверждения для модульных и интеграционных тестов.
Это позволяет повторно использовать бизнес-правила внутри методов утверждения:
<?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());
С помощью пользовательских методов утверждения вы можете улучшить читаемость ваших тестов и сосредоточить их на спецификации.
AspectMock
AspectMock — это расширенный фреймворк для создания моков, который позволяет заменять любые методы любого класса в тесте. Статические методы, методы класса, функции даты и времени можно легко заменить с помощью AspectMock. Например, вы можете тестировать синглтоны!
<?php
public function testSingleton()
{
$class = MySingleton::getInstance();
$this->assertInstanceOf('MySingleton', $class);
test::double('MySingleton', ['getInstance' => new DOMDocument]);
$this->assertInstanceOf('DOMDocument', $class);
}Обработка ошибок
По умолчанию Codeception использует уровень обработки ошибок E_ALL & ~E_STRICT & ~E_DEPRECATED. В модульных тестах вы можете изменить этот уровень в зависимости от политики обработки ошибок вашего фреймворка. Уровень обработки ошибок можно установить в файле конфигурации набора тестов:
actor: UnitTester ... error_level: E_ALL & ~E_STRICT & ~E_DEPRECATED
error_level также можно установить глобально в файле codeception.yml. Для этого необходимо указать error_level в качестве части settings. Для получения дополнительной информации см. Глобальная конфигурация. Обратите внимание, что значение error_level для набора тестов переопределяет глобальное значение.
Заключение
Тесты PHPUnit являются полноправными участниками наборов тестов. Всякий раз, когда вам нужно написать и выполнить модульные тесты, вам не нужно устанавливать PHPUnit отдельно, а используйте Codeception напрямую для их выполнения. Некоторые полезные функции могут быть добавлены в обычные модульные тесты путем интеграции модулей Codeception. Для большинства модульных и интеграционных тестов тесты PHPUnit достаточно. Они работают быстро и легко поддерживаются.
- Следующая глава: МодулиИВспомогательныеФункции >
- Предыдущая глава: < ФункциональныеТесты
© 2011 Michael Bodnarchuk and contributors
Licensed under the MIT License.
https://codeception.com/docs/05-UnitTests