@Tokenchik

Какой смысл mock объектов для юнит тестирования своего кода?

Всем привет, использую phpunit для тестов, столкнулся с непонимаем смысла мока классов и их методов.
Ситуация которая мне понятна - мок http клиента который возвращает конкрентный результат и завязан на внешний сервис, но как быть с простыми классами, которые не связаны с внешним миром?
Есть небольшой пример:
<?php
class SomeClass
{
    public function doSomething(): string
    {
        return 'foo';
    }
}

<?php
use PHPUnit\Framework\TestCase;

class StubTest extends TestCase
{
    public function testStub(): void
    {
        // Создать заглушку для класса SomeClass.
        $stub = $this->createMock(SomeClass::class);

        // Настроить заглушку.
        $stub->method('doSomething')
             ->willReturn('foo');

        // Вызов $stub->doSomething() теперь вернёт 'foo'.
        $this->assertSame('foo', $stub->doSomething());
    }
}


Какой смысл мока для таких классов? Можно ведь что угодно накидать в мок метода и в реальной жизни получать совсем не то, что проверяется во время такого теста.
  • Вопрос задан
  • 131 просмотр
Решения вопроса 4
@HellWalk
Смысл моков - эмулировать объекты с определенным поведением.

Самый банальный пример, помимо http запросов, это эмулирование неправильных объектов.

Допустим, у вас есть сервис, который обрабатывает какой-то объект. Объект написан хорошо, с валидацией данных, и его поведение корректное. Но чтобы вам написать качественный сервис - он не должен полагаться на то, что другой объект ведет себя корректно. Он должен дополнительно проверять граничные ситуации. И разумеется, на такие кейсы нужно написать тесты, а как их написать, если тестируемый объект написан так, что он ведет себя корректно? Вот здесь и приходят на помощь моки.

В phpunit есть функционал подсчета покрытия кода тестами - попробуйте на каком-нибудь относительно небольшом модуле добиться 100% покрытия кода тестами - вам обязательно придется использовать хитрые моки, эмулирующие нестандартное поведение объектов.

P.S. Если вы недавно знакомы с юнит-тестами - непонимание моков нормально. Если будете стремиться писать надежный код, с качественным покрытием кода тестами (здесь самое сложное - предугадать все плохие кейсы, которые будут пытаться сломать ваш код) - понимание придет.
Ответ написан
dmitriylanets
@dmitriylanets
веб-разработчик
Когда вы тестируете метод А вашего класса, вы проверяете его логику а не логику других (замоканых методов), может быть ситуация когда ваш метод А работает по логике корректно, а вот другой метод Б другого класса используемый в тестируемом методе выдает ошибку, возникает вопрос нужно ли считать что ваш метода А работает неправильно из за упавшего метода Б ?
Ответ написан
@oxidmod
В таком виде как в примере - смысла нет
Смысл будет вот такое

class SomeClass
{
    public function doSomething(): string
    {
        $result = .... // some complicated logic

        return $result;
    }
}

class AnotherClass {
    private SomeClass $dependency;

    public function __construct(SomeClass $dependency) {
        $this->dependency = $dependency;
    }

    public function anotherMethod(): int {
        $val = $this->dependency->doSomething();
        
        if (empty($val)) {
             throw new InvalidArgumentException('invalid val received');
        }

        return strlen($val);
    }
}


пример конечно надуманный, но суть в том, что в зависимости от результата работы SomeClass у тебя разное поведение в AnotherClass
А вот этот разный результат из SomeClass может получиться в зависимости т кучи разных начальных условий, вплоть до того, что в тесте ты их повторить не сможешь (например используется рандом и нет гарантии что в тесте у тебя сгенерится нужное значение). Как тогда протестить AnotherClass?
Ты исходишь из того, что SomeClass может вернуть пустую строку либо не пустую, это условно говоря, его контракт
Ты пишешь тест для AnotherClass

class AnotherClassTest extends TestCase
{
    private $mock;

    private $testingUnit;

    protected function setUp() {
        $this->mock = $this->createMock(SomeClass::class);
        $this->testingUnit = new AnotherClass($this->mock);
    }

    public function testAnotherMethod(): void
    {
        // Настроить заглушку.
        $this->mock->method('doSomething')
             ->willReturn('foo');

        $this->assertSame(3, $this->testingUnit->anotherMethod());
    }

    public function testAnotherMethodException(): void
    {
        // Настроить заглушку.
        $this->mock->method('doSomething')->willReturn('');

        $this->expectException(InvalidArgumentException::class);
        $this->expectExceptionMessage('invalid val received');
        
        $this->testingUnit->anotherMethod()
    }
}


Теперь в твоем тесте для AnotherClass тебе не нужно думать как подгадать, чтоб зависимость вернула правильный результат. Ты просто говоришь - верни такое значение и все =)
Ответ написан
Отрывок из книги.
Одной из важных особенностей unit-тестирования является тестирование в
изоляции. Unit (класс, функция или другой модуль) должен быть изолирован
от всего остального мира.Это будет гарантировать, что тест тестирует только
этот модуль. Тест может упасть только по двум причинам: неправильный тест
или неправильный код тестируемого модуля. Тестирование в изоляции даёт
нам эту простоту и быстродействие

Обычно зависимость - это интерфейс, который имеет несколько реализа-
ций. Использование реальных реализаций этого интерфейса во время unit-
тестирования - плохая идея, поскольку там могут проводиться те самые опера-
ции ввода-вывода, замедляющие тестирование и не дающие провести тести-
рование этого модуля в изоляции. Прогон unit-тестов должен быть быстр как
молния, поскольку запускаться они будут часто и важно, чтобы разработчик
запустив их не потерял фокус над кодом. Написал код - прогнал тесты, еще
написал код - прогнал тесты. Быстрые тесты позволят ему оставаться более
продуктивным, не позволяя отвлекаться. Решение в лоб задачи изоляции
класса от зависимостей - создание отдельной реализации этого интерфейса,
предназначенного просто для тестирования.

Так вот суть в том, чтобы точечно проверить текущий класс на его функциональность. Моки удодно делать на основе общего интерфейса. Всё мокать не нужно. Но если ваш класс работает с апи, бд, большим обьемом данных. То такая зависимость это плохо.
Цель теста проверить текущий класс на соответствие требований. А какие данные настоящие или фейк это не важно.

К примеру есть класс Заказ. Нам нужно его тестировать. У него есть зависимости: Налог и прочее. Класс налога может быть класс, который получает Значение по api. Так вот есть смысл при тестировании Заказа создать мок Налога
$tax = $this->createMock(Tax::class);
$tax->method('calculateTax')
->willReturn(0);

$newOrder = new Order($tax);

Дальше идут тесты класс Order. Они будут изолированные. Есть основной класс, который ты тестируешь. Для него ты Мок не создаешь. А вот для классов с зависимостями создаются Моки, но не всегда. Если класс зависимостей например содержит просто константы. То создавать Мок и дублировать код смысла нет
Ответ написан
Пригласить эксперта
Ваш ответ на вопрос

Войдите, чтобы написать ответ

Войти через центр авторизации
Похожие вопросы