Testowanie haków zwrotnych

34

Tworzę wtyczkę za pomocą TDD i jedną rzecz, której całkowicie nie testuję, są ... haki.

Mam na myśli OK, mogę przetestować wywołanie zwrotne haka, ale jak mogę sprawdzić, czy hak faktycznie się wyzwala (zarówno niestandardowe haki, jak i domyślne haki WordPress)? Zakładam, że niektóre kpiny pomogą, ale po prostu nie mogę zrozumieć, czego mi brakuje.

Zainstalowałem pakiet testowy z WP-CLI. Zgodnie z tą odpowiedzią , inithak powinien wyzwolić, ale ... to nie robi; kod działa również w WordPress.

Z mojego zrozumienia, pasek ładujący jest ładowany jako ostatni, więc sensowne jest, aby nie uruchamiać init, więc pozostaje pytanie: jak, do cholery, powinienem sprawdzić, czy haki są wyzwalane?

Dzięki!

Plik bootstrap wygląda następująco:

$_tests_dir = getenv('WP_TESTS_DIR');
if ( !$_tests_dir ) $_tests_dir = '/tmp/wordpress-tests-lib';

require_once $_tests_dir . '/includes/functions.php';

function _manually_load_plugin() {
  require dirname( __FILE__ ) . '/../includes/RegisterCustomPostType.php';
}
tests_add_filter( 'muplugins_loaded', '_manually_load_plugin' );

require $_tests_dir . '/includes/bootstrap.php';

testowany plik wygląda następująco:

class RegisterCustomPostType {
  function __construct()
  {
    add_action( 'init', array( $this, 'register_post_type' ) );
  }

  public function register_post_type()
  {
    register_post_type( 'foo' );
  }
}

I sam test:

class CustomPostTypes extends WP_UnitTestCase {
  function test_custom_post_type_creation()
  {
    $this->assertTrue( post_type_exists( 'foo' ) );
  }
}

Dzięki!

Ionut Staicu
źródło
Jeśli prowadzisz phpunit, czy widzisz testy zakończone niepowodzeniem lub pozytywnie? Czy zainstalowałeś bin/install-wp-tests.sh?
Sven
Myślę, że część problemu polega na tym, że być może RegisterCustomPostType::__construct()nigdy nie jest wywoływana, gdy wtyczka jest ładowana do testów. Możliwe jest również, że dotknął Cię błąd # 29827 ; może spróbuj zaktualizować swoją wersję pakietu testów jednostkowych WP.
JD
@ Sven: tak, testy kończą się niepowodzeniem; zainstalowałem bin/install-wp-tests.sh(ponieważ użyłem wp-cli) @JD: wywoływana jest konstrukcja RegisterCustomPostType :: __ (właśnie dodałem die()instrukcję i phpunit się tam kończy)
Ionut Staicu
Nie jestem zbyt pewny po stronie testów jednostkowych (nie mojej forte), ale z dosłownego punktu widzenia możesz użyć, did_action()aby sprawdzić, czy działania zostały uruchomione.
Rarst
@Rarst: dzięki za sugestię, ale nadal nie działa. Z jakiegoś powodu uważam, że czas jest zły (testy są uruchamiane przed initprzechwyceniem).
Ionut Staicu,

Odpowiedzi:

72

Testuj w izolacji

Podczas opracowywania wtyczki najlepszym sposobem na przetestowanie jej jest bez ładowania środowiska WordPress.

Jeśli napiszesz kod, który można łatwo przetestować bez WordPressa, Twój kod będzie lepszy .

Każdy komponent, który jest testowany jednostkowo, powinien być testowany osobno : podczas testowania klasy wystarczy przetestować tę konkretną klasę, zakładając, że cały inny kod działa idealnie.

Izolator

Z tego powodu testy jednostkowe nazywane są „jednostkami”.

Dodatkową korzyścią jest to, że bez ładowania rdzenia test będzie przebiegał znacznie szybciej.

Unikaj haków w konstruktorze

Wskazówka, którą mogę ci dać, to unikanie wprowadzania haków do konstruktorów. To jedna z rzeczy, która sprawi, że Twój kod będzie testowany w izolacji.

