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?
0sem comentários ainda