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?
Posts Relacionados
Por favor, se você deseja opinar, criticar ou até mesmo mandar uma receita de bolo, deixe um comentário! Ou fique atento aos feeds.





Comentários
Nenhum comentário ainda.
Deixe um comentário