Zobaczmy kod testowy w OP:

class CustomPostTypes extends WP_UnitTestCase {
  function test_custom_post_type_creation() {
    $this->assertTrue( post_type_exists( 'foo' ) );
  }
}

Załóżmy, że ten test się nie powiedzie . Kto jest winowajcą ?

  • hak nie został w ogóle dodany, czy nieprawidłowo?
  • metoda, która rejestruje typ postu, nie została w ogóle wywołana lub zawierała nieprawidłowe argumenty?
  • w WordPress jest błąd?

Jak można to poprawić?

Załóżmy, że twój kod klasy to:

class RegisterCustomPostType {

  function init() {
    add_action( 'init', array( $this, 'register_post_type' ) );
  }

  public function register_post_type() {
    register_post_type( 'foo' );
  }
}

(Uwaga: w dalszej części odniosę się do tej wersji klasy)

Sposób, w jaki napisałem tę klasę, pozwala tworzyć instancje klasy bez wywoływania add_action.

W powyższej klasie do przetestowania są 2 rzeczy:

  • metoda init faktycznie wywołuje add_actionprzekazanie jej odpowiednich argumentów
  • metoda register_post_type faktycznie wywołuje register_post_typefunkcję

Nie powiedziałem, że musisz sprawdzić, czy istnieje typ postu: jeśli dodasz odpowiednią akcję i zadzwonisz register_post_type , niestandardowy typ postu musi istnieć: jeśli nie istnieje, jest to problem WordPress.

Pamiętaj: kiedy testujesz wtyczkę, musisz ją przetestować swój kod, a nie kod WordPress. W testach musisz założyć, że WordPress (podobnie jak każda inna zewnętrzna biblioteka, której używasz) działa dobrze. Takie jest znaczenie testu jednostkowego .

Ale ... w praktyce?

Jeśli WordPress nie jest załadowany, jeśli spróbujesz wywołać metody klas powyżej, pojawi się błąd krytyczny, więc musisz wyśmiać funkcje.

Metoda „ręczna”

Na pewno możesz napisać swoją kpiącą bibliotekę lub „ręcznie” kpić z każdej metody. To jest możliwe. Powiem ci, jak to zrobić, ale potem pokażę ci łatwiejszą metodę.

Jeśli WordPress nie jest ładowany podczas testów, oznacza to, że możesz przedefiniować jego funkcje, np . add_actionLub register_post_type.

Załóżmy, że masz plik załadowany z pliku bootstrap, w którym masz:

function add_action() {
  global $counter;
  if ( ! isset($counter['add_action']) ) {
    $counter['add_action'] = array();
  }
  $counter['add_action'][] = func_get_args();
}

function register_post_type() {
  global $counter;
  if ( ! isset($counter['register_post_type']) ) {
    $counter['register_post_type'] = array();
  }
  $counter['register_post_type'][] = func_get_args();
}

Ponownie napisałem funkcje, aby po prostu dodać element do globalnej tablicy za każdym razem, gdy są wywoływane.

Teraz powinieneś utworzyć (jeśli jeszcze go nie masz) rozszerzenie swojej podstawowej klasy przypadków testowych PHPUnit_Framework_TestCase: pozwala to na łatwą konfigurację testów.

Może to być coś takiego:

class Custom_TestCase extends \PHPUnit_Framework_TestCase {

    public function setUp() {
        $GLOBALS['counter'] = array();
    }

}

W ten sposób przed każdym testem licznik globalny jest resetowany.

A teraz twój kod testowy (odnoszę się do przepisanej klasy, którą zamieściłem powyżej):

class CustomPostTypes extends Custom_TestCase {

  function test_init() {
     global $counter;
     $r = new RegisterCustomPostType;
     $r->init();
     $this->assertSame(
       $counter['add_action'][0],
       array( 'init', array( $r, 'register_post_type' ) )
     );
  }

  function test_register_post_type() {
     global $counter;
     $r = new RegisterCustomPostType;
     $r->register_post_type();
     $this->assertSame( $counter['register_post_type'][0], array( 'foo' ) );
  }

}

