PHP Profi

Mockery: частичные двойники (mock'и) Перевод

Работая со старым кодом, я часто сталкиваюсь с таким классом, который расширяет большой базовый абстрактный класс, а методы этого класса вызывают методы того большого базового абстрактного класса, который делает очень много вещей. Я сам писал такие классы и методы в прошлом. Век живи, век учись.

Одна из самых больших проблем с этим кодом заключается в том, что это довольно трудно тестировать. Методы из базового класса могут возвращать другие объекты, побочно затрагивают другие части кода (или вызывают сервисы), делают http-запросы...

Типичный пример такого класса будет базовая модель, которая имеет getDb() метод:

// AbstractModel.php

abstract class AbstractModel
{
    protected $db = null;

    protected function getDb()
    {
        if ($this->db == null) {
            $db = Config::get('dbname');
            $user = Config::get('dbuser');
            $pass = Config::get('dbpass');
            $this->db = new PDO('mysql:host=localhost;dbname='.$db, $user, $pass);
        }
        return $this->db;
    }
}

который можно вызвать в дочерних классах, чтобы получить доступ к подключению базы данных:

// ArticleModel.php

class ArticleModel extends AbstractModel
{
    public function listArticles()
    {
        $db = $this->getDb();
        $stmt = $db->query('SELECT * FROM articles');

        return $stmt->fetchAll();
    }
}

Если мы хотим написать юнит-тесты для метода listArticles(), наилучшим вариантом было бы, вероятно, рефакторинг моделей так, чтобы подключение к базе данных можно было внедрить либо через конструктор, либо через сеттер-метод.

В том случае, если рефакторинг — это не вариант по каким-либо причинам, то мы можем создать частичный mock для ArticleModel, используя Mockery, а потом замокать (сделать заглушку(stub), если быть более точным) только метод getDb(), который будет всегда возвращать "двойника" (mock) класса PDO:

// tests/ArticleModelTest.php

use Mockery\Adapter\Phpunit;

class ArticleModelTest extends MockeryTestCase
{
    public function testListArticlesReturnsAnEmptyArrayWhenTheTableIsEmpty()
    {
        $stmtMock = \Mockery::mock('\PDOStatement');
        $stmtMock->shouldReceive('fetchAll')
            ->andReturn([]);
        $pdoMock = \Mockery::mock('\PDO');
        $pdoMock->shouldReceive('query')
            ->andReturn($stmtMock);

        // Create a partial mock of ArticleModel
        $articleModel = \Mockery::mock('ArticleModel')->makePartial();

        // Stub the getDb method on the ArticleModel
        $articleModel->shouldReceive('getDb')
            ->andReturn($pdoMock);

        // List all the articles
        $result = $articleModel->listArticles();
        $expected = [];

        $this->assertSame($expected, $result);
    }
}

Когда мы говорим Mockery создать частичного "двойника" класса, любой метод этого частичного "двойника", для которого были заданы ожидания вызова, будет подменён заглушкой, но вызовы других методов Mockery перенаправит в настоящий класс. Другими словами, даже несмотря на то, что ArticleModel — частичный двойник, каждый раз, когда мы вызываем метод listArticles(), Mockery передаст этот вызов в оригинальный метод, и только вызовы метода getDb() будут идти к заглушке.

Использование частичного "двойника", наверное, должно быть последней инстанцией. Мы должны всегда стремиться выполнить рефакторинг кода, чтобы было легче тестировать, но бывают случаи, когда они действительно могут помочь нам в тестировании унаследованного кода.

Удачного хакинга!

2018-02-07 оригинал

Последние посты

Комментарии

авторизуйтесь или зарегистрируйтесь, чтобы оставить комментарий