Co to jest stub object? Do czego służy, jak nam pomaga i jak pomaga nam robić unit testy?
Dziś zajmiemy się zrozumieniem co to jest stub object. W kolejnej części opiszę też mock object. Ale zacznijmy od ich przeznaczenia.
Problem z testami
Dobre unit testy powinny działać szybko. Mają też testować pojedyncze elementy systemu. Ale co zrobić, gdy obiekt zapisuje do bazy, wysyła informacje, pobiera coś z sieci czy robi inne rzeczy na zewnętrznych serwisach?
Przykłady będą w PHP, ale w każdym języku działa to podobnie.
Mój znienawidzony Active Record jest dobrym przykładem. Mamy obiekt, który wypełniamy danymi i zapisujemy. Podczas zapisu dzieją się różne rzeczy – walidacja, wypełnianie pól itp. Weźmy taką metodę w active record:
class User extends ActiveRecord
{
public function createUser($name, $email)
{
$this->name = $name;
$this->e-mail = $email;
if ($this->vaidate()) {
return $this->save();
} else {
return false;
}
}
}
Jak to przetestować? Jak sprawdzić, czy nasza funkcja jest odporna na błędy?
Możemy wrzucać błędne wartości, null zamiast nazwy, pusty tekst zamiast e-maila itp. Ale jeśli funkcja źle działa to naśmieci nam w bazie.
Inny przykład, mamy obiekt wysyłający maile. Niech ma np. funkcję send():
class MyMail {
public function __construct(\Mailer $mailer)
{
$this->mailer = $mailer;
}
public function send($to, $title, $body)
{
if (!filter_var($email, FILTER_VALIDATE_EMAIL)) {
return false;
}
$body .= "\nSent by my mailer";
return $this->mailer->send($to, $title, $body);
}
}
Jak sprawdzić czy dokleja właściwą stopkę? Jak sprawdzić, czy przyjmuje właściwe parametry? Jak sprawdzić, czy weryfikuje dane?
Możemy oczywiście ustawić testową bazę danych lub testowy adres e-mail. Ale grzebanie w bazie lub wysyłanie maili zajmuje czas. Testy jednostkowe mają być szybkie. Dlatego korzystamy z mocków.
Stub object – jak to działa?
W pierwszym przykładzie chcemy wyłączyć zapisywanie do bazy czyli metodę save(). W drugim, chcemy nie wysyłać maila naprawdę a jedynie sprawdzić, czy funkcja poprawnie waliduje e-mail.
Object doble to kopia naszego obiektu ze zmodyfikowaną funkcjonalnością. W PHPUnit tworzymy doubla w ten sposób:
// kopiujemy naszą klasę User
$stub = $this->getMock('User');
// zmieniamy działanie jednej z metod
$stub->method('save')->willReturn(true);
Co tu się stało? W pierwszej linii stworzyliśmy obiekt klasy User, ale taki, którego zachowanie możemy zmieniać. W drugiej zmieniliśmy zachowanie funkcji save(). Teraz funkcja save() nic nie robi, jedynie zwraca true.
Nasz test wygląda teraz łatwo:
class UserTest extends PHPUnit_Framework_TestCase
{
public function testUser()
{
// kopiujemy naszą klasę User
$stub = $this->getMock('User');
// zmieniamy działanie jednej z metod
$stub->method('save')->willReturn(true);
// test niepoprawnego adresu e-mail
$this->assertFalse($stub->createUser('username', 'this-is-not-an-email'));
// test dobrego adresu e-mail
$this->assertTrue($stub->createUser('username', 'valid@e.mail'));
}
}
Gotowe! Mamy test walidacji poprawnych danych! Nic nie zapisuje się do bazy. Sprawdzamy tylko czy medoda działa jak powinna.
W drugim przykładzie jest podobnie. Przetestujemy funckję send podając fikcyjny mailer w konstruktorze:
class MyMailTest extends PHPUnit_Framework_TestCase
{
public function testSend()
{
// kopiujemy klasę Mailer i wyłączamy metodę send()
$stub = $this->getMock('Mailer');
$stub->method('send')->willReturn(true);
// teraz możemy podłożyć nasz obiekt klasie MyMail
$mail = new MyMail($stub);
// test niepoprawnego adresu e-mail
$this->assertFalse($mail->send('this-is-not-an-email', 'title', 'body'));
// test dobrego adresu e-mail
$this->assertTrue($mail->send('valid@e.mail', 'title', 'body'));
}
}
Teraz test działa bez ryzyka wysłania prawdziwego maila.
Co dalej?
W kolejnym wpisie poznamy mock object. Opiszę też czym się różni double, stub i mock. Zobaczymy przykłady jak z nich korzystać.
Warto poczytać dokumentację PHPUnit o object doubles. W praktycznie każdym języku mamy możliwości tworzenia takich obiektów.
Piękne, do rzeczy i najważniejsze pomocne. Dziękuję.
Bardzo prosto i jasno wytłumaczone. Super tłumaczysz!
Marcin