wprowadzenie do phpunit

39
Wprowadzenie do PHPUnit By Michał Kowalik

Upload: michal-kowalik

Post on 26-Jun-2015

486 views

Category:

Software


0 download

DESCRIPTION

Wprowadzenie do tworzenia testów jednostkowych w PHP (PHPUnit, testy jednostkowe, testy funkcjonalne, testy integracyjne). Wersja: alpha

TRANSCRIPT

Page 1: Wprowadzenie do PHPUnit

Wprowadzenie do PHPUnitBy Michał Kowalik

Page 2: Wprowadzenie do PHPUnit

Czym są testy jednostkowe?

● W programowaniu metoda testowania tworzonego oprogramowania poprzez wykonywanie testów weryfikujących poprawność działania pojedynczych elementów (jednostek) programu.

● Wyróżniamy następujące rodzaje:

- testy jednostkowe weryfikacja kodu

- testy integracyjne weryfikacja komunikacji z zasobami np. bazą danych

- testy funkcjonalne /end-to-end/weryfikacja wymagań użytkownika

Page 3: Wprowadzenie do PHPUnit

Korzyści

● Są automatyczneOdbywają się za nas, nie musimy pamiętać by ręcznie sprawdzić jakiś tam jeszcze edge-case.

● Bardzo dobrze wpływają na jakość koduDzielimy program na mniejsze klocki.

Zaczynamy korzystać z wzorców projektowych.

Za każdym razem musimy odpowiedzieć sobie na pytanie Jak ja to potem przetestuje?

Page 4: Wprowadzenie do PHPUnit

Korzyści

● Pozwalają wykrywać problemy na etapie tworzenia aplikacji.

● Skracają czas programowania.

Nie musimy przeskakiwać do przeglądarki i tracić czasu na ręczne testy. Możemy pracować bez odrywania się od IDE.

Page 5: Wprowadzenie do PHPUnit

Korzyści

● PHP jest dynamicznym językiemWykrywanie błędów składni, nieistniejących metod, niewłaściwego wykorzystania typów, błędnego wykorzystania funkcji wbudowanych

● Weryfikacja działania na różnych platformach

Programiści mogą pracować na Windowsach, ale serwery są zazwyczaj na Linuxach

Co pewien czas wychodzi nowa wersja PHP, testy pozwalają nam sprawdzić czy program zadziała w nowym środowisku (np. w HHVM)

Obecność testów jest konieczna do wykorzystania narzędzi typu Continous Integration (Github / Travis).

Page 6: Wprowadzenie do PHPUnit

Korzyści

● Testy nabierają znaczenia gdy nasz projekt rośnie

Początkowo testy mogą wydawać się zbędę i niepotrzebnie nas obciążać

W marę jak rozbudowujemy projekt testy pozwalają zweryfikować czy zmiany nie uszkodziły przedniej funkcjonaliści

Pozwalają nowemu programiści w zespole sprawdzić – samodzielnie – czy czegoś nie zepsuł.

Page 7: Wprowadzenie do PHPUnit

Korzyści tylko wtedy gdy:

● Testy działają szybko● Nie generują dodatkowych błędów

Są napisane w możliwie prosty sposób

● Są w stanie przetrwać ew. modyfikacje kodu

Testy nie mogą być zbyt szczegółowe

● Generalnie pisanie testów jest sztuką samą w sobie

Oddzielna specjalizacja

Page 8: Wprowadzenie do PHPUnit

Instalacja (phar) - zalecana

#unix

wget https://phar.phpunit.de/phpunit.phar

chmod +x phpunit.phar

mv phpunit.phar phpunit

#phpunit.bat

c:\php\php.exe c:\php\phpunit.phar %*

#sprawdzenie poprawności

> phpunit –-versionPHPunit 4.1.0 by Sebastian Bergmann.

Page 9: Wprowadzenie do PHPUnit

Instalacja 3.7 (pear) - przestarzała

pear update-channels

pear config-set auto_discover 1

pear channel-discover pear.phpunit.de

pear install --alldeps --force phpunit/PHPUnit