Należy pamiętać:

  • Mogłem wywołać te dwie metody osobno, a WordPress w ogóle nie został załadowany. W ten sposób, jeśli jeden test się nie powiedzie, wiem dokładnie, kto jest winowajcą.
  • Jak powiedziałem, tutaj testuję, że klasy wywołują funkcje WP z oczekiwanymi argumentami. Nie ma potrzeby sprawdzania, czy CPT naprawdę istnieje. Jeśli testujesz istnienie CPT, to testujesz zachowanie WordPress, a nie zachowanie wtyczki ...

Fajnie .. ale to PITA!

Tak, jeśli musisz ręcznie wyśmiewać wszystkie funkcje WordPressa, to naprawdę ból. Kilka ogólnych rad, których mogę udzielić, to korzystania z jak najmniejszej liczby funkcji WP: nie musisz przepisywać WordPress, ale abstrakcyjne funkcje WP, których używasz w niestandardowych klasach, aby można je było wyśmiewać i łatwo testować.

Na przykład w odniesieniu do powyższego przykładu możesz napisać klasę, która rejestruje typy postów, dzwoniąc register_post_type „init” z podanymi argumentami. Dzięki tej abstrakcji nadal musisz przetestować tę klasę, ale w innych miejscach kodu rejestrujących typy postów możesz skorzystać z tej klasy, kpiąc z niej w testach (zakładając, że działa).

Niesamowite jest to, że jeśli napiszesz klasę, która streszcza rejestrację CPT, możesz stworzyć dla niej osobne repozytorium, a dzięki nowoczesnym narzędziom, takim jak Composer, osadzić ją we wszystkich projektach, w których jest to potrzebne: przetestuj raz, używaj wszędzie . A jeśli kiedykolwiek znajdziesz w nim błąd, możesz go naprawić w jednym miejscu i za pomocą prostego rozwiązania composer updatewszystkie projekty, w których jest on używany.

Po raz drugi: napisanie kodu, który można przetestować w izolacji oznacza napisanie lepszego kodu.

Ale wcześniej czy później muszę gdzieś użyć funkcji WP ...

Oczywiście. Nigdy nie powinieneś działać równolegle do rdzenia, to nie ma sensu. Możesz pisać klasy, które otaczają funkcje WP, ale klasy te również muszą zostać przetestowane. Metodę „ręczną” opisaną powyżej można stosować do bardzo prostych zadań, ale gdy klasa zawiera wiele funkcji WP, może to być uciążliwe.

Na szczęście są tam dobrzy ludzie, którzy piszą dobre rzeczy. 10up , jedna z największych agencji WP, utrzymuje bardzo dobrą bibliotekę dla osób, które chcą przetestować wtyczki we właściwy sposób. Jest WP_Mock.

Pozwala wyśmiewać funkcje WP i haki . Zakładając, że załadowałeś w swoich testach (zobacz plik repo) ten sam test, który napisałem powyżej, wygląda następująco:

class CustomPostTypes extends Custom_TestCase {

  function test_init() {
     $r = new RegisterCustomPostType;
     // tests that the action was added with given arguments
     \WP_Mock::expectActionAdded( 'init', array( $r, 'register_post_type' ) );
     $r->init();
  }

  function test_register_post_type() {
     // tests that the function was called with given arguments and run once
     \WP_Mock::wpFunction( 'register_post_type', array(
        'times' => 1,
        'args' => array( 'foo' ),
     ) );
     $r = new RegisterCustomPostType;
     $r->register_post_type();
  }

}

Proste, prawda? Ta odpowiedź nie jest tutorialem WP_Mock, więc przeczytaj plik repo, aby uzyskać więcej informacji, ale myślę, że powyższy przykład powinien być dość jasny.

Co więcej, nie musisz pisać żadnych szydzonych add_actionlub register_post_typesamodzielnie, ani utrzymywać żadnych zmiennych globalnych.

A zajęcia WP?

WP ma też pewne klasy, a jeśli WordPress nie jest ładowany podczas uruchamiania testów, musisz je wyśmiewać.

To o wiele łatwiejsze niż kpiące funkcje, PHPUnit ma wbudowany system do kpienia z obiektów, ale tutaj chcę zasugerować kpinę . Jest to bardzo potężna biblioteka i bardzo łatwa w użyciu. Co więcej, jest to zależność odWP_Mock , więc jeśli ją masz, masz także Kpinę.

