Nest Starter cz. III

Po zdefiniowaniu wymagań pora zabrać się za ich implementacje. Jednak przed tym krokiem możemy sobie pracę ułatwić odpalając komendę bin/behat –apend-snippets. Komenda to dodaje wszystkie potrzebne definicje kroków potrzebnych do przetestowania wymagań.

Okej gdy szkielet klasy FeatureContext.php mamy gotowy to pora zabrać się za implementacje. W tym wpisie zajmiemy się content_page.feature.

Konfiguracja PhpSpec

Do tworzenia wszystkich klas użyjemy narzędzia PhpSpec, jednak aby ono generowało klasy tak jak sobie tego życzymy jest potrzebna odpowiednia konfiguracja. Najpierw najważniejszy plik czyli phpspec.yml:

formatter.name: pretty

suites:
    app:
        namespace: App
        src_path: .
    storage:
        namespace: Plugin\Storage
        src_path: Plugin/Storage
    webserver:
        namespace: Plugin\WebServer
        src_path: Plugin/WebServer

Plik szablonu generacji klas: (.phpspec/class.tpl)

<?php
/**
 * This file is part Nest Static Page application
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 *
 * @license    MIT
 */
namespace %namespace%;

/**
 * %namespace%\%name%
 *
 * @author Tomasz Ślązok <tomek@sabaki.pl>
 */
class %name%
{
}

Szablon generowanych specyfikacji (.phpspec/specification.tpl)

<?php
/**
 * This file is part Nest Static Page application
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 *
 * @license    MIT
 */
namespace %namespace%;

use PhpSpec\ObjectBehavior;
use Prophecy\Argument;

/**
 * %namespace%\%name%
 *
 * @author Tomasz Ślązok <tomek@sabaki.pl>
 * @mixin  \%subject%
 */
class %name% extends ObjectBehavior
{
    function it_is_initializable()
    {
        $this->shouldHaveType('%subject%');
    }
}

Static Page

Gdy PhpSpec jest skonfigurowany to pora na pierwszy UseCase. Odpalamy komende bin/phpspec describe App\\UseCase\\ViewStaticPage a następnie bin/phpspec run i odpwiadamy „Y” na zadane pytanie: Do you want me to create `App\UseCase\ViewStaticPage` for you?

Teraz pora przygotować specyfikacje klasy (jego API). ViewStaticPage bedzie wymagać przy stworzeniu Repository (obiekt za pomocą którego będą pobierane strony statyczne). Implementacja Repository nie jest istotna dla działania UseCase. Wymagane jest by Repository implementowało interfejs App\Repository\PageRepositoryInterface.

<?php
/**
 * This file is part Nest Static Page application
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 *
 * @license    MIT
 */
namespace App\Repository;

/**
 * Interface PageRepository
 *
 * @author  Tomasz Ślązok <tomek@landingi.com>
 */
interface PageRepositoryInterface
{
    public function findBySlug($slug);
}

Do zwrócenia strony będzie służyć metoda execute() która przyjmie jako parametr zmienna $slug. Natomiast metoda zwracać będzie w wyniku obiekt App\Entity\Page. Zaczniemy od specyfikacji. Tak wiec w pliku spec/App/UseCase/ViewStaticPageSpec.php dodajemy specyfikacje metody execute:

<?php
/**
 * This file is part Nest Static Page application
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 *
 * @license    MIT
 */
namespace spec\App\UseCase;

use PhpSpec\ObjectBehavior;

/**
 * spec\App\UseCase\ViewStaticPageSpec
 *
 * @author Tomasz Ślązok <tomek@sabaki.pl>
 * @mixin  \App\UseCase\ViewStaticPage
 */
class ViewStaticPageSpec extends ObjectBehavior
{
    function it_is_initializable()
    {
        $this->shouldHaveType('App\UseCase\ViewStaticPage');
    }

    function it_return_page_for_slug()
    {
        $this->execute('slug')->shouldReturnAnInstanceOf('App\Entity\Page');
    }
}

Nastepnie uzywamy komendy bin/phpspec run aby PhpSpec stworzył metodę execute() za nas. Niestety metoda execute jest pusta i zwraca null. Z tego powodu gdy odpalimy ponownie bin/phpspec run to otrzymamy komunikat o błędzie.

✘ return page for slug 
    expected an instance of App\Entity\Page, but got null. 

Specyfikacja wymaga aby metoda zwróciła entity Page. Nie ma jeszcze takiej klasy więc szybko ją tworzymy za pomocą phpspeca:

> bin/phpspec describe App\\Entity\\Page
Specification for App\Entity\Page created in /home/nexik/projects/nest/nest-static/spec/App/Entity/PageSpec.php.

> bin/phpspec run                                                                        
  Do you want me to create `App\Entity\Page` for you? [Y/n] Y

Class App\Entity\Page created in /home/nexik/projects/nest/nest-static/App/Entity/Page.php.

Narazie nie interesuje nas implementacja klasy Page, tak więc możemy wrócić do definicji metody ViewStaticPage::execute(). Tworzymy kod który spełni wymagania naszej wcześniejszej specyfikacji:

<?php
/**
 * This file is part Nest Static Page application
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 *
 * @license    MIT
 */
namespace App\UseCase;

use App\Entity\Page;

/**
 * App\UseCase\ViewStaticPage
 *
 * @author Tomasz Ślązok <tomek@sabaki.pl>
 */
class ViewStaticPage
{
    public function execute($slug)
    {
        return new Page();
    }
}

