Testy z Codeception w CodeIgniter

Kiedyś wspominaliśmy już o tym czemu warto pisać testy dla swoich aplikacji. Dzisiaj chciałbym pokazać, jak można zintegrować CodeIgniter z Codeception. Czym jest Codeception? W uproszczeniu można powiedzieć, że to taki „kombajn” do testów – dzięki czemu możemy przeprowadzać różne rodzaje testów (jednostkowe, funkcjonalne i akceptacyjne). Mam nadzieję, że brzmi to zachęcająco.

Za co odpowiadają konkretne testy

Zanim zaczniemy, w kilku słowach wspomnę o rodzajach testów. Można oczywiście sięgnąć po definicje z wiki lub jakiejś książki, ale przedstawię tutaj w miarę proste przykłady, które moim zdaniem dosyć dobrze oddają przeznaczenie każdego z rodzaju testów.

Testy jednostkowe.
Testujemy konkretną metodę naszego kodu źródłowego. Dostarczamy potrzebne parametry wejściowe i sprawdzamy, czy wynik zgadza się z tym czego oczekujemy.

Testy funkcjonalne.
Testujemy funkcję w naszej aplikacji. Np. rejestrację. Dostarczamy dane wejściowe, tutaj będzie to email i hasło, i nie interesuje nas już co się dzieje „pod spodem” (nie ważne z ilu różnych metod po drodze korzystamy), chcemy ustalić, czy funkcja rejestracji działa. Możemy więc sprawdzić jaki jest wynik wysłania formularza i czy np. został wysłany email z potwierdzeniem rejestracji albo czy zostaliśmy automatycznie zalogowani (sprawdzamy stan sesji).

Testy akceptacyjne.
Testujemy naszą aplikację od strony użytkownika. Innymi słowy sprawdzamy, czy użytkownicy są w stanie poprawnie korzystać z naszej strony. Podczas testów możemy więc polegać tylko na tym co widzi użytkownik. Możemy więc np. sprawdzić, czy wypełnienie opowiednich pól w formularzu i kliknięcie w konkretny przycisk pozwoli nam się zalogować.

 Po co mi Codeception?

PHPUnit jest najpopularniejszą metodą testowania aplikacji w PHP. Nic dziwnego, to świetne narzedzie. Jednak testy jednostkowe sprawdzają się najlepiej, kiedy chcemy przetestować konkretne zachowanie. Np. metodę modelu, metodę biblioteki, metodę API lub funkcję w helperze. Sprawa wygląda gorzej, kiedy chcemy testować „zwykłe” kontrolery w naszej aplikacji – czyli sprawdzić, czy tak naprawdę nasi użytkownicy są w stanie używać naszej aplikacji i nie napotkają na błędy. W takim przypadku PHPUnit nie jest najporęczniejszym narzędziem.

Przykładowo, chcemy przetestować taki scenariusz:
Wchodząc na stronę „login”, podaję nazwę użytkownika i hasło. Po kliknięciu w przycisk „Zaloguj” zostaję zalogowany na stronę.

Ciężko byłoby zrealizować taki scenariusz w PHPUnit. Na szczęście Codeception pozwala nam na tworzenie testów akceptacyjnych, które świetnie nadają się do sprawdzania tego rodzaju rzeczy. Co więcej, w Codeception możemy korzystać również z testów jednostkowych. Tym sposobem dzięki jednemu narzędziu, możemy przeprowadzać różne rodzaje testow.

Do dzieła