pear install --alldeps --force phpunit/DbUnit

pear install --alldeps --force phpunit/PHPUnit_Selenium

pear install --alldeps --force phpunit/PHPUnit_SkeletonGenerator

pear install --alldeps --force phpunit/PHPUnit_Story

pear install --alldeps --force phpunit/PHP_CodeCoverage

pear install --alldeps --force phpunit/PHP_Invoker

Page 10: Wprowadzenie do PHPUnit

Konfiguracja

● phpunit.xml

Dzięki niemu możemy skonfigurować środowisko w jednym miejscu i uruchomić testy poprzez ./phpunit

● bootsrap.php

W tym piku inicjujemy projekt (ustawiamy globalne zmienne, class loadery, startujemy framework).

Bardzo często zawiera prawie to samo co public/index.php

Położenie można ustalić w phpunit.xml

Page 11: Wprowadzenie do PHPUnit

phpunit.xml<?xml version="1.0" encoding="UTF-8"?><phpunit backupGlobals="true" backupStaticAttributes="true" bootstrap="bootstrap.php" cacheTokens="false" colors="false" convertErrorsToExceptions="true" convertNoticesToExceptions="true" convertWarningsToExceptions="true" processIsolation="false" stopOnError="false" stopOnFailure="false" stopOnIncomplete="false" stopOnSkipped="false" timeoutForSmallTests="1" timeoutForMediumTests="10" timeoutForLargeTests="60" strict="false" verbose="false"><testsuites> <testsuite name="Kohana Tests"> <directory>./</directory> </testsuite></testsuites>

<!-- Selenium browser set --><selenium> <browser name="Internet Explorer" browser="*iexplore" /> <browser name="Firefox" browser="*firefox" /></selenium><!-- Code coverage filter --><filter> <whitelist processUncoveredFiles... ...FromWhitelist="false"> <directory suffix=".php"> ../../../hako/classes </directory> </whitelist></filter><!-- Code Coverage report --><logging> <log type="coverage-html" target="./report" charset="UTF-8" highlight="false" lowUpperBound="35" highLowerBound="70"/></logging></phpunit>

Page 12: Wprowadzenie do PHPUnit

phpunit.xml

● backupStaticAttributes="true"

Przełącznik te pozwala za zachowanie zmiennych statycznych pomiędzy testami. Zmienne globalne są zachowywane domyślnie.

● bootstrap="bootstrap.php"

Wskazuje położenie pliku inicjującego testy

● <filter>Pozwala na odfiltrowanie plików źródłowych dla generowanych raportów pokrycia kodu.

Page 13: Wprowadzenie do PHPUnit

Jak działa PHPUnit?

● bootstrap.php → ładuje klase TestTest → setUpBeforeClass() → setUp() → testTest1() → tearDown() → setUp() → testTest2() → tearDown() → … → tearDownAfterClass()

● Automatycznie konwertuje błędy do wyjątków które są potem wyświetlane w opisie pod testem.

● Automatycznie traktuje klasy z suffiksem Test jak test jednostkowy

● Wykonuje metody z prefiksem test lub oznaczone komentarzem @test.

● Metody setUp() oraz tearDown() służa do „sprzątania” przed i po testach.

Page 14: Wprowadzenie do PHPUnit

Testy weryfikujemy porównaniami

self::assertEquals() // ==

self::assertSame() // === lepsze przy testowaniu tablic

self::assertEmpty()

self::assertContains()

self::assertCount()

self::assertTrue()

self::assertRegExp()

self::assertFileExists()

self::assertJsonStringEqualsJsonFile()

self::fail($message)

● W wersji 3.7 self:: → $this->

● I wiele innych...

Page 15: Wprowadzenie do PHPUnit

Prosty przykład

<?php

namespace tests;

class ValidTest extends \Kohana_UnitTest_TestCase{ /** * @covers \Valid::min_value */ public function test_min_value() { $actual = \Valid::min_value(33, 40); $this->assertEquals(40, $actual); }}

Page 16: Wprowadzenie do PHPUnit

@dataProvider

