Spec-Zone.ru › Codeception

Единые и интеграционные тесты

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);
}
  • AspectMock на GitHub
  • AspectMock в действии
  • Как это работает

Обработка ошибок

По умолчанию 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

Spec-Zone.ru

Настройки Оффлайн Что нового Помощь О нас
Spec-Zone .ru
спецификации, руководства, описания, API