No dobrze, skoro już wiemy czemu warto się zainteresować tym narzędziem, możemy przystąpić do instalacji.

  • Na początek tworzymy nowy, czysty projekt z CI 3.0.4. Nie zapomnijmy o podstawowej konfiguracj, tak żebyśmy mogli używać strony.
  • Teraz pora pobrać Codeception. Możemy to zrobić na tej stronie. Zwróćmy uwagę na to, że zależnie od wersji PHP, której używamy, musimy ściągnąć różne wersje pliku. Ja użyję polecenia:
    wget http://codeception.com/codecept.phar. Pamiętajcie, że plik codeception.phar powinien się znaleźć w głównym katalogu naszego projektu CI.
  • Następny krok to inicjalizacja testów. Z poziomu konsoli wpisujemy: php codecept.phar bootstrap. Utworzony zostnie katalog „tests” oraz plik konfiguracyjny „codeception.yml”.
  • Pora przygotować specjalny plik, który pozwoli na uruchamianie testów dla CI. Znajdzie się tam klasa „CodeIgniterTestCase”, z której będziemy korzystać przy pisaniu testów jednostkowych.
    <?php
    // Upewniamy się, że mamy ustawioną strefę czasową
    if( ! ini_get('date.timezone') )
    {
    	date_default_timezone_set('UTC');
    }
    
    // Nadpsujemy zachowanie niektórych funkcji, które znajdują się w pliku code/Common.php
    function show_error($message, $status_code = 500, $heading = 'An Error Was Encountered')
    {
    	throw new PHPUnit_Framework_Exception($message, $status_code);
    }
    
    function show_404($page = '', $log_error = TRUE)
    {
    //	throw new PHPUnit_Framework_Exception($page, 404);
    }
    
    // Ładujemy CodeIgniter
    ob_start();
    include(getcwd() . '/' . 'index.php');
    ob_end_clean();
    
    /**
     * Klasa CodeIgniterTestCase
     *
     * Używamy tego wszędzie, gdzie potrzebujemy dostępu do super obiektu $ci.
     *
     */
    class CodeIgniterTestCase extends \Codeception\TestCase\Test {
    
    	protected $ci;
    
    	public function __construct()
    	{
    		parent::__construct();
    
    		$this->ci =& get_instance();
    	}
    
    	public function __get($var)
    	{
    		return $this->ci->$var;
    	}
    
    }
  • Teraz musimy edytować plik „tests/_bootstrap.php” i dopisać dwie linijki:
    // Ustawiamy zmienną środowiskową
    $_SERVER['CI_ENV'] = 'testing';
    // Ładujemy nasz specjalny plik
    require_once dirname(__FILE__) .'/CodeIgniterTestCase.php';
  • Pozostało jeszcze zmodyfikować lekko jedną metodę klasy Router, abyśmy mogli uruchomić CodeIgniter w Codeception. Musimy więc tę klasę rozszerzyć:
    <?php
    class MY_Router extends CI_Router {
    
    	/**
    	 * Validate request
    	 *
    	 * Attempts validate the URI request and determine the controller path.
    	 *
    	 * @used-by CI_Router::_set_request()
    	 * @param   array   $segments   URI segments
    	 * @return  mixed   URI segments
    	 */
    	protected function _validate_request($segments)
    	{
    		$c = count($segments);
    		$directory_override = isset($this->directory);
    
    		// Loop through our segments and return as soon as a controller
    		// is found or when such a directory doesn't exist
    		while ($c-- > 0)
    		{
    			$test = $this->directory
    				.ucfirst($this->translate_uri_dashes === TRUE ? str_replace('-', '_', $segments[0]) : $segments[0]);
    
    			if ( ! file_exists(APPPATH.'controllers/'.$test.'.php')
    				&& $directory_override === FALSE
    				&& is_dir(APPPATH.'controllers/'.$this->directory.$segments[0])
    			)
    			{
    				$this->set_directory(array_shift($segments), TRUE);
    				continue;
    			}
    			elseif (file_exists(APPPATH.'controllers/'.$test.'.php'))
    			{
    				return $segments;
    			}
    		}
    
    		show_404(implode('/', $segments));
    	}
    }

Testy jednostkowe

  • Możemy teraz sprawdzić, czy jesteśmy w stanie uruchomić testy PHPUnit. Najpierw stwórzmy jakąś przykładową metodę w modelu (bardzo prostą).
    <?php
    class Sample_model extends CI_Model {
    	
    	/**
    	 * Sprawdzamy, czy liczba jest parzysta
    	 *
    	 * @param int $number Liczba
    	 * @return bool
    	 */
    	public function is_even_number($number)
    	{
    		return $number % 2 === 0 ? TRUE : FALSE;
    	}
    }
  • Teraz pora na testy:
    <?php
    class SampleTest extends \CodeIgniterTestCase {
    
        protected $ci;
    
        //--------------------------------------------------------------------
    
        public function __construct()
        {
            parent::__construct();
    
            $this->ci = get_instance();
    
            $this->ci->load->model('sample_model');
        }
    
        //--------------------------------------------------------------------
    
        public function _before()
        {
    
        }
    
        //--------------------------------------------------------------------
    
        public function _after()
        {
    
        }
    
        //--------------------------------------------------------------------
    
        public function testIsEvenNumber()
        {
            $result = $this->ci->sample_model->is_even_number(10);
            $this->assertTrue($result);
        }
    
        //--------------------------------------------------------------------
    
        public function testIsOddNumber()
        {
            $result = $this->ci->sample_model->is_even_number(11);
            $this->assertFalse($result);
        }
    
    }

Skoro wszystko jest już gotowe, spróbujmy uruchomić testy jednostkowe. W tym celu należy wykonac komendę: php codecept.phar run unit. Jeśli wszystko zrobiliśmy dobrze, to testy powinny „przejść”. Tym sposobem uruchomiliśmy testy jednostkowe w Codeception.