public function dataTestMinValue(){ return array( array(0, 0, true), array(0, 1, false), array('23', 0, true), array('sdfdsf', 0, false), );}

/** * @dataProvider dataTestMinValue * @covers \Valid::min_value */public function test_min_value ($value, $limit, $excepted){ $actual = \Valid::min_value($value, $limit); $this->assertEquals($excepted $actual);}

● Testują funkcję zdarza się ze powielamy testy różnice się tylko o parametry, definiujące provider danych możemy temu zapobiec.

Page 17: Wprowadzenie do PHPUnit

Przechwytywanie wyjątków

/** * @covers \Valid::min_value * @expectedException ExceptionClass * @expectedExceptionCode 123 * @expectedExceptionMessage Tekst wyj tkuą */public function test_min_value ($value, $limit, $excepted){ functionUnderTest('should throw exception');}

● @expectedExceptionCode oraz @expectedExceptionMessage są opcjonalne.

● Dla @expectedException domyślna klasa to Exception.

Page 18: Wprowadzenie do PHPUnit

Testy jednostkowe

● Powinny działać ultra szybko.● Weryfikują działanie cząstki testowanego kodu● Skupiamy się jedynie na testowanej metodzie /

funkcji. Testowany kod powinien działać nawet gdy zależny on od komponentu którego jeszcze nie ma → wszelkie powiązania powinniśmy zastępować mockami.

● Traktujemy testy jak użytkownika pisanego przez nas api. Powinniśmy formować kod tak by był łatwy do przetestowania.

Page 19: Wprowadzenie do PHPUnit

Testy jednostkowefunction abc($a, $b){ $c = $a; if ($a > 0) { $c += $b; if ($b < 0) { $c *= $b; } } return $c;}

public function testAbc1(){ $this->assertEquals(-1, -1);}public function testAbc2(){ $this->assertEquals(1, -1);}public function testAbc3(){ $this->assertEquals(1, 1);}

● Testujemy pojedynczą metodę / funkcję

Dokładniej mówią testujemy pojedyncza ścieżkę wykonywania się tak by pokrycie kody wyniosło 100%.

● Nie testujemy frameworka, nie powinniśmy testować tej samej funkcjonalności w kilku miejscach.

Page 20: Wprowadzenie do PHPUnit

Co oznacz że kod jest łatwy do przetestowania?

● Zależności można łatwo zastąpić makietami obiektów (Dependancy Injection)

● Złożoność cyklomatyczna jest niska. Tzn. jest stosunkowo niedużo ifów, forów itp., a ich zagnieżdżenia nie przekraczają głębokość ok. 5.

● Metoda nie powinna być nadmiernie długa (ok. 200 linijek).

● Cyklomatyczność oraz duża ilość metod prywatnych może sugerować utworzenie nowej klasy.

Page 21: Wprowadzenie do PHPUnit

Dependency Injection

● Największym wrogiem testów jest tzw. Hardcodeded dependency, czyli zależność której nie możemy w łatwy sposób zastąpić makietą.public function login(){ $login = $this->param('login'); $pass = $this->param('pass'); if ($this->authenticate($login, $pass)) { $this->redirect('/'); } else { $mail = new Mail(); $mail->subject = …; $mail->to = …; $mail->body = …; $mail->send(); }}

● Jak zastąpić $mail = new Mail();?

Page 22: Wprowadzenie do PHPUnit

Dependency Injection

● By kod przetestować należy go odpowiednio zmodyfikować, szczególnie w przypadku gdy kod został stworzony przed napisaniem testów.

● Popularnymi sposobami na wstrzykiwanie zależności są:

- Constructor injection (przez konstruktor)- Property injection (dodatkowa właściwość obiektu)- Factory method (metoda generująca obiekty)- Isolation of Control Container

Page 23: Wprowadzenie do PHPUnit

Makiety obiektów

● StubsSymulują / udają działanie obiektów.

$stub = $this->getMock('Mail');$stub->expects($this->any()) ->method('send') ->will( $this->returnValue(true) );

● Mocks - Weryfikują czy prawidłowo korzystamy z obiektów.

