Testowanie dziedziczących kontrolerów

0

Cześć,

Zastanawiam się jak powinny wyglądać testy małej aplikacji typu CRUD, powiedzmy 20 kontrolerów, wszystkie dziedziczą po kontrolerze bazowym, 5 z nich posiada własną implementację funkcji pod CRUD, reszta zasadniczo robi to samo ale bazuje na różnych właściwościach poszczególnego kontrolera.

I teraz czy testy powinny wyglądać tak że dla tych 5 piszę każdy test odrębnie (logiczne - różnią się implementacją), a dla pozostałych tworze jeden test bazując na jednym z tych 15 pozostałych kontrolerów?
Czy może powinienem zrobić pętle na teście i robić test dla każdego kontrolera?
A może losować jeden z nich i go testować?

Te 15 kontrolerów różni się tylko właściwościami (inne requesty, inne modele lecz zasadniczo funkcjonalność CRUD spełnia się dla nich wszystkich w kontrolerze bazowym).
Pytanie może o podstawy ale chciałem upewnić się czy myślę podobnie.
Aktualnie mam tak że dla takich funkcjonalności losuje sobie po prostu testowany element lecz nie wiem czy jest to na tyle dobre gdyż może łamać regułę powtarzalności *(?), czy może jednak w testach wszystkie elementy powinny być na sztywno?

Doszedłem po prostu do miejsca w którym mam wrażenie że metoda testująca, nie tylko testuje ale w dużej mierze sama wylicza potrzebne do testu elementy, np. do przetestowania losowego kontrolera muszę pobrać dla niego przykładowy model, pobrać zasady walidacji, wygenerować dane i dopiero wykonać faktyczny test np. tworzenia/edycji.

Wszelkie rady mile widziane.
Mam nadzieję że jasno to opisałem.

2

Piszesz ze bazuja na jakichś tam właściwościach kontrolera? Co masz na myśli?
Bo w tym fragmencie, troszkę tego nie rozumiem

Te 15 kontrolerów różni się tylko właściwościami (inne requesty, inne modele lecz zasadniczo funkcjonalność CRUD spełnia się dla nich wszystkich w kontrolerze bazowym).

Czym innym mogą się różnić te kontrolery? Inne requesty, inne responsy i inne metody, używające różnych metod z formularzy/service'ow.

Poza tym... Ja bym skupił się ciutke na nazewnictwie i pisał o testowaniu metod, a nie kontrolera. Poza tym załóżmy że w bazowym kontrolerze masz 5 metod.No to piszesz do nich testy. W kontrolerach dziedziczących masz inne metody/implementację to je także testujesz. Nieważne ile masz kontrolerów, ważne ile masz metod i testów do nich. Tak ja to widzę.

BTW czy Ty w testach losujesz dane? Jeśli tak, to czy w Twoim przypadku może być tak, że dla jednych dane testy przejdą, a dla innych nie? Bo jakoś nie widzę sensu w losowaniu czegokolwiek.

0

@GoHard: Najpierw piszesz testy potem piszesz kontrolery

0

@axelbest:
Przykład 2 kontrolery dziedziczące po bazowym, każdy z kontrolerów posiada właściwość Model oraz arrayke wskazującą jaki Request stosować do danej metody. Wszystkie dziedziczą 5 metod index, create, edit, update i destroy z kontrolera bazowego. Może żywy przykład na kodzie:

<?php


class BaseController {

    // Model
    protected $model;

    // Request array
    protected $requestArray = [];

    /**
     * Index, listing of all available items
     */
    public function index() {
        // Pobranie requesta dla tego typu
        $request = $this->requestArray['index'];

        // Walidacja requestu (dla każdego kontrolera dziedziczącego będzie to inna klasa)
        $validatedInput = $request->validated();

        // wyświetlenie cruda uruchamiając odpowiedni widok zależnie od modelu
    }

    /**
     * Display create form template for resource
     */
    public function create() {
        // Pobranie requesta dla tego typu
        $request = $this->requestArray['create'];

        // Walidacja requestu (dla każdego kontrolera dziedziczącego będzie to inna klasa)
        $validatedInput = $request->validated();

        // wyświetlenie formularza dodania dla cruda uruchamiając odpowiedni widok zależnie od modelu  i spełnionych warunków requesta
    }