Testy akceptacyjne

  • Teraz pora na testy akceptacyjne. Najpierw edytujemy plik konfiguracyjny „tests/acceptance.suite.yml”. Musimy podać aktualny adres URL, pod którym znajduje się nasz lokalny projekt CI.
    class_name: AcceptanceTester
    modules:
        enabled:
            - PhpBrowser:
                url: http://nasz_lokalny_adres_projektu.dev
            - \Helper\Acceptance
  • Czas stworzyć jakiś przykładowy kontroler i widok, dla których napiszemy testy
    <?php
    class Login extends CI_Controller {
    	
    	/**
    	 * Logowanie do aplikacji
    	 */
    	public function index()
    	{
    		$this->load->helper('form');
    		
    		if ($this->input->method() !== 'post')
    		{
    			$this->load->view('login');
    		}
    		else
    		{
    			$username = $this->input->post('username');
    			$password = $this->input->post('password');
    
    			if ($username === 'ren' && $password === 'secret')
    			{
    				echo '<h1>Witaj ' . $username . '!</h1>';
    			}
    		}
    	}
    }
    <!DOCTYPE html>
    <html lang="en">
    <head>
    	<meta charset="utf-8">
    	<title>Logowanie</title>
    </head>
    <body>
    
    <?= form_open(); ?>
    
    	<?= form_label('Login'); ?>
    	<?= form_input('username'); ?>
    
    	<?= form_label('Hasło'); ?>
    	<?= form_password('password'); ?>
    
    	<?= form_submit('submit', 'Zaloguj'); ?>
    
    <?= form_close(); ?>
    
    </body>
    </html>
  • Możemy teraz stworzyć nasze testy akceptacyjne.
    <?php
    $I = new AcceptanceTester($scenario);
    $I->wantTo('log in'); // określamy co chcemy zrobić
    $I->amOnPage('/login'); // określamy na jakiej stronie chcemy się znaleźć
    $I->seeInTitle('Logowanie'); // sprawdzamy jaki jest tytuł strony
    $I->fillField('username', 'ren'); // wypełniamy pole username
    $I->fillField('password', 'secret'); // wypełniamy pole password
    $I->click('Zaloguj'); // klikamy Zaloguj
    $I->see('Witaj ren!'); // sprawdzamy, czy widzimy tekst powitalny

Przykład jest bardzo prosty, ale pozwoli nam sprawdzić, czy możemy uruchomić testy. Wykonajmy więc komendę z konsoli: php codecept.phar run acceptance.
Kolejny raz – jeśli wszystko zrobiliśmy dobrze, to testy powinny wykonać się poprawnie. W ten sposób możemy uruchamiać zarówno testy jednostkowe, jak i akceptacyjne – świetnie.

Jeśli chodzi o testy akceptacyjne, to jest jeszcze kilka kwestii, o których chciałbym wspomnieć. Pierwszą ważną sprawą jest używanie modułu bazy danych przy tworzeniu testów. Codeception posiada specjalny moduł „Db”, który to umożliwia. Więcej szczegółów oraz informacji jak z niego korzystać znajdziemy pod tym linkiem.

Drugą sprawą jest sposób przeprowadzania testów akceptacyjnych. W tej chwili w pliku konfiguracyjnym (tests/acceptance.suite.yml), do uruchamiania testów używamy crawlera, który komunikuje się z naszą strona za pomocą cURL (ustawienie: PhpBrowser). Ma to niestety swoje minusy. Jednym z nich jest, to że nie możemy testować tych rzeczy, które są napisane w JavaScript. Innymi słowy, uruchamiając testy w ten sposob nie mamy takich samych doświadczeń jak użytkownik posługujący się zwykłą przeglądarką internetową. Na szczęście jest sposób na to, żeby to ominąć – z pomocą przyjdzie nam server Selenium.

Serwer Selenium

Aby uruchamiać testy akceptacyjne przy pomocy serwera Selenium wystarczą w zasadzie dwie rzeczy:

  1. Zmina ustawień w pliku konfiguracyjnym – zmiany są kosmetyczne, ale jednak kluczowe:
    class_name: AcceptanceTester
    modules:
        enabled:
        - WebDriver:
            url: http://nasz_lokalny_adres_projektu.dev
            browser: firefox
        - \Helper\Acceptance

    Czemu ustawiamy przeglądarke na Firefox? Dla wygody. Serwer Selenium ma wbudowaną obsługe dla tej przeglądarki, jeśli chcemy użyć np. Chrome, to musimy pobrać dodatkowy moduł. Tak więc dla uproszczenia zostaniemy przy przeglądarce Firefox – będzie nam poprostu łatwiej.

  2. Instalacja serwera Selenium
    Pobieramy plik ze strony projektu (Selenium Standalone Server) i umieszczamy go w głównym katalogu naszego projektu CI. W naszym wypadku jest to wersja 2.52.0 serwera Selenium. Teraz musimy uruchomoć serwer – posłużymy się komendą: java -jar selenium-server-standalone-2.52.0.jar