$mock = $this->getMock('Mail');$mock->expects($this->once()) ->method('addTo') ->with( $this->equalTo('[email protected]') );

Page 24: Wprowadzenie do PHPUnit

Makiety obiektów

● W większość frameworków stuby i mocki są w rzeczywistości tym samym obiektem.

● W praktyce częściej stosujemy stuby, ale zdarzają się mocki hybrydowe, czasami mockujemy obiekt które testujemy.

● Makiety obiektów pozwalają nam zasymulowac dowolną sytuację w badanym kodzie.

Page 25: Wprowadzenie do PHPUnit

Makiety obiektów

● Parametry dla expects()

self::any()

self::never()

self::atLeastOnce()

self::once()

self::exactly($count)

self::at($index)

● Jeżeli któryś z warunków nie zostanie spełniony test zostanie oznaczony jako nieudany.

Page 26: Wprowadzenie do PHPUnit

Makiety obiektów

● with() - Akceptuje dowolną listę argumentów:self::anything()self::contains($value)self::arrayHasKey($key)self::equalTo($value, $delta, $maxDepth)self::classHasAttribute($attribute)self::greaterThan($value)self::isInstanceOf($className)self::isType($type)self::matchesRegularExpression($regex)self::stringContains($string, $case)

● withAnyParameters() → cokolwiek

● Niespełnienie warunków zfailuje test.

Page 27: Wprowadzenie do PHPUnit

Makiety obiektów

● will() → wartości zwracana przez metodę

self::returnValue($value)

self::returnArgument($argumentIndex)

self::returnCallback($stub)

self::returnSelf()

self::returnValueMap($valueMap)

self::throwException($exception)

self::onConsecutiveCalls(...)

Page 28: Wprowadzenie do PHPUnit

Makiety obiektównamespace tests;

class ClassToMock{ public function method($arg) { throw new Exception('Original method invoked'); }}

class MockTest extends \PHPUnit_Framework_TestCase{ public function testMock() { $stubMock = $this->getMock('\tests\ClassToMock'); $stubMock->expects($this->at(0)) ->method('method') ->with(self::equalTo('getMe33')) ->will(self::returnValue(33)); $stubMock->expects($this->at(1)) ->method('method') ->with(self::equalTo('getMe44')) ->will(self::returnValue(44)); self::assertSame(33, $stubMock->send('getMe33')); self::assertSame(44, $stubMock->send('getMe44')); }}

Page 29: Wprowadzenie do PHPUnit

Makiety obiektów

● XpMock → warrper dla PHPUnit mocks

$this->mock('MyClass') ->getBool(true)

->getNumber(1)

->getString('string')

->new();

● Wymaga 5.4 (działa jako trait)● https://github.com/ptrofimov/xpmock

Page 30: Wprowadzenie do PHPUnit

Makiety globalnych obiektów i funkcji//applicationnamespace app{ class TestObject { public function method() { $model = ORM::factory('Test'); file_exists('some file'); } }}//testsnamespace app{ class ORM { static public function factory($className, $id=null) { echo "my ORM::factory\n"; return new \stdClass; } }}

//testsnamespace app{ function file_exists($filename) { echo "my file_exists\n"; return \file_exists($filename); }}

namespace test{ class TestTest extends \PHPUnit_Framework_TestCase { public function testTest() { $obj = new \app\TestObject; $obj->method(); } }}

Page 31: Wprowadzenie do PHPUnit

Reflection API

● Gdy piszemy dla testy dla cudzego kodu, po fakcie, bardzo przydatnym narzędziem jest Reflection API. Pozwala ono dostac się do niedostepnych zakamarków kodu.namespace tests;

class SomeClass{ private function prvMethod($arg1) { return $arg1; }}

class PrvTest extends \PHPUnit_Framework_TestCase{ public function testPrivateMethod() { $obj = new \tests\SomeClass(); $class = new \ReflectionClass($obj); $method = $class->getMethod('prvMethod'); $method->setAccessible(true); $method->invoke($obj, 'arg1'); }}

Page 32: Wprowadzenie do PHPUnit