    /**
     * Display edit form for specific entity
     */
    public function edit(Request $request, int $id) {
        // Pobranie requesta dla tego typu
        $request = $this->requestArray['edit'];

        // Walidacja requestu (dla każdego kontrolera dziedziczącego będzie to inna klasa)
        $validatedInput = $request->validated();

        // wyświetlenie formularza edycji elementu dla cruda uruchamiając odpowiedni widok zależnie od modelu i spełnionych warunków requesta
    }

    /**
     * Store new entity in database
     */
    public function store() {
        // Pobranie requesta dla tego typu
        $request = $this->requestArray['store'];

        // Walidacja requestu (dla każdego kontrolera dziedziczącego będzie to inna klasa)
        $validatedInput = $request->validated();

        // Implementacja tworząca element i zapisująca do bazy
    }

    /**
     * Save existing entity in database
     *
     * @param int $id
     */
    public function update(int $id) {
        // Pobranie requesta dla tego typu
        $request = $this->requestArray['update'];

        // Walidacja requestu (dla każdego kontrolera dziedziczącego będzie to inna klasa)
        $validatedInput = $request->validated();

        // Implementacja aktualizująca element i zapisująca do bazy
    }

    /**
     * Delete entity
     *
     * @param int $id
     */
    public function destroy(int $id) {
        // Pobranie requesta dla tego typu
        $request = $this->requestArray['destroy'];

        // Walidacja requestu (dla każdego kontrolera dziedziczącego będzie to inna klasa)
        $validatedInput = $request->validated();

        // Implementacja usuwania elementu z bazy
    }
}

I teraz przykładowo dwa kontrolery dziedziczące (kontroler produktów i artykułów):

<?php

use App\Model\Article;
use App\Requests\{CreateArticleRequest, UpdateArticleRequest};

class ArticlesController extends BaseController  {

    /**
     * @var Model
     */
    protected $model = Article::class;

    /**
     * @var array
     */
    protected $requestArray = [
        'store' => CreateArticleRequest::class,
        'update' => UpdateArticleRequest::class
    ];
}
<?php

use App\Model\Product;
use App\Requests\{CreateProductRequest, UpdateProductRequest};

class ProductsController extends BaseController  {

    /**
     * @var Model
     */
    protected $model = Product::class;

    /**
     * @var array
     */
    protected $requestArray = [
        'store' => CreateProductRequest::class,
        'update' => UpdateProductRequest::class
    ];
}

Z kolei klasy Requestowe mają te same metody do walidacji, jednak walidują różne pola, więc dla każdego kontrolera walidacja może przebiegać nieco inaczej. I tu pojawia się pytanie, powinienem testować jeden z dwóch kontrolerów (ponieważ faktyczna implementacja jest w BaseController)? Czy może powinienem testować każdy z osobna, czy losowo jeden z nich?

1

Wg mnie tworzenie kontrolerow w ten sposob jest niezbyt dobre. Czemu unikasz wpakowania modelu np article, do wnetrza metody? Tutaj wg mnie brakuje Ci raczej Dependency Injection. Poza tym, metody jakie masz w baseControlerze tez sa ciutke niefajne, przez co Twoje kontrolery maja zbyt wiele powiazan. Powiem tak, obecnie w projektach ktorymi sie zajmuje, kontrolery wygladaja tak, ze jestem w stanie przeniesc metode do innego kontrolera i to tylko tyle (dochodza tylko importy za pomoca use). Z tego powodu nie musze kombinowac tak jak Ty teraz czy costam losowo testowac. Jedna metoda to przynajmniej jeden test.

Mam wrazenie ze to Twoj wlasny framework/kod odpowiadajacy za zadania. Zobacz jak takie rzeczy dzialaja w symfony, zendzie czy laravelu. W Twoi przypadku zastanowilby sie tez czy nie uzyc jakiegos psr’a (chyba 7), ktory odpowiada za requesty/responsy. Gotowe implementacje znajdziesz szybko.

Pamietaj tez ze czesto nie warto upraszczac kod do jakichs mikro rozmiarow, tak jak u Ciebie. Po prostu widzac takie kontroleryy jaki Articles i Products, widze ze cos tu poszlo nie tak. Przeciez cala logika siedzi w base. Nie zawsze dziedziczenie jest najlepszym roziwazaniem, co czesto obserwuje u osob poczatkujacych (sam tez kiedys mocno przesadzalem z dziedziczeniem). Pomysl lepiej nad kompozycja.

