Como fazer testes unitários em Models no CakePHP
Olá! Dando continuidade ao post anterior Introdução a testes unitários no CakePHP e SimpleTest, este post tenta explicar como testar models.
A camada de modelo (model) geralmente é conhecida pela sua capacidade de abstrair as fontes dos dados, tornando o sistema independente de banco de dados, isto é, independente se é utilizado MySQL, Postgres ou até mesmo arquivos CSV ou XML.
Pelo fato desta camada ser responsável pela manutenção dos dados do sistema, é de extrema importância testá-la. Os testes unitários devem garantir que esta esteja funcionando de acordo, para evitar incosistências.
Vamos criar os testes unitários desde o início, desde a criação das tabelas do banco de dados, passando pelos testes e a criação do model em si.
Vamos criar no banco de dados, que já suponho estar configurado no CakePHP, a tabela com a qual vamos trabalhar. Vamos criar um Model simples para produtos.
CREATE TABLE `products` ( `id` INT(15) UNSIGNED NOT NULL AUTO_INCREMENT, `name` VARCHAR(200) NOT NULL, `description` TEXT, `price` DOUBLE(10,2) UNSIGNED NOT NULL, PRIMARY KEY (`id`) ) comment = 'Produtos' engine = InnoDB;
Tendo esta tabela no banco, podemos utilizar o próprio CakePHP para gerar automaticamente o fixture e o model. Para isto vamos executar o seguinte:
cd meu_projeto/app ../cake/console/cake bake model Product
Interagindo com o Shell do cake, você pode criar seu model de forma simples e prática. Observe que ele criou 2 arquivos:
app/tests/fixtures/product_fixture.php
app/tests/cases/models/product.test.php
Em app/tests/fixtures/product_fixture.php vamos definir nossos dados de teste, os dados que utilizaremos nos testes. Para isto, basta ajustar a propriedade $records:
public $fields = array(
'id' => array(
'type' => 'integer',
'null' => false,
'default' => NULL,
'length' => 15,
'key' => 'primary'
),
'name' => array(
'type' => 'string',
'null' => false,
'default' => NULL,
'length' => 200
),
'description' => array(
'type' => 'text',
'null' => true,
'default' => NULL
),
'price' => array(
'type' => 'float',
'null' => false,
'default' => NULL,
'length' => '10,2'
),
'indexes' => array(
'PRIMARY' => array('column' => 'id', 'unique' => 1)
),
'tableParameters' => array(
'charset' => 'utf8',
'collate' => 'utf8_general_ci',
'engine' => 'InnoDB'
)
);
public $records = Array(
Array(
'id' => 1,
'name' => 'Nome do Produto',
'description' => 'Descrição longa',
'price' => 15.23
),
Array(
'id' => 2,
'name' => 'Nome do Segundo Produto',
'description' => 'Descrição super longa',
'price' => 12
)
);
Pronto, nosso fixture se encontra com a estrutura da tabela na propriedade $fields e os dados na propriedade $records.
Em app/tests/cases/models/product.test.php ficarão nossos testes. O CakePHP já escreve algumas coisas no arquivo:
public $fixtures = array('app.product');
É essencial que todos os models relacionados devam ter seus fixtures adicionados a esta propriedade, pois senão ele não é capaz de criar as tabelas com os dados de teste.
public function startTest) {
$this->Product = ClassRegistry::init('Product');
}
Este método é executado sempre antes de cada teste (de ser executado o método que inicia com test). É interessante ter o objeto sempre "reiniciado" ao se fazer cada teste, pois senão um teste pode influenciar no valor do outro. Por exemplo, se o campo name tivesse um índice unique, poderia ter erro caso dois métodos de teste tivessem o mesmo valor sendo inserido, sendo que não era isto que estava sendo testado.
Para ter certeza que o objeto está sendo reinicializado, eu sugiro adicionar (somente necessário nos testes de 1.2.X):
public function endTest() {
unset($this->Product);
ClassRegistry::flush();
}
Assim temos mais garantia de que tudo vai funcionar como o esperado.
O teste que o CakePHP 1.2.X insere automaticamente não deixa de ser importante, porém, na versão 1.3.X do CakePHP este teste não é mais inserido automaticamente.
public function testIsA...() {
$this->asssertIsA($this->Product, 'Product');
}
Quando o ClassRegistry::init() não acha o arquivo com o model sendo inicializado, porém consegue encontrar uma tabela que satisfaça o nome deste model, o CakePHP cria automaticamente um model com a classe GenericModel. Assim, será possível saber se o CakePHP está encontrando o model na estrutura de diretórios.
Bom, a primeira coisa que quero testar neste model, é garantir que não será possível a inserção de dados vazios. Os campos são obrigatórios e devem ser preenchidos com valores não vazios.
public function testNoPassedData() {
$data = Array()
$this->assertFalse($this->Product->save($data));
$error_fields = array_keys($this->Product->validationErrors);
// Verifica se o campo deu erro na validação
$this->assertTrue(in_array('name', $error_fields);
$this->assertTrue(in_array('description', $error_fields);
$this->assertTrue(in_array('price', $error_fields);
}
public function testEmptyData() {
$data = Array(
'name' => '',
'description' => '',
'price' => ''
)
$this->assertFalse($this->Product->save($data));
$error_fields = array_keys($this->Product->validationErrors);
// Verifica se o campo deu erro na validação
$this->assertTrue(in_array('name', $error_fields);
$this->assertTrue(in_array('description', $error_fields);
$this->assertTrue(in_array('price', $error_fields);
}
Bom, o nome e a descrição podem conter o que quiser, desde que contenham alguma coisa. Então, a princípio não há necessidade de mais testes. Mas e o preço do produto?
Bom, vamos pensar no preço: um preço é um valor numérico acima de 0. Então podemos por os seguintes testes:
public function testValidPriceFormat() {
$valid_prices = Array(3, 5.04, 124.3, 12000, 0.01, 1.0000);
$data = Array(
'name' => 'nome válido',
'description' => 'descrição válida',
'price' => null // será substituido
);
foreach ($valid_prices as $counter => $price) {
$data['price'] = $price;
$data['name'] = $data['name'];
$this->assertTrue($this->Product->save($data));
}
}
public function testInvalidPriceFormat() {
$invalid_prices = Array('', -14,0,'zero','1,00', 0);
$data = Array(
'name' => 'nome válido',
'description' => 'descrição válida',
'price' => null // será substituido
);
foreach ($invalid_prices as $price) {
$data['price'] = $price;
$this->assertFalse($this->Product->save($data));
$error_fields = array_keys($this->Product->validationErrors);
// verifica se está no array de erros
$this->assertTrue(in_array('price', $error_fields));
}
}
Vejam que não vale a pena misturar as coisas: quando eu testo valores válidos ou inválidos de preço, somente o preço eu modifico. O restante das informações permancem imutáveis e devem ser válidas. O que queremos testar agora é a validação do preço e não de outras partes.
Uma outra coisa importante é considerar strings e valores com vírgulas para estes casos, são potenciais problemas! Aqui vai uma dica: sempre que trabalhar com campos inteiros, verifique o comportamento do sistema com valores 0 e negativos também!
Bom, temos nossos testes. Precisamos fazer eles passarem. Para isto, vamos adicionar o array de validação ao model.
No arquivo app/tests/cases/model/product.test.php vamos por o seguinte:
public $validate = Array(
'name' => Array(
'required' => Array(
'rule' => '/\S/',
'message' => 'Deve ser especificado um nome para o produto',
'required' => true
)
),
'description' => Array(
'required' => Array(
'rule' => '/\S/',
'message' => 'Deve ser especificado uma descrição para o produto',
'required' => true
)
),
'price' => Array(
'valid' => Array(
'rule' => array('comparison', '>', 0),
'message' => 'O preço deve ser maior que zero'
),
'format' => Array(
'rule' => array('numeric'),
'message' => 'O preço deve ser um valor numérico válido'
),
'required' => Array(
'rule' => '/\S/',
'message' => 'Deve ser especificado um preço para o produto',
'required' => true
)
)
);
Bom, agora é só rodar os testes e ver a barra verde. Simples, huh?





Comentários
Olá interessante o post, só não estou conseguindo fazer funcionar o teste de validação dos campos, o atributo $this->Product->validationErrors esta retornando um array vazio, sendo que eu já coloquei as regras de validação no model. se puder me dar um toque do que pode ser? o atributo funciona normalmente no controller
Como fazer testes unitarios em models no cakephp.. Awful :)