TDD

● Test Driven Development● Polega na stworzeniu testów przed przystąpieniem

do kodowania (praca red to green)● Podejście to pozwala na tworzenie lepszych testów● Mitem jest przeświadczenie że należy posiadać

dokładne założenia projektowe by go zastosować● TDD można stosować do pojedynczych tasków.

Page 33: Wprowadzenie do PHPUnit

Code Coverage

● Gotowe narzędzie do analizowania pokrycia testów jednostkowych.

● Dobrze jest stosować tag @covers. Raport będzie brał pod uwagę tylko kod do którego stworzyliśmy testy intencjonalnie.

● Wymaga zainstalowanego Xdebug 2.1.3 (nie instaluj 2.2.4 bo nie działa, najlepiej 2.2.3).

● If ($a == 0) $c= 3 else $d = 5 traktowane jest jako jedno wyrażenie dlatego wymaganie jest stosowanie klamer.

Page 34: Wprowadzenie do PHPUnit

Code Coverage● Dodatkowo raport zawiera metrykę kodu

CRAP. Jeżeli osiągnęliśmy 100% a metryka >= 100 powinniśmy refaktoryzować program.

● phpunit --coverage-html ./report

● phpunit.xml

<filter> <whitelist processUncovered... ...FilesFromWhitelist="false"> <directory suffix=".php"> ../../../hako/classes </directory> </whitelist></filter><logging> <log type="coverage-html" target="./report" charset="UTF-8" highlight="false" lowUpperBound="35" highLowerBound="70"/></logging>

Page 35: Wprowadzenie do PHPUnit

Testy integracyjne

● Weryfikacja poprawności komunikacji między aplikacją a zewnętrznymi zasobami (gł. baza danych).

● Powinniśmy jedynie sprawdzać czy obiekty się poprawnie wstawiają / usuwają / pobierają. Nie powinniśmy mieszać z logiką biznesową (od tego są unit testy).

● Wymaga utworzenia zbioru testowego który należy odbudowywać przed wykonaniem każdego z testów.

Gdy framework korzysta z PDO można wykorzystać hack z zagnieżdżonymi transakcjamihttps://github.com/wakeless/transaction_pdo/blob/master/TransactionPDO.php

Page 36: Wprowadzenie do PHPUnit

Testy integracyjne

● Przykładem dobrego ORM (pod kątem testów jednostkowych) jest ten z Zend Framework.

● ORM oparte o ActiveRecord są trudne do testowania. Powodem jest powiązanie obiektu biznesowego z reprezentacją w bazie danych (Kohana, Yii).

● W aplikacji nie powinniśmy stosować statycznych zapytań SQL, a korzystać z tego co oferuje framework.

Page 37: Wprowadzenie do PHPUnit

Testy funkcjonalne (e2e)

● Pozwalają testować wymagania użytkownikaclass TestFunctional extends PHPUnit_Extensions_SeleniumTestCase{ const SITE_URL = 'http://beta.modeview.dev/'; protected function setUp() { $this->setBrowser('*firefox'); $this->setBrowserUrl(self::SITE_URL); } public function testTitle() { $this->open(self::SITE_URL); $this->assertElementPresent('div.languages'); }}

● Wymaga uruchomienia servera seleniumjava -jar selenium-server-standalone.jar

Page 38: Wprowadzenie do PHPUnit

Testowanie MVC

● Nie ma potrzeby tworzenia testów e2e by zweryfikować działanie akcji.

● Przykładem może być Zend Framework → można mockować obiekt request / response i badać efekt działania akcji poszczególnych kontrolerów (ta sama zasada dotyczy metod typu before(), after()).

● Źródłem wielu błędów są widoki. Mają one ustalone parametry przekazywane z kontrolera, możemy więc generować ich rożne wartości i przekazywać do widoków.

Page 39: Wprowadzenie do PHPUnit

Do przeczytania

● http://phpunit.de● http://artofunittesting.com/● xUnit Design Patterns● PHP Reflection API● Art of Unit Testing [Part 2]● https://github.com/ptrofimov/xpmock