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