Serwer wystartował. Jeśli odpalimy teraz testy php codecept.phar run acceptance, to powinna uruchomic się przeglądarka Firefox i przeprowadzić czynności, które mamy w testach, czyli: wejść na stronę „login”, wypełnić formularz i go wysłać. W konsoli zobaczymy końcowy wynik dla testów, tak jak poprzednio. Jeśli chcemy wyłączyć serwer Selenium, to wystarczy użyć skrótu Ctrl + Z.

Podsumowanie

Na koniec mała uwaga co do zmiennej środowiskowej ENVIRONMENT, którą posługujemy się w CodeIgniter.
Testy akceptacyjne są uruchamiane w środowisku, które mamy ustawione lokalnie, czyli w takim, w którym normalnie wchodzimy na stronę. Zazwyczaj będzie to środowisko „development”. Testy powinniśmy uruchamiać tylko w środowisku testowym, więc mamy dwa wyjścia. Powinniśmy zmieniać środowisko przed każdym uruchomieniem testow (bardzo niewygodne), lub zdefiniować sobie specjalną domenę do testów, której użycie będzie od razu przełączało naszą aplikację w tryb środowiska testowego. Potrzebna będzie wtedy mała zmiana w naszym głównym pliku index.php oraz zmiennej „url” w pliku „tests/acceptance.suite.yml”, tak żeby domena była ustawiona na tą, która automatycznie ustawia środowisko na testowe.

Powyższe zmiany dotyczą tylko testów akceptacyjnych. Testy jednostkowe są automatycznie uruchamiane w środowisku testowym.

To wszystko. Mamy teraz działające środowisko testowe Codeception, zintegrowane z frameworkiem CodeIgniter. Trzeba pamiętać, że zaprezentowane tutaj sposoby testowania i możliwości jakie oferuje Codeception, są bardzo podstawowe. Mamy bardzo wiele różnych opcji do dyspozycji i jeśli chcemy je bliżej poznać, musimy zagłębić się w dokumentację, która całkiem dobrze tłumaczy jak możemy testować naszą aplikację.

Kod źródłowy tego artykułu jest dostępny na GitHub (za wyjątkiem serwera Selenium, który możecie ściągnąć sobie samodzielnie).

4 komentarze do wpisu „Testy z Codeception w CodeIgniter”

  1. Dzięki za wpis, pożyteczne i ciekawe rozwinięcie poprzedniego postu! Jeszcze tylko temat testów funkcjonalnych poproszę i trylogia gotowa :) Warto wytłuścić różnicę między testami – jednostkowe, funkcjonalne, akceptacyjne. Poczytałem ze strony codeception, ale to wiem tylko,że jednostkowe różnią się od funkcjonalnych tym, że te ostatnie nie wykorzystują serwera i są szybsze. No i testy z bazami danych, czy za każdym razem, żeby zrobić test jednostkowy, opróżniam bazę danych?

    Odpowiedz
    • Uzupełniłem wpis o krótki opis rodzajów testów.

      Testy funkcjonalne zostały przeze mnie pominięte, ponieważ Codeception nie posiada oficjalnego modułu dla CI. Bez tego takie testy trochę mijają się z celem, bo przy testach funkcjonalnych powinniśmy mieć możliwość sprawdzenia stanu niektorych elemetów frameworka. Co prawda znalazłem nieoficjalny moduł, ale wpis był już na tyle długi, że nie chciałem już tego tutaj przedstawiać (zresztą sam moduł jest dosyć ubogi). Dla zainteresowanych link: https://github.com/luka-zitnik/CodeIgniterModule
      Swoją drogą, testy funkcjonalne w CI są dosyć problematyczne, ponieważ w niektórych sytuacjach używamy funkcji „exit”…

      Przy testach jednostkowych zazwyczaj tylko raz ładuję testową bazę, albo wcale (Mockery).

      Odpowiedz
  2. Pytanie: Jak powinien wyglądać MY_Router jeżeli mamy codeigniter zintegrowamy z modułem HMVC ? Czy możemy bez problemu testować wszystkie pliki z modułów ??

    Odpowiedz
    • Z tego co widzę, w takim wypadku żadne zmiany nie są potrzebne – można pominąć całkowicie krok z klasą MY_Router.

      Odpowiedz

Dodaj komentarz

Witryna wykorzystuje Akismet, aby ograniczyć spam. Dowiedz się więcej jak przetwarzane są dane komentarzy.