Ale co z WP_UnitTestCase?

Pakiet testowy WordPress został stworzony do testowania rdzenia WordPress , a jeśli chcesz przyczynić się do rdzenia, jest on kluczowy, ale użycie go do wtyczek sprawia, że ​​nie testujesz go osobno.

Spójrz na świat WP: istnieje wiele nowoczesnych frameworków PHP i CMS, a żaden z nich nie sugeruje testowania wtyczek / modułów / rozszerzeń (lub jakkolwiek są one nazywane) przy użyciu kodu frameworka.

Jeśli tęsknisz za fabrykami, przydatną funkcją pakietu, musisz wiedzieć, że są niesamowite rzeczy .

Gotcha i wady

Zdarzało się, że brakuje przepływu pracy, który zasugerowałem tutaj: niestandardowe testowanie bazy danych .

W rzeczywistości, jeśli użyć standardowych tabel WordPress i funkcje, aby tam pisać (na najniższym poziomie $wpdbmetod) nie trzeba faktycznie zapisu danych lub testu, jeśli dane są rzeczywiście w bazie danych, po prostu mieć pewność, że odpowiednie metody są wywoływane z odpowiednimi argumentami.

Możesz jednak pisać wtyczki z niestandardowymi tabelami i funkcjami, które budują zapytania, aby tam pisać i sprawdzać, czy te zapytania działają, to na twoją odpowiedzialność.

W takich przypadkach pakiet testowy WordPress może ci bardzo pomóc, a ładowanie WordPress może być w niektórych przypadkach konieczne do uruchomienia takich funkcji dbDelta.

(Nie trzeba mówić, że do testów używa się innej bazy danych, prawda?)

Na szczęście PHPUnit umożliwia organizowanie testów w „pakietach”, które można uruchamiać osobno, dzięki czemu możesz napisać zestaw niestandardowych testów bazy danych, w których ładujesz środowisko WordPress (lub jego część), pozostawiając wszystkie pozostałe testy bez WordPressa .

Pamiętaj tylko, aby pisać klasy, które wyodrębniają jak najwięcej operacji bazy danych, w taki sposób, aby wszystkie inne klasy wtyczek korzystały z nich, aby za pomocą prób można poprawnie przetestować większość klas bez zajmowania się bazą danych.

Po raz trzeci pisanie kodu łatwo testowalnego w izolacji oznacza pisanie lepszego kodu.

gmazzap
źródło
5
Cholera jasna, wiele przydatnych informacji! Dziękuję Ci! Jakoś udało mi się przeoczyć cały punkt testowania jednostkowego (do tej pory ćwiczyłem testy PHP tylko w Code Dojo). Dowiedziałem się też dzisiaj o wp_mock, ale z jakiegoś powodu udaje mi się to zignorować. Wkurzyło mnie to, że jakikolwiek test, bez względu na to, jak mały, zajmował co najmniej dwie sekundy, aby uruchomić (najpierw załaduj WP env, a następnie uruchom test). Jeszcze raz dziękuję za otwarcie oczu!
Ionut Staicu
4
Dzięki @IonutStaicu Zapomniałem wspomnieć, że brak ładowania WordPressa znacznie przyspiesza testy
gmazzap
6
Warto również zauważyć, że platforma testów jednostkowych WP Core jest niesamowitym narzędziem do przeprowadzania testów INTEGRACYJNYCH, które byłyby testami automatycznymi, zapewniającymi dobrą integrację z samą WP (np. Brak przypadkowych kolizji nazw funkcji itp.).
John P Bloch,
1
@JohnPBloch +1 za dobry punkt. Nawet jeśli użycie przestrzeni nazw wystarcza, aby uniknąć kolizji nazw funkcji w WordPress, gdzie wszystko jest globalne :) Ale, na pewno, integracja / testy funkcjonalne to coś. W tej chwili gram z Behat + Mink, ale nadal to ćwiczę.
gmazzap
1
Dzięki za „jazdę helikopterem” po lesie WordTress w UnitTest - wciąż śmieję się z tego epickiego zdjęcia ;-)
pan