Stub і Mock (стаб і мок) – це дві різновидності Test Double, тобто тестового двійника. Тестовий двійник – це обʼєкт, що передається в SUT (систему, що тестується). Крім них, існує ще декілька різновидів, найпопулярніші з них: Dummy, Fake, Spy.
Test Double
|
-> Dummy
|
-> Stub
|
-> Mock
|
-> Fake
|
-> Spy
Найпростішим тестовим двійником є dummy, єдина функція якого – задовільнити умовам інтерфейсу. Методи цього класу не викликаються, чи їх виклик не впливає на тестуєму систему.
Для stub головне – повернути заздалегідь визначені данні, при чьому не важливо, скільки разів ці данні запитувались.
Для mock навпаки, найголовніше – це простежити що визначені методи викликались задану кількість разів. Мок може повертати дані для того, щоб тест пройшов по потрібним гілкам, проте основний акцент залишається на взаємодії тестуємої системи і мока.
Fake – це спрощена реалізація обʼєкта. Прикладом фейка може бути in-memory база даних, що збергіє данні в масив, замість того щоб робити справжній запит у базу даних.
Spy – здатний записувати взаємодію тестуємої системи з обʼєктом, які можна провалідитувати пізніше.
Тестові двійники у PHPUnit
У PHPUnit явним чином виділені сутності stub і mock. Замість dummy можна використовувати звичайний клас, стаб чи мок. Fake – зазвичай імплементується як звийчайний клас. Про використання Spy у PHPUnit є гарна стаття (див. посилання внизу).
Як використовувати stub у тесті?
// Arrange
$dependency = $this->createStub(Dependency::class);
$dependency->method('getExpression')
->willReturn('40 + 2');
$sut = new Calculator(dependency);
// Act
$result = $sut->process();
// Assert
$this->assertEquals(42, $result);
У прикладі вище створюється стаб, який імплементує метод getExpression, що завжди повертає строку з виразом ’40 + 2′. Далі стаб передається в обʼєкт калькулятора як залежність – це єтап arrange. На єтапі act – виконується тестуєма дія. На єтапі assert – перевіряється результат виконуємої дії.
Тестові стаби можуть повертати список завчасно вказаних значень, при послідовних викликах. Приклад з документації PHPUnit:
// Create a stub for the SomeClass class.
$stub = $this->createStub(SomeClass::class);
// Configure the stub.
$stub->method('doSomething')
->willReturn(1, 2, 3);
// $stub->doSomething() returns a different value each time
$this->assertSame(1, $stub->doSomething());
$this->assertSame(2, $stub->doSomething());
$this->assertSame(3, $stub->doSomething());
Також існує спрощенний синтаксис створеня мока, у якому необхідно визначити декілька функцій.
$o = $this->createConfiguredStub(
SomeInterface::class,
[
'doSomething' => 'foo',
'doSomethingElse' => 'bar',
]
);
Не всі IDE підтримують синтаксис createConfiguredStub: строки з назвами методів не сприймаються як методи. Відповідно при рефакторингу коду IDE може не побачити ці методи, що призведе до падіння тесту.
Як використовувати mock у тесті?
// Arrange
$dependency = $this->createMock(Dependency::class);
$dependency->expects($this->once())
->method('fetch')
->willReturn('{"data":"..."}');
$sut = new CacheService(dependency);
// Act
$result = $sut->getData();
// Assert
$this->assertStringEndsWith('{"data":"..."}', $result);
Використання моків схоже з використанням стабів, різниця полягає у єтапі arrange, де налаштовується перевірка на виклики методів тестового двійника. У прикладі використовується обмеження once – на один виклик метода. Якщо у кінці теста метод не викликався чи викликався більше одного разу – тест впаде.
Існує 6 видів обмежень:
- any() – будь яка кількість викликів(навіть 0)
- never() – жодного виклику
- atLeastOnce() – мінімум один виклик
- once() – рівно один виклик
- atMost(int $count) – не більше ніж count разів
- exactly(int $count) – рівно count разів