wprowadzenie do phpunit
DESCRIPTION
Wprowadzenie do tworzenia testów jednostkowych w PHP (PHPUnit, testy jednostkowe, testy funkcjonalne, testy integracyjne). Wersja: alphaTRANSCRIPT
Wprowadzenie do PHPUnitBy Michał Kowalik
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
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?
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.
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).
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ł.
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
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.
●
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
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
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>
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.
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.
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...
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); }}
@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.
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.
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.
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.
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.
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();?
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
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]') );
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.
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.
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.
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(...)
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')); }}
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
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(); } }}
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'); }}
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.
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.
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>
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
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.
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
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.
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