Квест → Как хакнуть форму
Прошли: 77
Работая со старым кодом, я часто сталкиваюсь с таким классом, который расширяет большой базовый абстрактный класс, а методы этого класса вызывают методы того большого базового абстрактного класса, который делает очень много вещей. Я сам писал такие классы и методы в прошлом. Век живи, век учись.
Одна из самых больших проблем с этим кодом заключается в том, что это довольно трудно тестировать. Методы из базового класса могут возвращать другие объекты, побочно затрагивают другие части кода (или вызывают сервисы), делают 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() будут идти к заглушке.
Использование частичного "двойника", наверное, должно быть последней инстанцией. Мы должны всегда стремиться выполнить рефакторинг кода, чтобы было легче тестировать, но бывают случаи, когда они действительно могут помочь нам в тестировании унаследованного кода.
Удачного хакинга!