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