Hoje falarei sobre o principal framework para a realização de testes unitários no PHP, o PHPUnit.
Testes Unitários: O que são? Qual a importância?
Basicamente os testes unitários servem para verificar a integridade comportamental de unidades do sistema.
São testes feitos pelo programador, para validar o comportamento de cada unidade do sistema, na maioria das vezes métodos, dos quais são feitos testes nas mais diferentes condições sobre eles, validando o retorno de exceções, o conteúdo do retorno de um método etc. ajudando assim o programador a encontrar mais facilmente determinados erros ou comportamentos não esperados.
Se quiser entender mais os benefícios dos testes unitários, indico este artigo do André Thiago, do blog A Arte do Software: http://andrethiago.wordpress.com/2011/04/06/as-vantagens-do-teste-unitario/
Obtenção do PHPUnit
O PHPUnit pode ser obtido através do Composer pelo seguinte pacote: phpunit/phpunit.
Ou por download do arquivo PHAR (PHP Archive):
Nomenclatura dos testes
A nomenclatura deve ser o nome da classe a ser testada mais Test.
Exemplo:
Classe a ser testada: UsersEntity
Nome do teste: UsersEntityTest
Métodos
Os métodos dos testes não receberão argumentos e devem ser públicos. A nomenclatura de conter o prefixo test, além de serem auto-descritivos.
Exemplo:
Teste de um setter de nome: testSetterForName()
Métodos setUp() e tearDown()
Esses dois métodos são como o __construct() e o __descruct():
- setUp(): será executado assim que o teste for iniciado. Geralmente utilizado para pré-configurações, como instanciação de conexão com banco de dados para testes etc.
- tearDown(): será executado ao final do teste. Seguindo o exemplo anterior, desfazendo as alterações feitas no banco depois do teste, desfazendo a conexão etc.
Exemplo de teste
Primeiro vamos criar uma classe para ser testada:
class User{
private $name;
private $email;
private $age;
public function setName( $name ){
if( !is_string( $name ) ){
throw new InvalidArgumentException( 'Name must be a string' );
}
$this->name = $name;
return $this;
}
public function setEmail( $email ){
if( !is_string( $name ) ){
throw new InvalidArgumentException( 'Email must be a string' );
}
$this->email = $email;
return $this;
}
public function setAge( $age ){
if( !is_int( $age ) ){
throw new InvalidArgumentException( 'Age must be an integer' );
}
$this->age = $age;
return $this;
}
public function getName(){
return $this->name;
}
public function getEmail(){
return $this->email;
}
public function getAge(){
return $this->age;
}
}
e o teste:
class UserTest extends PHPUnit_Framework_TestCase{
public function assertPreConditions(){
$this->assertTrue( class_exists( 'User' ), 'Class not found: User' );
}
/**
* @expectedExcpetion InvalidArgumentExcpetion
* @expectedExceptionMessage Name must be a string
*/
public function testSetterForName(){
$this->assertTrue( method_exists( new User, 'setName' ), 'Method not found: User::setName()' );
$name = 'Gabriel Jacinto';
$user = new User();
$setName = $user->setName( $name );
$this->assertEquals( $setName, $user, 'Method User::setName() must return a instance of User' );
$this->assertAttributeEquals( $name, 'name', $user, 'Attribute was not correctly setted' );
}
/**
* @depends testSetterForName
*/
public function testGetterForName(){
$this->assertTrue( method_exists( new User, 'getName' ) );
$name = 'Gabriel Jacinto';
$user = new User();
$user->setName( $name );
$getName = $user->getName();
$this->assertEquals( $name, $getName, 'Method User::getName() must return the name setted in setName' );
}
}
Explicando
-
public function assertPreConditions(){
$this->assertTrue( class_exists( 'User' ), 'Class not found: User' );
}
O método assetPreConditions server para mostrar ao PHPUnit que é necessário este teste não falhar para os outros funcionarem. Ai está sendo necessário a classe User existir.Daqui a pouco explico o método assertTrue.
-
/**
* @expectedExcpetion InvalidArgumentExcpetion
* @expectedExceptionMessage Name must be a string
*/
O PHPUnit lê e interpreta anotações feitas nas classes. Dentre elas, é possível indicar qual será a exception esperada caso haja algum erro na classe e qual a mensagem esperada por essa exception. Isso também é possível pelo método setExptectedException.
-
$this->assertTrue( method_exists( new User, 'setName' ), 'Method not found: User::setName()' );
O método assertTrue verifica se uma afirmação é verdadeira, no caso a verificação se o método User::setName() estava presente na classe User. Como segundo argumento, é passada uma mensagem de erro caso o teste não passe.
-
$name = 'Gabriel Jacinto';
$user = new User();
$setName = $user->setName( $name );
É a instanciação de User e a utilização do método setName, passando como o argumento a variável $name, que é uma string. Mas se pode fazer testes propositalmente errados para verificar se o teste realmente falha.
-
$this->assertEquals( $setName, $user, 'Method User::setName() must return a instance of User' );
O método assertEquals verifica se dois valores são iguais, no caso verifica se o retorno do método setName é a própria instância da classe, no caso uma técnica chamada de Fluent Interface. Como terceiro argumento, é passada uma mensagem de erro caso o teste falhe.
-
$this->assertAttributeEquals( $name, 'name', $user, 'Attribute was not correctly setted' );
O método assertAttributeEquals verifica se algum atributo de sua classe é igual a determinado valor. Ai testamos se a variável User::$name está com o valor de $name (Gabriel Jacinto), para verificar se foi alterada corretamente. O quarto argumento é para a mensagem de erro caso o teste falhe.
-
/**
* @depends testSetterForName
*/
Como já dito, o frameowrk interpreta anotações. @depends diz ao teste que o para ele funcionar, ele necessita que o teste indicado também funcione.
Bem, explicado tudo o que foi feito, podemos seguir com os outros testes:
/**
* @exptectedException InvalidArgumentException
* @exptectedExceptionMessage Email must be a string
*/
public function testSetterForEmail(){
$this->assertTrue( method_exists( new User, 'setName' ) );
$email = 'gamjj74@hotmail.com';
$user = new User();
$setEmail = $user->setEmail( $email );
$this->assertEquals( $setEmail, $user, 'Method User::setEmail() must return a instance of User' );
$this->assertAttributeEquals( $email, 'email', $user, 'Attribute was not correctly setted' );
}
/**
* @depends testSetterForEmail
*/
public function testGetterForEmail(){
$this->assertTrue( method_exists( new User, 'getName' ) );
$email = 'gamjj74@hotmail.com';
$user = new User();
$user->setEmail( $email );
$getEmail = $user->getEmail();
$this->assertEquals( $email, $getEmail, 'getEmail must return the email setted in setEmail' );
}
/**
* @exptectedException InvalidArgumentException
* @exptectedExceptionMessage Age must be an integer
*/
public function testSetterForAge(){
$this->assertTrue( method_exists( new User, 'setAge' ) );
$age = 15;
$user = new User();
$setAge = $user->setAge( $age );
$this->assertEquals( $setEmail, $user, 'Method User::setAge() must return a instance of User' );
$this->assertAttributeEquals( $age, 'age', $user, 'Attribute was not correctly setted' );
}
/**
* @depends testSetterForAge
*/
public function testGetterForAge(){
$this->assertTrue( method_exists( new User, 'getAge' ) );
$age = 15;
$user = new User();
$user->setAge( $age );
$getAge = $user->getAge();
$this->assertEquals( $age, $getAge, 'getAge must return the age setted in setAge' );
}
Este foi um pequeno exemplo com algumas demonstrações de testes, mas você pode encontrar tudo o que precisa na documentação do PHPUnit: todos os assertions, todas as anotações aceitas e muito mais.
Rodando os testes
Para rodar os testes, basta ir no terminal e indicar qual o diretório do teste e do arquivo do PHPUnit:
php vendor/phpunit/phpunit/composer/bin/phpunit Tests/UserTest.php
Bem, por hoje é só isso. Mostrei que testes unitários são importantes para o desenvolvimento de uma aplicação e um pouco do principal framework de testes unitários do PHP, que também é usado por grandes frameworks como o Zend.