Mock object – co to jest i do czego służy?

Poprzednio pisałem o obiektach stub i jak nam pomagają w testowaniu. Dziś zajmiemy się mockowaniem.

Obiekty mock to prawie to samo co stub z tą różnicą, że pozwajalą nam sprawdzić, co zostało wywołane. Użyjmy przykładów z poprzedniego wpisu.

Przykład pierwszy – active record.

Mamy kod obiektu active record, który zapisuje użytkownika:

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 sprawdzić, czy metoda save() jest faktycznie wywoływana? Jak sprawdzić, że nie jest wywołana gdy podamy błędne dane? Do tego służy właśnie obiekt mock. W PHPUnit definiujemy go w ten sposób:

class UserTest extends PHPUnit_Framework_TestCase
{
   public function testUser()
   {
      // kopiujemy naszą klasę User
      $mock = $this->getMock('User');

      // oczekujemy, że metoda zostanie wywołana tylko raz
      $mock->expects($this->once())
         ->method('save')
         ->willReturn(true);

      // test dobrego adresu e-mail
      $this->assertTrue($mock->createUser('username','valid@e.mail'));
   }
}

Najważniejsza jest tu linia definiująca metodę:

// oczekujemy, że metoda zostanie wywołana raz
$mock->expects($this->once())
   ->method('save')
   ->willReturn(true);

W podobny sposób możemy przetestować, że metoda nie powinna zostać wywołana (podajemy w parametrze $this->never()):

class UserTest extends PHPUnit_Framework_TestCase
{
   public function testUser()
   {
      // kopiujemy naszą klasę User
      $mock = $this->getMock('User');

      // oczekujemy, że metoda zostanie wywołana tylko raz
      $mock->expects($this->never())
         ->method('save')
         ->willReturn(true);

      // test niepoprawnego adresu e-mail
      $this->assertFalse($mock->createUser('username','this-is-not-an-email'));
   }
}

Co tu się dzieje? Gdy stworzyliśmy obiekt mock ustawiliśmy mu oczekiwania. Obiekt spodziewa się, że metoda save() zostanie wywołana raz (w pierwszym przypadku) lub że nie zostanie wywołana (w drugim). Jeśli rzeczywistość nie będzie zgodna z oczekiwaniami cały test zwróci błąd.

Przykład drugi – wysyłanie maili

Przypomnijmy kod mailera z poprzedniego wpisu:

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);
   }
}

Wiemy jak przetestować walidację e-maila. Ale jak sprawdzić, czy nasza funkcja dokleja właściwą stopkę?

Znów posłużymy się mockiem. Tym razem będziemy sprawdzać, czy dostajemy właściwe parametry.

class MyMailTest extends PHPUnit_Framework_TestCase
{
   public function testSend()
   {
      // kopiujemy klasę Mailer i wyłączamy metodę send()
      $stub = $this->getMock('Mailer');
      $stub->expects($this->once())
         // oczekujemy, że metoda send ...
         ->method('send')
         // zostanie wywołana z ...
         ->with(
            // pierwszym parametrem - jakimkolwiek ...
            $this->anything(),
            // drugim parametrem - tytułem ...
            $this->equalTo('title'),
            // trzecim parametrem zawierającym stopkę ...
            $this->stringContains("\nSent by my mailer")
         );
         // a na koniec zwróci true
         ->willReturn(true);

      // teraz możemy podłożyć nasz obiekt klasie MyMail
      $mail = new MyMail($stub);

      // test dobrego adresu e-mail
      $this->assertTrue($mail->send('valid@e.mail','title','body'));
   }
}

Znów ogromną część testowania wykonał za nas obiekt mock. Nie trzeba pisać asercji. Na koniec testu obiekt nakrzyczy jeśli nie spełnimy jego oczekiwań.

Double, stub czy mock?

Czym więc się różnią obiekty double, stuby i mocki? Podsumujmy:

  • Double to po prostu duplikat obiektu, który tworzymy aby pomóc w testowaniu.
  • Stub to pusty obiekt, który zamiast prawdziwych metod ma podstawione… kikuty. Stub to po angielsku kikut :)
  • Mock to obiekt podobny do stuba ale trochę inteligentniejszy, potrafi sprawdzać co zostało wywołane i oczekiwać odpowiednich parametrów.

Widzimy, że duplikaty obiektów bardzo pomagają nam w testach. Unit testy powinny być szybkie. Potrafimy odpiąć obiekty od zewnętrznych serwisów – baz danych, e-maili itp. Testujemy same obiekty, ich zachowanie.

Dzięki dużej prędkości takich testów możemy napisać dużo testów i sprawdzać wszystkie ważne warunki – całą pulę parametrów, które powinny zadziałać dobrze oraz tych, które mają wywołać błędy.

Co dalej?

Warto poczytać artykuł Martina Fowlera o mock obiektach.

Praktycznie każdy framework do testów posiada mocki i stuby. Oprócz PHPUnit mamy je w Codeception, Mockery itp. Warto ich używać aby nasze unit testy były naprawdę testami jednostkowymi.

2 myśli na temat “Mock object – co to jest i do czego służy?”

Dodaj komentarz

Twój adres email nie zostanie opublikowany. Pola, których wypełnienie jest wymagane, są oznaczone symbolem *