[PHP] Не мокайте то, чем вы не владеете (перевод)
Автор
Сообщение
news_bot ®
Стаж: 6 лет 9 месяцев
Сообщений: 27286
Прим. переводчика: само правило достаточно старое, да и пример, приведенный в статье - на мой взгляд самый простой. Поэтому статья подойдет скорее для новичков, люди с хорошим опытом написания автотестов, возможно, не найдут для себя ничего нового.Веб-приложения зачастую созданы для обработки HTTP-запросов. Обычно объекты используются для инкапсуляции данных запроса. В зависимости от фреймворка у нас может быть такой интерфейс, как
interface HttpRequest
{
public function get(string $name): string;
// ...
}
или даже конкретный класс, такой как
class HttpRequest
{
public function get(string $name): string
{
// ...
}
// ...
}
которые мы можем (и должны) использовать для доступа к данным запроса.В symfony, например, есть Symfony\Component\HttpFoundation\Request::get(). В качестве примера мы не будем беспокоиться о том, какой тип HTTP-запроса мы обрабатываем (GET, POST или другой). Вместо этого давайте сосредоточимся на неявных API, таких как HttpRequest::get(), и проблемах, которые они создают.Когда нам нужно получить данные запроса, например, в контроллере, нам нужно использовать один и тот же метод get() для любого параметра, который мы хотим получить. Не существует специального метода с явным именем для отдельной части данных запроса. Вместо этого имя параметра передается только как строковый аргумент универсальному методу get():
class SomeController
{
public function execute(HttpRequest $request): HttpResponse
{
$id = $request->get('id');
$amount = $request->get('amount');
$price = $request->get('price');
// ...
}
}
Мы не будем спорить о том, должен ли контроллер иметь один action-метод или несколько (подсказка: у него должен быть только один (eng видео)). Дело в том, что контроллеру необходимо извлекать и обрабатывать данные из HTTP-запроса.Когда мы заменяем объект HttpRequest на тестовую заглушку (stub) или mock-объект для тестирования SomeController изолированно от сети и от фреймворка, мы сталкиваемся с проблемой множественных вызовов одного и того же метода get() с разными аргументами, которые представляют собой просто строки: 'id', 'amount' и 'price'.Мы должны обеспечить осмысленные возвращаемые значения для каждого вызова, иначе данные не пройдут проверку, и мы не пройдем по позитивному пути нашего action-метода контроллера.Для тестирования SomeController изолированно от реального объекта HttpRequest мы можем использовать тестовую заглушку (stub) в unit тесте с PHPUnit примерно так:
$request = $this->createStub(HttpRequest::class);
$request->method('get')
->willReturnOnConsecutiveCalls(
'1',
'2',
'3',
);
$controller = new SomeController;
$controller->execute($request);
Если мы также хотим проверить связь между SomeController и объектом HttpRequest, нам понадобится mock-объект, для которого мы должны настроить ожидаемые значения в нашем тесте:
$request = $this->createMock(HttpRequest::class);
$request->expects($this->exactly(3))
->method('get')
->withConsecutive(
['id'],
['amount'],
['price']
)
->willReturnOnConsecutiveCalls(
'1',
'2',
'3',
);
$controller = new SomeController;
$controller->execute($request);
Код, показанный выше, немного трудно читать, это запах кода (прим пер. на русском почитать можно тут).Мы заявляем, что HttpRequest::get() необходимо вызывать три раза: сначала с аргументом «id», затем с «amount» и, наконец, с «price».Если мы изменим реализацию SomeController::execute(), например изменим порядок вызовов HttpRequest::get(), наш тест завершится ошибкой. Это говорит нам о том, что мы слишком сильно связали наш тестовый код с рабочим кодом. Это еще один запах.Настоящая проблема заключается в том, что мы работаем с HTTP-запросом, используя неявный API, где мы передаем строковый аргумент, определяющий имя параметра HTTP, в общий метод get(). И, что еще хуже, мы имитируем тип, которым не владеем: HttpRequest предоставляется фреймворком, а не находится под нашим контролем.Мудрость «не мокайте то, что вам не принадлежит» берет свое начало в сообществе «Лондонской школы разработки, основанной на тестировании». Как написали Стив Фриман и Нат Прайс в 2009 году в статье «Развитие объектно-ориентированного программного обеспечения с помощью тестов»:
«Мы обнаружили, что тесты, мокающие внешние библиотеки, часто должны быть сложными, чтобы привести код в правильное состояние для функциональности, которая нам нужна. Беспорядок в таких тестах говорит нам, что дизайн неправильный, но вместо того, чтобы исправить проблему улучшением кода, мы должны вносить дополнительную сложность как в код, так и в тесты».
Но если мы не должны мокать то, что нам не принадлежит, то как нам изолировать наш код от стороннего кода? Стив Фриман и Нат Прайс продолжили:
«Мы [...] проектируем интерфейсы для сервисов, которые нужны для наших объектов, - интерфейсов, которые будут определяться в терминах домена наших объектов, а не внешней библиотеки. Мы пишем слой адаптера [...], который использует третье-сторонний API для реализации этих интерфейсов [...] "
Давайте применим это к нашему коду:
interface SomeRequestInterface
{
public function getId(): string;
public function getAmount(): string;
public function getPrice(): string;
}
Вместо того, чтобы просто возвращать строку, теперь мы можем использовать конкретные типы или даже value-объекты. Однако в этом примере мы будем придерживаться строк.Создать тестового двойника для SomeRequestInterface очень просто:
$request = $this->createStub(SomeRequestInterface::class);
$request->method('getId')
->willReturn(1);
$request->method('getAmount')
->willReturn(2);
$request->method('getPrice')
->willReturn(3);
С точки зрения фреймворка, стандартный объект HTTP-запроса является правильной абстракцией, потому что это работа фреймворка - представлять входящий HTTP-запрос в виде объекта. Однако это не должно мешать нам поступать правильно. Мы можем сопоставить общий объект HTTP-запроса фреймворка с нашим конкретным объектом запроса. Нам даже не нужен отдельный маппер. Мы можем просто обернуть общий запрос:
class SomeRequest implements SomeRequestInterface
{
private HttpRequest $request;
public function __construct(HttpRequest $request)
{
$this->request = $request;
}
public function getId(): string
{
return $this->request->get('id');
}
public function getAmount(): string
{
return $this->request->get('amount');
}
public function getPrice(): string
{
return $this->request->get('price');
}
}
И вот как мы заставляем этот код работать вместе:
class SomeController
{
public function execute(HttpRequest $request)
{
return $this->executable->execute(
new SomeRequest($request)
)
}
}
Даже если SomeController является подклассом базового класса контроллера, предоставляемого фреймворком, ваш фактический код остаётся независимым от HTTP абстракции фреймворка. Вы, конечно, должны будете делать свою обертку request'a, специфичную для каждого контроллера. Вашему коду нужны определенные заголовки? Создайте метод, чтобы просто получить их. Вашему коду нужен загруженный файл? Создайте метод для получения именно этого.Полный HTTP-запрос может содержать заголовки, значения, возможно, загруженные файлы, тело POST и т. д. Настройка тестовой заглушки или mock'а для всего этого, пока вы не владеете интерфейсом, мешает вам выполнить работу. Определение собственного интерфейса значительно упрощает задачу.
===========
Источник:
habr.com
===========
===========
Автор оригинала: Stefan Priebsch, Sebastian Bergmann
===========Похожие новости:
- [PHP, Программирование] От версии 8 к 8.1: новый виток развития PHP (перевод)
- [Тестирование IT-систем, Go, Тестирование веб-сервисов] Подсказки по написанию тестов в приложениях на Go
- [Системное администрирование, PHP, Программирование, Разработка систем связи] Голосовое меню своими руками
- [Управление разработкой, Управление проектами] Как работать в команде, которая пишет на 5 языках
- [PHP, Тестирование веб-сервисов] Так как же не страдать от функциональных тестов?
- [Разработка веб-сайтов, PHP, Проектирование и рефакторинг] Ты приходишь в проект, а там легаси…
- [PHP] Мифы об асинхронном PHP: он не по-настоящему асинхронный (перевод)
- [PHP, API, CRM-системы] Заметки по API Aliexpress. Экспорт заказов в Bitrix24, RetailCRM, amoCRM
- [PHP, Go, Параллельное программирование, Изучение языков, Микросервисы] Тонкости реализации Singleton на Golang
- [Разработка веб-сайтов, PHP, Symfony, Конференции] Говорим, как структурировать код
Теги для поиска: #_php, #_php, #_phpunit, #_unittesting, #_unittestirovanie (unit-тестирование), #_nikto_ne_chitaet_tegi (никто не читает теги), #_php
Вы не можете начинать темы
Вы не можете отвечать на сообщения
Вы не можете редактировать свои сообщения
Вы не можете удалять свои сообщения
Вы не можете голосовать в опросах
Вы не можете прикреплять файлы к сообщениям
Вы не можете скачивать файлы
Текущее время: 22-Ноя 19:12
Часовой пояс: UTC + 5
Автор | Сообщение |
---|---|
news_bot ®
Стаж: 6 лет 9 месяцев |
|
Прим. переводчика: само правило достаточно старое, да и пример, приведенный в статье - на мой взгляд самый простой. Поэтому статья подойдет скорее для новичков, люди с хорошим опытом написания автотестов, возможно, не найдут для себя ничего нового.Веб-приложения зачастую созданы для обработки HTTP-запросов. Обычно объекты используются для инкапсуляции данных запроса. В зависимости от фреймворка у нас может быть такой интерфейс, как interface HttpRequest
{ public function get(string $name): string; // ... } class HttpRequest
{ public function get(string $name): string { // ... } // ... } class SomeController
{ public function execute(HttpRequest $request): HttpResponse { $id = $request->get('id'); $amount = $request->get('amount'); $price = $request->get('price'); // ... } } $request = $this->createStub(HttpRequest::class);
$request->method('get') ->willReturnOnConsecutiveCalls( '1', '2', '3', ); $controller = new SomeController; $controller->execute($request); $request = $this->createMock(HttpRequest::class);
$request->expects($this->exactly(3)) ->method('get') ->withConsecutive( ['id'], ['amount'], ['price'] ) ->willReturnOnConsecutiveCalls( '1', '2', '3', ); $controller = new SomeController; $controller->execute($request); «Мы обнаружили, что тесты, мокающие внешние библиотеки, часто должны быть сложными, чтобы привести код в правильное состояние для функциональности, которая нам нужна. Беспорядок в таких тестах говорит нам, что дизайн неправильный, но вместо того, чтобы исправить проблему улучшением кода, мы должны вносить дополнительную сложность как в код, так и в тесты».
«Мы [...] проектируем интерфейсы для сервисов, которые нужны для наших объектов, - интерфейсов, которые будут определяться в терминах домена наших объектов, а не внешней библиотеки. Мы пишем слой адаптера [...], который использует третье-сторонний API для реализации этих интерфейсов [...] "
interface SomeRequestInterface
{ public function getId(): string; public function getAmount(): string; public function getPrice(): string; } $request = $this->createStub(SomeRequestInterface::class);
$request->method('getId') ->willReturn(1); $request->method('getAmount') ->willReturn(2); $request->method('getPrice') ->willReturn(3); class SomeRequest implements SomeRequestInterface
{ private HttpRequest $request; public function __construct(HttpRequest $request) { $this->request = $request; } public function getId(): string { return $this->request->get('id'); } public function getAmount(): string { return $this->request->get('amount'); } public function getPrice(): string { return $this->request->get('price'); } } class SomeController
{ public function execute(HttpRequest $request) { return $this->executable->execute( new SomeRequest($request) ) } } =========== Источник: habr.com =========== =========== Автор оригинала: Stefan Priebsch, Sebastian Bergmann ===========Похожие новости:
|
|
Вы не можете начинать темы
Вы не можете отвечать на сообщения
Вы не можете редактировать свои сообщения
Вы не можете удалять свои сообщения
Вы не можете голосовать в опросах
Вы не можете прикреплять файлы к сообщениям
Вы не можете скачивать файлы
Вы не можете отвечать на сообщения
Вы не можете редактировать свои сообщения
Вы не можете удалять свои сообщения
Вы не можете голосовать в опросах
Вы не можете прикреплять файлы к сообщениям
Вы не можете скачивать файлы
Текущее время: 22-Ноя 19:12
Часовой пояс: UTC + 5