Po tej zmianie dostajemy zielone światło, jednak nie jest to końcowy efekt jaki byśmy chcieli, dlatego musimy zmodyfikować trochę specyfikacje. Klasa ViewStaticPage nie powinna być odpowiedzialna za tworzenie obiektu Page. Jak już wcześniej pisałem, zadanie to należy wydelegować do obiektu Repository. Dodajemy odpowiednią zależność do specyfikacji:

<?php
/**
 * This file is part Nest Static Page application
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 *
 * @license    MIT
 */

namespace spec\App\UseCase;

use App\Entity\Page;
use App\Repository\PageRepositoryInterface as Repository;
use PhpSpec\ObjectBehavior;
use Prophecy\Argument;

/**
 * spec\App\UseCase\ViewStaticPageSpec
 *
 * @author Tomasz Ślązok <tomek@sabaki.pl>
 * @mixin  \App\UseCase\ViewStaticPage
 */
class ViewStaticPageSpec extends ObjectBehavior
{
    function let(Repository $repository)
    {
        $this->beConstructedWith($repository);
    }

    function it_return_page_for_slug(Repository $repository)
    {
        $repository->findBySlug('slug')->willReturn(new Page());

        $this->execute('slug')->shouldReturnAnInstanceOf('App\Entity\Page');
    }
}

Dokonaliśmy kilka ciekawych rzeczy.

  • Najpierw usuneliśmy specyfikacje it_is_initializable, ponieważ jest to domyślna specyfikacja która po stworzeniu klasy ma bardzo małą wartość. Czym mniej niepotrzebnego kodu tym lepiej.
  • Następnie uzyliśmy w specyfikacji metody let(). Metoda ta służy do przygotowania obiektu którego testujemy. Metoda let() służy także jako specyfikacja constructora.
  • W metodzie let skorzystaliśmy z wewnętrznego mechanizmu mockowania jaki dostarcza PhpSpec. System mockowania służy do uproszczenia budowy zależności pomiędzy obiektami. Podczas budowania obiektu repository nie interesuje nas jego implementacja ważne tylko żeby implementował odpowiedni interfejs
  • W metodzie let() zdefiniowaliśmy zachowanie $repository->findBySlug(). Jeżeli zostanie przekazany jako parametr string slug to repository zwróci obiekt Page
  • W specyfikacji it_return_page_for_slug() zdefiniowalismy zachowanie repository dla wywolania findBySlug(‚slug’)

Po tych zmianach gdy odpalimy bin/phpspec run to dostaniemy komunikat o błędzie:

> bin/phpspec run
✘ return page for slug
   some predictions failed:
      Double\PageRepositoryInterface\PageRepositoryInterface\P1:
        No calls been made that match:
          Double\PageRepositoryInterface\PageRepositoryInterface\P1->findBySlug(exact("slug"))
        but expected at least one.

Musimy zaktualizować metodę ViewStaticPage::execute() aby korzystała z repository:

<?php
/**
 * This file is part Nest Static Page application
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 *
 * @license    MIT
 */
namespace App\UseCase;

use App\Entity\Page;
use App\Repository\PageRepositoryInterface as Repository;

/**
 * App\UseCaseViewStaticPage
 *
 * @author Tomasz Ślązok <tomek@sabaki.pl>
 */
class ViewStaticPage
{
    /**
     * @var \App\Repository\PageRepositoryInterface
     */
    private $repository;

    /**
     * Constructor
     *
     * @param Repository $repository
     */
    public function __construct(Repository $repository)
    {
        $this->repository = $repository;
    }

    /**
     * Get static page
     *
     * @param $slug
     * @return \App\Entity\Page
     */
    public function execute($slug)
    {
        return $this->repository->findBySlug($slug);
    }
}

Teraz komenda bin/phpspec run zakończy się sukcesem. Trochę się nazbierało, następnym razem zabierzemy się za implementacje Repository dla stron statycznych w oparciu o pliki yaml a potem już tylko krok do napisania testów funkcjonalnych do Behata.

5 comments

  1. Hej, fajny artykul!

    Mala uwaga: Twoj ostatni spec moglby byc troche poprawiony.

    Wchodzac w interakcje z metodami typu query, jaka jest metoda repozytorium, nie interesuje mnie tak naprawde czy zostala wykonana, wiec jej nie mockuje (shouldBeCalled), ale stubuje (willReturn). Oczekuje z kolei, ze to co zostanie zwrocone, to wynik zapytania na repozytorium:

    function it_returns_page_for_slug(Repository $repository, Page $page)
    {
    $repository->findBySlug(‚slug’)->willReturn($page);

    $this->execute(‚slug’)->shouldReturn($page);
    }

    Zgodnie z zasada Command/Query separation, metoda albo cos robi, albo cos zwraca, wiec raczej nie mieszamy shouldBeCalled() z willReturn() (albo mockujemy albo stubujemy).

    1. Mala niescislosc w moim ostatnim komentarzu :P

      W ostatnim paragrafie chodzilo mi o to, ze nie mieszamy shouldBeCalled() z willReturn() w JEDNYM obiekcie. Oczywiscie w jednej specyfikacji mozemy miec stuby i mocki na roznych obiektach :)

    2. Wprowadziłem zmiany w artykule.
      Dzieki za radę i zwrócenie uwagi na zasadę Command/Query separation.

      1. Tak, jednak zostawiłem to na następną cześć, ponieważ obecnie wszystkie obiekty Page są takie same.

        Staram się w tych artykułach zachować moj proces myślenia i stopniowego pisania kodu. Żeby czytelnicy zwrócili uwagę że pisanie składa się z wielu małych etapów.

Dodaj komentarz

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

Możesz użyć następujących tagów oraz atrybutów HTML-a: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <strike> <strong>