Тестовые окружения (Fixtures) в PHPUnit

При тестировании часто необходимо привести состояние окружающего мира в определённое состояние, то есть создать для теста тестовое окружение (Fixture). В PHPUnit для этого есть методы setUp() и tearDown(). Перед запуском тестового метода запускается метод setUp(). В этом методе вы можете настраивать тестовое окружение: создавать объекты, с которыми тестируете и т.п. После выполнения тестового метода вызывается tearDown(), вне зависимости успешно прошёл тест или нет. В этом методе можно возвращать тестовое окружение в первоначальное состояние, очищать объекты, с которыми тестируете и т.п.

Приведём пример. Нужно протестировать класс SomeController:

<?php
// src/ExampleProject/SomeController.php
namespace ExampleProject;

class SomeController
{
    /**
     * @var StorageInterface
     */
    protected $storage;

    /**
     * @param StorageInterface $storage
     */
    public function __construct(StorageInterface $storage)
    {
        $this->storage = $storage;
    }

    /**
     * @param array $data
     * @return array
     * @throws \Exception
     */
    public function saveAction(array $data): array
    {
        if (!$this->storage->save($data)) {
            throw new \Exception();
        }

        return [];
    }
}
            

 

Код теста как и ранее (см. PHPUnit mock-объекты):

<?php
// tests/ExampleProject/SomeControllerTest.php
namespace Tests\ExampleProject;

use PHPUnit\Framework\TestCase;
use ExampleProject\SomeController;
use ExampleProject\StorageInterface;

class SomeControllerTest extends TestCase
{
    /**
     * @throws \Exception
     */
    public function testSaveAction()
    {
        $data = ['a', 'b'];

        /** @var StorageInterface | \PHPUnit_Framework_MockObject_MockObject $storageMock */
        $storageMock = $this->createMock(StorageInterface::class);
        $storageMock->expects($this->once())
            ->method('save')
            ->with($data)
            ->will($this->returnValue(true));

        $someController = new SomeController($storageMock);
        $this->assertInternalType('array', $someController->saveAction($data));
    }

    /**
     * @expectedException \Exception
     */
    public function testSaveActionFailSaving()
    {
        /** @var StorageInterface | TestCase $storageMock */
        $storageMock = $this->createMock(StorageInterface::class);
        $storageMock->method('save')
            ->willReturn(false);

        $someController = new SomeController($storageMock);
        $someController->saveAction(['any data']);
    }
}
            

 

В каждом тесте создаётся mock-объект хранилища и объект контроллера. Это и дублирование кода и отвлекает внимание от сути теста.

Доработаем тест:

<?php
// tests/ExampleProject/SomeControllerTest.php
namespace Tests\ExampleProject;

use PHPUnit\Framework\TestCase;
use ExampleProject\SomeController;
use ExampleProject\StorageInterface;

class SomeControllerTest extends TestCase
{
    /**
     * @var StorageInterface | \PHPUnit_Framework_MockObject_MockObject
     */
    protected $storageMock;

    /**
     * @var SomeController
     */
    protected $someController;

    /**
     *
     */
    public function setUp()
    {
        $this->storageMock    = $this->createMock(StorageInterface::class);
        $this->someController = new SomeController($this->storageMock);
    }

    /**
     * @throws \Exception
     */
    public function testSaveAction()
    {
        $data = ['a', 'b'];
        $this->storageMock->expects($this->once())
            ->method('save')
            ->with($data)
            ->will($this->returnValue(true));

        $this->assertInternalType('array', $this->someController->saveAction($data));
    }

    /**
     * @expectedException \Exception
     */
    public function testSaveActionFailSaving()
    {
        $this->storageMock->method('save')->willReturn(false);
        $this->someController->saveAction(['any data']);
    }
}
            

 

В новом варианте тестовые методы значительно упростились и отсутствует дублирование кода.

Помимо методов setUp() и tearDown() есть также статические методы setUpBeforeClass() и tearDownAfterClass(). Метода setUpBeforeClass() запускается перед первым запуском первого тестового метода, а tearDownAfterClass() запускается после выполнения всех тестовых методов.