1

No to tak:

  1. Masz w kodzie baseControllera takie cos „Store new entity in database”. Wydziel ta odpowiedzialnosc do jakiejs klasy poza kontrolerem. Ogolnie kontroler powinien robic jak najmniej, przyjac request, odpytac odpowiednia warstwe z aplikacji i wypluc response. Czasem mozna sie pokusic o jeszcze obsluge formularzy czy autoryzacje. Tak wiec zapis czegos w bazie zrob sobie w klase ArticleRepository.

  2. Jesli widzisz ze jakis kod sie powtarza (to dobrze ogolnie, ze widzisz to i nie olewasz tematu), to tez zastanow sie czy wydzielajac do go klasy rodzica, nie bedziesz potem musial czegos nadpisywac. Zastanow sie tez czy duplikacji ulega cala metoda czy tylko fragment?

  3. Pamietaj tez o single responsibility. Obecnie baseController robi duzo, jest tak uniwersalny, ze jak masz 20 kontrolerow, to mala zmiana w baseControlerze moze Ci rozsadzic funkcjonalnosci w pozostalych 19 kontrolerach.

  4. Obecnie nadpisujesz metody (nie wiem czy w ich wntrzu nie odpalasz metod parent::), ale moze jak masz tam sporo crud’ow to moze lepiej niech te kontrolery maja jakis wspolny interfejs.

  5. Pomysl tez co bedzie jak Ci ten kontroler spuchnie i nagle staniesz przed pomyslem - „a moze by tak zrobic drugi bazowy kontroler z innymi metodami?” Jesli dojdziesz do takiego pytania to zawroc, bo raczej na pewno poszedles w zla strone.

  6. Pewne wspolne rzeczy mozna wydzielic do traitow. A te tez da sie testowac (moze nie tak latwo jak zwykle klasy, ale cos tam mozna)

  7. No i jak tam cos z laravelem dlubiesz to ten jego Eloquent ma na 100% repository do encji.

1

Odnosnie pisania testow - jesli piszesz je najpierw to super. Jesli napisales testy, a potem pakujesz sporo kodu w metode, do ktorej masz juz test, to wlasnie tu popelniasz blad. Powinienes najpierw modfykowac test, a dopiero potem wlasciwa metode.

Ja testy zaczalem pisac pozno i tutaj tez wyszla ciekawa rzecz, mianowicie - testy, pisze sie latwiej, gdy masz dobry kod. Bardzo czesto odbijalem sie na tym, ze ciezko mi bylo napisac testy do niektorych metod, ale to juz wynikalo z tego, ze w danych metodach bylo zbyt duzo kodu/odpowiedzialnosci. Wlasnie wtedy docieralo do mnie, ze kod wlasciwy jest do kitu, bo to test uzmyslowil mi, ze to juz bedzie rzezbienie, ze tak ujmę, w czekoladzie.

0

@axelbest:

  1. W tym przypadku w base kontrolerze trzymam całe metody (i robię je tak jak wspomniałeś uniwersalnie), we właściwych kontrolerach jeśli implementacja się różni dopiero tworzę tam implementację (override) bez wywołania parent::.

  2. No i właśnie to jest mega słaby punkt którego nie zauważałem. Rozumiem że trzymając implementację w każdym kontrolerze osobno (choć różnią się drobnymi elementami), spełnię single responsibility ale przynajmniej częściowo będę duplikował kod, więc i tak nie jest idealnie?

  3. Tak interfejs jest dla mnie jasny, w/w kod jest tylko poglądowy.

  4. Tak jak pkt. 3, dzięki za wskazanie dużego minusu, z początku uznałem że to tylko crud, więc większość spełni się wspólnie ale faktycznie przyblokuje to kolejne modyfikacje.

  5. Tak korzystam, może zbyt mało - z drugiej strony często przeraża mnie wężyk traitów przy klasie np. typu User.

  6. Nie korzystałem, oczywiście już udało mi się znaleźć odpowiednie tutki więc będę zmieniał tą implementację.

Wyzeruje tą implementację i spóbuję od nowa - dzięki za informacje nim nie zaszło to za daleko.

1 użytkowników online, w tym zalogowanych: 0, gości: 1