PHP
Testów jednostkowych
Szukaj…
Składnia
- Pełna lista twierdzeń . Przykłady:
-
assertTrue(bool $condition[, string $messageIfFalse = '']);
-
assertEquals(mixed $expected, mixed $actual[, string $messageIfNotEqual = '']);
Uwagi
Testy Unit
służą do testowania kodu źródłowego, aby sprawdzić, czy zawiera on transakcje z danymi wejściowymi, tak jak się spodziewamy. Testy Unit
są obsługiwane przez większość platform. Istnieje kilka różnych testów PHPUnit i mogą one różnić się składnią. W tym przykładzie używamy PHPUnit
.
Testowanie reguł klasowych
Powiedzmy, że mamy prostą klasę LoginForm
z metodą rules () (używaną na stronie logowania jako szablon frameworka):
class LoginForm {
public $email;
public $rememberMe;
public $password;
/* rules() method returns an array with what each field has as a requirement.
* Login form uses email and password to authenticate user.
*/
public function rules() {
return [
// Email and Password are both required
[['email', 'password'], 'required'],
// Email must be in email format
['email', 'email'],
// rememberMe must be a boolean value
['rememberMe', 'boolean'],
// Password must match this pattern (must contain only letters and numbers)
['password', 'match', 'pattern' => '/^[a-z0-9]+$/i'],
];
}
/** the validate function checks for correctness of the passed rules */
public function validate($rule) {
$success = true;
list($var, $type) = $rule;
foreach ((array) $var as $var) {
switch ($type) {
case "required":
$success = $success && $this->$var != "";
break;
case "email":
$success = $success && filter_var($this->$var, FILTER_VALIDATE_EMAIL);
break;
case "boolean":
$success = $success && filter_var($this->$var, FILTER_VALIDATE_BOOLEAN, FILTER_NULL_ON_FAILURE) !== null;
break;
case "match":
$success = $success && preg_match($rule["pattern"], $this->$var);
break;
default:
throw new \InvalidArgumentException("Invalid filter type passed")
}
}
return $success;
}
}
Aby przeprowadzić testy na tej klasie, używamy testów jednostkowych (sprawdzając kod źródłowy, aby sprawdzić, czy spełnia nasze oczekiwania):
class LoginFormTest extends TestCase {
protected $loginForm;
// Executing code on the start of the test
public function setUp() {
$this->loginForm = new LoginForm;
}
// To validate our rules, we should use the validate() method
/**
* This method belongs to Unit test class LoginFormTest and
* it's testing rules that are described above.
*/
public function testRuleValidation() {
$rules = $this->loginForm->rules();
// Initialize to valid and test this
$this->loginForm->email = "[email protected]";
$this->loginForm->password = "password";
$this->loginForm->rememberMe = true;
$this->assertTrue($this->loginForm->validate($rules), "Should be valid as nothing is invalid");
// Test email validation
// Since we made email to be in email format, it cannot be empty
$this->loginForm->email = '';
$this->assertFalse($this->loginForm->validate($rules), "Email should not be valid (empty)");
// It does not contain "@" in string so it's invalid
$this->loginForm->email = 'invalid.email.com';
$this->assertFalse($this->loginForm->validate($rules), "Email should not be valid (invalid format)");
// Revert email to valid for next test
$this->loginForm->email = '[email protected]';
// Test password validation
// Password cannot be empty (since it's required)
$this->loginForm->password = '';
$this->assertFalse($this->loginForm->validate($rules), "Password should not be valid (empty)");
// Revert password to valid for next test
$this->loginForm->password = 'ThisIsMyPassword';
// Test rememberMe validation
$this->loginForm->rememberMe = 999;
$this->assertFalse($this->loginForm->validate($rules), "RememberMe should not be valid (integer type)");
// Revert remeberMe to valid for next test
$this->loginForm->rememberMe = true;
}
}
Jak dokładnie testy Unit
mogą pomóc (z wyjątkiem ogólnych przykładów) tutaj? Na przykład pasuje bardzo dobrze, gdy otrzymujemy nieoczekiwane wyniki. Na przykład weźmy tę regułę z wcześniejszych:
['password', 'match', 'pattern' => '/^[a-z0-9]+$/i'],
Zamiast tego, jeśli przegapiliśmy jedną ważną rzecz i napisaliśmy to:
['password', 'match', 'pattern' => '/^[a-z0-9]$/i'],
Przy dziesiątkach różnych zasad (zakładając, że używamy nie tylko adresu e-mail i hasła), trudno jest wykryć błędy. Ten test jednostkowy:
// Initialize to valid and test this
$this->loginForm->email = "[email protected]";
$this->loginForm->password = "password";
$this->loginForm->rememberMe = true;
$this->assertTrue($this->loginForm->validate($rules), "Should be valid as nothing is invalid");
Zda nasz pierwszy przykład, ale nie drugi . Dlaczego? Ponieważ w drugim przykładzie napisaliśmy wzór z literówką (brak +
znak), co oznacza, że akceptuje tylko jedną literę / cyfrę.
Testy jednostkowe można uruchomić w konsoli za pomocą polecenia: phpunit [path_to_file]
. Jeśli wszystko jest w porządku, powinniśmy być w stanie zobaczyć, że wszystkie testy są OK
stanie, w przeciwnym wypadku będziemy świadkami albo Error
(błędy składniowe) lub Fail
(przynajmniej jedna linia w tej metodzie nie przechodzą).
Dzięki dodatkowym parametrom, takim jak --coverage
, możemy również wizualnie zobaczyć, ile wierszy kodu zaplecza zostało przetestowanych i które przeszły / nie powiodły się. Dotyczy to każdego frameworka, w którym zainstalowano PHPUnit .
Przykładowy wygląd testu PHPUnit
w konsoli (wygląd ogólny, niezgodny z tym przykładem):
PHPUnit Data Providers
Metody testowe często wymagają testowania danych. Aby całkowicie przetestować niektóre metody, musisz podać różne zestawy danych dla każdego możliwego warunku testu. Oczywiście możesz to zrobić ręcznie za pomocą pętli:
...
public function testSomething()
{
$data = [...];
foreach($data as $dataSet) {
$this->assertSomething($dataSet);
}
}
...
I ktoś może uznać to za wygodne. Istnieją jednak pewne wady tego podejścia. Najpierw musisz wykonać dodatkowe czynności, aby wyodrębnić dane, jeśli funkcja testowa akceptuje kilka parametrów. Po drugie, w przypadku awarii trudno byłoby odróżnić uszkodzony zestaw danych bez dodatkowych komunikatów i debugowania. Po trzecie, PHPUnit zapewnia automatyczny sposób radzenia sobie z zestawami danych testowych przy użyciu dostawców danych .
Dostawca danych to funkcja, która powinna zwracać dane dla konkretnego przypadku testowego.
Metoda dostawcy danych musi być publiczna i zwracać tablicę tablic lub obiekt, który implementuje interfejs Iterator i zwraca tablicę dla każdego kroku iteracji. Dla każdej tablicy, która jest częścią kolekcji, zostanie wywołana metoda testowa z zawartością tablicy jako argumentami.
Aby użyć dostawcy danych w teście, użyj adnotacji @dataProvider
z @dataProvider
nazwą funkcji dostawcy danych:
/**
* @dataProvider dataProviderForTest
*/
public function testEquals($a, $b)
{
$this->assertEquals($a, $b);
}
public function dataProviderForTest()
{
return [
[1,1],
[2,2],
[3,2] //this will fail
];
}
Tablica tablic
Zauważ, że
dataProviderForTest()
zwraca tablicę tablic. Każda zagnieżdżona tablica ma dwa elementy i będą one wypełniać parametry niezbędne dotestEquals()
jeden po drugim. Błąd taki jak ten zostanieMissing argument 2 for Test::testEquals()
jeśli nie ma wystarczającej liczby elementów. PHPUnit automatycznie zapętla dane i uruchamia testy:
public function dataProviderForTest()
{
return [
[1,1], // [0] testEquals($a = 1, $b = 1)
[2,2], // [1] testEquals($a = 2, $b = 2)
[3,2] // [2] There was 1 failure: 1) Test::testEquals with data set #2 (3, 4)
];
}
Każdy zestaw danych można nazwać dla wygody. Łatwiej będzie wykryć błędne dane:
public function dataProviderForTest()
{
return [
'Test 1' => [1,1], // [0] testEquals($a = 1, $b = 1)
'Test 2' => [2,2], // [1] testEquals($a = 2, $b = 2)
'Test 3' => [3,2] // [2] There was 1 failure:
// 1) Test::testEquals with data set "Test 3" (3, 4)
];
}
Iteratory
class MyIterator implements Iterator {
protected $array = [];
public function __construct($array) {
$this->array = $array;
}
function rewind() {
return reset($this->array);
}
function current() {
return current($this->array);
}
function key() {
return key($this->array);
}
function next() {
return next($this->array);
}
function valid() {
return key($this->array) !== null;
}
}
...
class Test extends TestCase
{
/**
* @dataProvider dataProviderForTest
*/
public function testEquals($a)
{
$toCompare = 0;
$this->assertEquals($a, $toCompare);
}
public function dataProviderForTest()
{
return new MyIterator([
'Test 1' => [0],
'Test 2' => [false],
'Test 3' => [null]
]);
}
}
Jak widać, działa również prosty iterator.
Pamiętaj, że nawet dla jednego parametru dostawca danych musi zwrócić tablicę
[$parameter]
Ponieważ jeśli zmienimy naszą metodę current()
(która faktycznie zwraca dane przy każdej iteracji) na to:
function current() {
return current($this->array)[0];
}
Lub zmień rzeczywiste dane:
return new MyIterator([
'Test 1' => 0,
'Test 2' => false,
'Test 3' => null
]);
Otrzymamy błąd:
There was 1 warning:
1) Warning
The data provider specified for Test::testEquals is invalid.
Oczywiście nie jest użyteczne używanie obiektu
Iterator
na prostej tablicy. Powinien zaimplementować określoną logikę dla twojego przypadku.
Generatory
Nie jest to wyraźnie zaznaczone i pokazane w instrukcji, ale można również użyć generatora jako dostawcy danych. Zauważ, że klasa Generator
faktycznie implementuje interfejs Iterator
.
Oto przykład użycia DirectoryIterator
połączeniu z generator
:
/**
* @param string $file
*
* @dataProvider fileDataProvider
*/
public function testSomethingWithFiles($fileName)
{
//$fileName is available here
//do test here
}
public function fileDataProvider()
{
$directory = new DirectoryIterator('path-to-the-directory');
foreach ($directory as $file) {
if ($file->isFile() && $file->isReadable()) {
yield [$file->getPathname()]; // invoke generator here.
}
}
}
Dostawca notatek
yield
tablicę. Zamiast tego otrzymasz ostrzeżenie o nieprawidłowym dostawcy danych.
Testuj wyjątki
Powiedzmy, że chcesz przetestować metodę, która zgłasza wyjątek
class Car
{
/**
* @throws \Exception
*/
public function drive()
{
throw new \Exception('Useful message', 1);
}
}
Możesz to zrobić, umieszczając wywołanie metody w bloku try / catch i dokonując asercji na temat właściwości obiektu wykonawczego, ale wygodniej można użyć metod asercji wyjątków. Od PHPUnit 5.2 dostępne są metody expectX () do potwierdzania typu wyjątku, komunikatu i kodu
class DriveTest extends PHPUnit_Framework_TestCase
{
public function testDrive()
{
// prepare
$car = new \Car();
$expectedClass = \Exception::class;
$expectedMessage = 'Useful message';
$expectedCode = 1;
// test
$this->expectException($expectedClass);
$this->expectMessage($expectedMessage);
$this->expectCode($expectedCode);
// invoke
$car->drive();
}
}
Jeśli korzystasz z wcześniejszej wersji PHPUnit, zamiast setMode () można zastosować metodę setExpectedException, ale pamiętaj, że jest przestarzała i zostanie usunięta w wersji 6.
class DriveTest extends PHPUnit_Framework_TestCase
{
public function testDrive()
{
// prepare
$car = new \Car();
$expectedClass = \Exception::class;
$expectedMessage = 'Useful message';
$expectedCode = 1;
// test
$this->setExpectedException($expectedClass, $expectedMessage, $expectedCode);
// invoke
$car->drive();
}
}