Квест → Как хакнуть форму
Прошли: 77
Иногда при работе с mock объектами библиотеки Mockery, мы хотим сообщить фейковому методу, чтобы он возвращал разные значения для разных аргументов. Это редкий случай, когда мне нужна эта функциональность, но каждый раз, когда мне это нужно, я счастлив, что она есть.
Функция, которая позволяет возвращать различные значения, основываясь на аргументах - это метод Mockery andReturnUsing
, который принимает замыкание в качестве аргумента:
// example.php $dependencyMock = \Mockery::mock('SomeDependency'); $dependencyMock->shouldReceive('callDependency') ->andReturnUsing(function ($argument) { if ($argument <= 10) { return 'low'; } return 'high'; }); $dependencyMock->callDependency(10); // 'low' $dependencyMock->callDependency(10); // 'low' $dependencyMock->callDependency(11); // 'high'
Теперь когда мы вызываем на метод callDependency
с параметром 10 или меньше, он будет возвращать 'low'
, в противном случае он будет возвращать 'high'
Не очень хороший пример, так что давайте взглянем на другой, который чуть ближе к реальной ситуации.
Предположим, мы используем Doctrine`овский entity-менеджер для получения репозиториев наших сущностей в таком классе-сервисе:
// src/ArticleService.php class ArticleService { public function __construct(EntityManager $em) { $this->articleRepo = $em->getRepository(Entity\Article::class); $this->authorRepo = $em->getRepository(Entity\Author::class); } }
Метод getRepository
диспетчера сущностей ( EntityManager`а :) ) вызывается дважды: один раз - для статьи, второй - для автора.
При тестировании мы можем настроить наши моки вот так:
// tests/ArticleServiceTest.php class ArticleServiceTest extends MockeryTestCase { public function setup() { $this->authorRepositoryMock = \Mockery::mock(AuthorRepository::class); $this->articleRepositoryMock = \Mockery::mock(ArticleRepository::class); $this->entityManagerMock = \Mockery::mock(EntityManager::class); } public function testArticleService() { $repositoryMap = [ 'Entity\Author' => $this->authorRepositoryMock, 'Entity\Article' => $this->articleRepositoryMock, ]; $this->entityManagerMock->shouldReceive('getRepository') ->andReturnUsing(function($argument) use ($repositoryMap) { return $repositoryMap[$argument]; }); $articleService = new ArticleService($this->entityManagerMock); } }
Здесь в методе setup
мы создаём три mock-объекта, которые нам нужны, а затем в тестовом методе создаём $repositoryMap
для сопоставления объектов и репозиториев. Карту сопоставления репозиториев также можно было создать внутри замыкания, переданного в andReturnUsing
.
Теперь, когда мы инстанциируем ArticleService
с за`mock`аным entity-менеджером, этот mock получит два вызова метода getRepository
при вызове конструктора ArticleService
. При этом для возвращения соответствующего репозитория мок-объектов, он будет использовать замыкание переданное в andReturnUsing
.
Конечно есть и другой способ, чтобы достичь того же. Например, с помощью andReturn
, но в этом случае немного больше писанины:
// tests/ArticleServiceTest.php public function testArticleService() { $this->entityManagerMock->shouldReceive('getRepository') ->with('Entity\Author') ->andReturn($this->authorRepositoryMock); $this->entityManagerMock->shouldReceive('getRepository') ->with('Entity\Article') ->andReturn($this->articleRepositoryMock); $articleService = new ArticleService($this->entityManagerMock); }
Будет сделано то же самое, что и в предыдущем случае. Можно даже утверждать, что этот второй вариант даже понятнее, чем первый, конечно, для относительно небольшого аргумента “map”. Но в случае, где будет более двух возможных аргументов, нам поможет andReturnUsing
.
П. С.: правильныее это на самом деле было бы отрефакторить код таким образом, что ArticleService
не получал два репозитория из entity manager`а, а инжектить непосредственно их самих.