микрозаймы онлайн займы на карту займы по паспорту

Testes automatizados com Laravel 4: Models 8

A versão 4 do Laravel apresentou sérias melhoras relacionadas a testes automatizados quando comparada a versão 3. No entanto ainda assim existem alguns macetes que irão otimizar, acelerar e melhorar em geral o aspecto dos testes.

Não pretendo entrar em detalhes sobre o porque deve-se testar o código. Pois acredito que se você esta lendo esse artigo já deve muito bem saber das vantágens e provavelmente está decidido a cobrir sua aplicação de testes.

Versão original (em inglês) Testing like a boss with Laravel 4: Models

Esse é o prmeiro artigo de uma série que abordará como escrever testes para aplicações que utilizem o Laravel 4 como framework.

Preparação

Banco de dados na memória

A menos que você esteja executando queries cruas na base de dados, o Laravel permite que sua aplicação seja agnostica quanto a base de dados utilizada, ou seja, basta mudar o driver e sua aplicação já pode funcionar com outro SGBD ( MySQL, Postgree, SQLite, etc. ). Dentre as opções padrão, o SQLite possuí uma opção pecúliar e muito útil: Baso de dados na memória.

Ao utilizar o driver 'sqlite' e se conectar a base de dados :memory: é criada uma base de dados na memória. Pelo fato dessa base de dados não existir no disco rígido ela acaba sendo extremamente mais rápida. A diferença é realmente absurda, esse é um detalhe que vai acelerar muito a execução dos testes. Além disso, sua base de dados principal não ficará cheia de dados inseridos pelos testes e ao mesmo tempo a base de testes estará sempre limpa ao inicio de cada teste, pois a conexão :memory: sempre inicia uma base de dados vazia.

Em resumo: Base de dados na memória, banco de dados rápido e sempre limpo.

No diretório app/config/testing crie um novo arquivo chamado database.php com o seguinte conteúdo:

// app/config/testing/database.php

<?php

return array(

    'default' => 'sqlite',

    'connections' => array(
        'sqlite' => array(
            'driver'   => 'sqlite',
            'database' => ':memory:',
            'prefix'   => '',
        ),
    ),
);

O fato do arquivo database.php estar dentro do diretório testing das configurações permite que essas configurações sejam utilizadas apenas para os testes. Portanto quando sua aplicação estiver sendo executada normalmente a base de dados na memória não será utilizada.

Preparar antes de testar

Uma vez que a base de dados na memória está sempre vazia assim que a conexão é feita. É preciso certficar-se de executar as migrations antes de qualquer coisa. Para isso, abra o arquivo app/tests/TestCase.php. E adicione o seguinte método ao final da classe:

/**
 * Migrates the database and set the mailer to 'pretend'.
 * This will cause the tests to run quickly.
 *
 */
private function prepareForTests()
{
    Artisan::call('migrate');
    Mail::pretend(true);
}

Esse método visa preparar a base de dados e alterar o estado do Mailer do Laravel para pretend. O Mailer não dispara e-mails de verdade quando estiver no modo pretend, no entanto ele salva um log com as mensagens enviadas. Isso vai acelerar os testes e evitar que sejam enviados e-mails a endereços usados apenas nos testes.

Por fim, ainda no arquivo app/tests/TestCase.php, declare o método setUp() chamando o método prepareForTests(), mas não se esqueça do parent::setUp(), uma vez que estamos sobre-escrevendo o método da classe pai.

/**
 * Default preparation for each test
 *
 */
public function setUp()
{
    parent::setUp(); // Don't forget this
    $this->prepareForTests();
}

OBS: O método setUp() é sempre executado pelo PHPUnit antes de cada um dos testes.

Por final, o arquivo app/tests/TestCase.php deve se parecer com o seguinte:

// app/tests/TestCase.php

<?php

class TestCase extends Illuminate\Foundation\Testing\TestCase {

    /**
     * Default preparation for each test
     */
    public function setUp()
    {
        parent::setUp();
        $this->prepareForTests();
    }

    /**
     * Creates the application.
     *
     * @return Symfony\Component\HttpKernel\HttpKernelInterface
     */
    public function createApplication()
    {
        $unitTesting = true;

        $testEnvironment = 'testing';

        return require __DIR__.'/../../start.php';
    }

    /**
     * Migrates the database and set the mailer to 'pretend'.
     * This will cause the tests to run quickly.
     */
    private function prepareForTests()
    {
        Artisan::call('migrate');
        Mail::pretend(true);
    }
}

Assim, para escrevermos nossos testes, basta extender a classe TestCase e toda a base de dados estará pronta e limpa antes de cada teste.

Os Testes

É correto dizer que nesse artigo não estamos fazendo TDD uma vez que o código dos models será mostrado antes dos testes.
A questão aqui é didática, com o objetivo de demonstrar como os testes podem ser escritos. Devido a isso opto por mostrar primeiramente os modelos em questão, e depois os testes relativos. Acredito que essa é uma forma melhor de exemplificar uma situação em que testes são empregados.

OBS: É uma boa prática o emprego do TDD que é, em resumo, escrever os testes antes de escrever o código do modelo.

O contexto da aplicação é um blog/cms simples. Contendo usuários (autenticação), posts e páginas estáticas (que são mostradas no menu).

Model Post

Observe que o modelo extende a classe Ardent ao invés de Eloquent. Ardent é um package que permite a validação (antes de salvar) dos models de forma fácil, as regras são definidas na array public static $rules.

Além disso, existe a array public static $factory que discrimina valores a serem gerados no caso de testes, graças ao package FactoryMuff.

OBS: Os pacotes Ardent e FactoryMuff estão disponíveis via Composer.

No nosso model temos apenas uma relação com o model User através do “alias” author.

E por fim um método simples que retorna a data formatada como “dia/mês/Ano”.

// app/models/Post.php

<?php

use LaravelBook\Ardent\Ardent;

class Post extends Ardent {

    /**
     * Table
     */
    protected $table = 'posts';

    /**
     * Ardent validation rules
     */
    public static $rules = array(
        'title' => 'required',              // Post tittle
        'slug' => 'required|alpha_dash',    // Post Url
        'content' => 'required',            // Post content (Markdown)
        'author_id' => 'required|numeric',  // Author id
    );

    /**
     * Array used by FactoryMuff to create Test objects
     */
    public static $factory = array(
        'title' => 'string',
        'slug' => 'string',
        'content' => 'text',
        'author_id' => 'factory|User', // Will be the id of an existent User.
    );

    /**
     * Belongs to user
     */
    public function author()
    {
        return $this->belongsTo( 'User', 'author_id' );
    }

    /**
     * Get formatted post date
     *
     * @return string
     */
    public function postedAt()
    {
        $date_obj =  $this->created_at;

        if (is_string($this->created_at))
            $date_obj =  DateTime::createFromFormat('Y-m-d H:i:s', $date_obj);

        return $date_obj->format('d/m/Y');
    }
}

Testes de Post

Por questões de organização, eu posicionei a classe com os testes do Post em: app/tests/models/PostTest.php. Segue o conteúdo do arquivo de forma seccionada afím de explicar cada trecho:

// app/tests/models/PostTest.php

<?php

use Zizaco\FactoryMuff\Facade\FactoryMuff;

class PostTest extends TestCase
{

Note que extendemos a classe TestCase, assim a base de dados estará migrada e limpa e o envio real de e-mails será desativado. (Devido a implementação feita anteriormente).

    public function test_relation_with_autor()
    {
        // Instantiate, fill with values, save and return
        $post = FactoryMuff::create('Post');

        // Thanks to FactoryMuff this $post have an author
        $this->assertEquals( $post->author_id, $post->author->id );
    }

Esse é um teste “opcional”. Estamos testando a relação “Post pertence a User“. A finalidade aqui é mostrar o funcionamento do FactoryMuff:

Uma vez que Post tem a array estática $factory contendo 'author_id' => 'factory|User' (observe no código fonte do model, mostrado anteriormente) o FactoryMuff intancia um novo usuário, preencher os seus atributos, salvar no banco de dados e por fim retornar o id dele para o capo author_id do Post.

Pra isso, no modelo User existe uma array $factory também declarada e seguindo o padrão do FactoryMuff.

Note como é possível acessar o User relacionado através de $post->author sendo assim possível acessar $post->author->username ou qualquer campo do usuário existente.

O FactoryMuff permite a rápida instanciação de objetos para a finalidade de testes e respeitando e instanciando quaisquer relações necessárias. Nesse caso, ao criar o Post com o commando FactoryMuff::create('Post') o User também será preparado e estará disponível como em uma execução real.

    public function test_posted_at()
    {
        // Instantiate, fill with values, save and return
        $post = FactoryMuff::create('Post');

        // Regular expression that represents d/m/Y pattern
        $expected = '/\d{2}\/\d{2}\/\d{4}/';

        // True if preg_match finds the pattern
        $matches = ( preg_match($expected, $post->postedAt()) ) ? true : false;

        $this->assertTrue( $matches );
    }
}

Para finalizar os testes, verificamos se a string retornada pelo método postedAt() segue o padrão “dia/mês/Ano”. Para tal verificação é utilizada uma expressão regular que testa se existe o padrão \d{2}\/\d{2}\/\d{4} (“2 numeros” + “barra” + “2 numeros” + “barra” + “4 numeros”).

Ao final, nosso arquivo app/tests/models/PostTest.php é o seguinte:

// app/tests/models/PostTest.php

<?php

use Zizaco\FactoryMuff\Facade\FactoryMuff;

class PostTest extends TestCase
{
    public function test_relation_with_autor()
    {
        // Instantiate, fill with values, save and return
        $post = FactoryMuff::create('Post');

        // Thanks to FactoryMuff this $post have an author
        $this->assertEquals( $post->author_id, $post->author->id );
    }

    public function test_posted_at()
    {
        // Instantiate, fill with values, save and return
        $post = FactoryMuff::create('Post');

        // Regular expression that represents d/m/Y pattern
        $expected = '/\d{2}\/\d{2}\/\d{4}/';

        // True if preg_match finds the pattern
        $matches = ( preg_match($expected, $post->postedAt()) ) ? true : false;

        $this->assertTrue( $matches );
    }
}

OBS: Optei por escrever o nome dos testes sem utilizar CamelCase para finalidade de legibilidade. PSR-1 que me perdoe mas, testRelationWithAuthor não é tão legível quanto eu gostaria.

Model Page

Para nosso cms precisamos de um model para representar cada uma das páginas. Esses models foram implementados da seguinte forma:

<?php

// app/models/Page.php

use LaravelBook\Ardent\Ardent;

class Page extends Ardent {

    /**
     * Table
     */
    protected $table = 'pages';

    /**
     * Ardent validation rules
     */
    public static $rules = array(
        'title' => 'required',              // Page Title
        'slug' => 'required|alpha_dash',    // Slug (url)
        'content' => 'required',            // Content (markdown)
        'author_id' => 'required|numeric',  // Author id
    );

    /**
     * Array used by FactoryMuff
     */
    public static $factory = array(
        'title' => 'string',
        'slug' => 'string',
        'content' => 'text',
        'author_id' => 'factory|User',  // Will be the id of an existent User.
    );

    /**
     * Belongs to user
     */
    public function author()
    {
        return $this->belongsTo( 'User', 'author_id' );
    }

    /**
     * Renders the menu using cache
     *
     * @return string Html for page links.
     */
    public static function renderMenu()
    {
        $pages = Cache::rememberForever('pages_for_menu', function()
        {
            return Page::select(array('title','slug'))->get()->toArray();
        });

        $result = '';

        foreach( $pages as $page )
        {
            $result .= HTML::action( '[email protected]', $page['title'], ['slug'=>$page['slug']] ).' | ';
        }

        return $result;
    }

    /**
     * Forget cache when saved
     */
    public function afterSave( $success )
    {
        if( $success )
            Cache::forget('pages_for_menu');
    }

    /**
     * Forget cache when deleted
     */
    public function delete()
    {
        parent::delete();
        Cache::forget('pages_for_menu');
    }

}

Podemos observar que o método estático renderMenu() renderiza uma serie de links para todas as páginas existentes. Esse valor é salvo no cache na chave 'pages_for_menu' de forma que em futuras chamadas do renderMenu() não haja a necessidade de acessar o banco de dados com a finalidade de otimizar o desempenho da aplicação.

No entanto, caso uma Page seja salva ou excluída (métodos afterSave() e delete()) o valor do cache é apagado para que os menu renderizado por renderMenu() reflita no novo estado das paginas existentes. Assim, caso o nome de uma pagina for alterado, ou esta for excluída, a chave 'pages_for_menu' é apagada do cache. (Cache::forget('pages_for_menu');)

OBS: O método afterSave() é proveniente do pacote Ardent. Do contrário seria necessário implementar o método save() de forma semelhante a como foi feito no método delete(), ou seja, chamando o parent::save();

Testes de Page

Em: app/tests/models/PageTest.php. Teremos o seguinte conteúdo:

<?php

// app/tests/models/PageTest.php

use Zizaco\FactoryMuff\Facade\FactoryMuff;

class PageTest extends TestCase
{
    public function test_get_author()
    {
        $page = FactoryMuff::create('Page');

        $this->assertEquals( $page->author_id, $page->author->id );
    }

Mais uma vez o teste “opcional” de relacionamento. Uma vez que a responsabilidade dos relacionamentos é do Illuminate\Database\Eloquent, classe que já é coberta pelos testes do Laravel. De qualquer forma, vamos assumir que queremos nos certificar de que o relacionamento está ocorrendo como devia, então por isso o test_get_author().

    public function test_render_menu()
    {
        $pages = array();

        for ($i=0; $i < 4; $i++) { 
            $pages[] = FactoryMuff::create('Page');
        }

        $result = Page::renderMenu();

        foreach ($pages as $page)
        { 
            // Check if each page slug(url) is present in the menu rendered.
            $this->assertGreaterThan(0, strpos($result, $page->slug));
        }

        // Check if cache has been written
        $this->assertNotNull(Cache::get('pages_for_menu'));
    }

Provavelmente esse é o teste mais importante do model Page. Primeiramente são criadas 4 paginas no loop for. Após guardamos o resultado do renderMenu() na variável $result. Essa variável deve conter um código HTML conténdo os links para as paginas existentes.

O Loop foreach verifica se a slug (url) de cada pagina está presente no $result. Isso é o suficiente, visto que o formato exato do HTML não é tão relevante.

Por fim, verificamos se o cache possuí a chave pages_for_menu. Ou seja, se o método renderMenu() realmente guardou algum valor no cache.

    public function test_clear_cache_after_save()
    {
        // An test value is saved in cache
        Cache::put('pages_for_menu','avalue', 5);

        // This should clean the value in cache
        $page = FactoryMuff::create('Page');

        $this->assertNull(Cache::get('pages_for_menu'));
    }

Esse teste visa verificar se ao salvar uma nova Page a chave 'pages_for_menu' do cache é esvaziada. A chamada FactoryMuff::create('Page'); acaba por executar o save(), portanto deve bastar para que a chave 'pages_for_menu' cache seja limpa.

    public function test_clear_cache_after_delete()
    {
        $page = FactoryMuff::create('Page');

        // An test value is saved in cache
        Cache::put('pages_for_menu','value', 5);

        // This should clean the value in cache
        $page->delete();

        $this->assertNull(Cache::get('pages_for_menu'));
    }

Semelhante ao teste anterior. Verifica se a chave 'pages_for_menu' é esvaziada devidamente após performar a exclusão de uma Page.

Ao final, o nosso arquivo de testes é:

<?php

// app/tests/models/PageTest.php

use Zizaco\FactoryMuff\Facade\FactoryMuff;

class PageTest extends TestCase
{
    public function test_get_author()
    {
        $page = FactoryMuff::create('Page');

        $this->assertEquals( $page->author_id, $page->author->id );
    }

    public function test_render_menu()
    {
        $pages = array();

        for ($i=0; $i < 4; $i++) { 
            $pages[] = FactoryMuff::create('Page');
        }

        $result = Page::renderMenu();

        foreach ($pages as $page)
        { 
            // Check if each page slug(url) is present in the menu rendered.
            $this->assertGreaterThan(0, strpos($result, $page->slug));
        }

        // Check if cache has been written
        $this->assertNotNull(Cache::get('pages_for_menu'));
    }

    public function test_clear_cache_after_save()
    {
        // An test value is saved in cache
        Cache::put('pages_for_menu','avalue', 5);

        // This should clean the value in cache
        $page = FactoryMuff::create('Page');

        $this->assertNull(Cache::get('pages_for_menu'));
    }

    public function test_clear_cache_after_delete()
    {
        $page = FactoryMuff::create('Page');

        // An test value is saved in cache
        Cache::put('pages_for_menu','value', 5);

        // This should clean the value in cache
        $page->delete();

        $this->assertNull(Cache::get('pages_for_menu'));
    }
}

Model User

Relacionado com os modelos apresentados anteriormente temos o modelo User. Segue o código do modelo:

<?php

// app/models/User.php

use Zizaco\Confide\ConfideUser;

class User extends ConfideUser {

    // Array used in FactoryMuff
    public static $factory = array(
        'username' => 'string',
        'email' => 'email',
        'password' => '123123',
        'password_confirmation' => '123123',
    );

    /**
     * Has many pages
     */
    public function pages()
    {
        return $this->hasMany( 'Page', 'author_id' );
    }

    /**
     * Has many posts
     */
    public function posts()
    {
        return $this->hasMany( 'Post', 'author_id' );
    }

}

Esse modelo é ausente de testes.

Podemos observar que, com exceção das relações, não existe métodos implementados no model User. Isso ocorre pois a utilização do Package Confide já fornece a implementação das funcionalidades básicas de usuário.

Inclusive, os testes para Zizaco\Confide\ConfideUser se localizam no arquivo ConfideUserTest.php.

É importante determinar muito bem as responsabilidades antes de escrever os testes. Testar a opção de “resetar o password do usuário” do modelo User seria uma redundância, podendo ser considerado desorganizado. Pois a responsabilidade por essa função é da classe Zizaco\Confide\ConfideUser.

O mesmo serve para testes de validação dos dados. Uma vez que o pacote Ardent tem essa responsabilidade, não faria muito sentido testar novamente se a validação dos campos ocorre como deveria.

Por isso os testes escritos para as relações são um tanto controversos e foram descritos como “opcionais”. Eles foram mostrados no artigo como forma de exemplo.

Em resumo: Mantenha seus testes limpos e organizados. Determine bem a responsabilidade de cada classe e teste somente o que for de estrita responsabilidade desta.

Conclusão

Running Tests

A utilização da base de dados na memória é uma boa prática para executar os testes rapidamente, visto que não é feito acesso ao disco rígido. Além disso, a base de dados estará sempre limpa e pronta para os testes a cada execução.

Com a ajuda de alguns pacotes (Ardent, FactoryMuff e Confide) é possível minimizar o volume de código nos models enquanto mantém os testes limpos e objetivos.

É importante definir muito bem o que deve ser testado. Evitar fazer testes redundantes, determinar a responsabilidade de cada classe antes de escrever os testes é uma boa prática.

Em sequencia a esse artigo, vamos dar uma olhada em testes de Controllers.

  • http://brayanrastelli.com/ Brayan Rastelli

    Muito Top !

    Esperando a parte de controllers :-)

    Valeu !

  • http://brayanrastelli.com/ Brayan Rastelli

    Apenas uma dúvida, qual a diferença do seu package FactoryMuff com o Mockery?

  • Zizaco Zizuini

    Brayan Rastelli, São finalidades diferentes.

    O Mockery visa criar objetos que não são “reais”, O que é muito útil em certos casos.

    Já o FactoryMuff tem o objetivo de criar objetos (comumente dos Models) de forma mais fiel.

    Por ex: No contexto de testar o model, controller ou mesmo a view de ‘Notícias’ que possua relações com ‘Autor’, ‘Categoria’, ‘Tags’ e ‘Comentários’, onde ‘Comentários’ também tem um ‘Autor’.

    Com mockery você teria que escrever várias linhas para “preparar” o objeto mockado para reagir a todas as relações. Se por exemplo sua view possuir a seguinte linha:

    {{ $post->comments[2]->author->email }}

    Imagine a dor de cabeça e o quão seu Mock vai ter de “fugir” do comportamento real do modelo para testar essa unica linha.

    Sem o uso do Mockery, você teria de instanciar, preencher os attributos e salvar cada um dos objetos dessa “corrente” de relações. O que acaba sendo “melhor” pois você esta realmente lidando com objetos reais (que devem ser testados, eu presumo).

    De ambas as formas (Mockery ou com Objetos Reais) será preciso uma quantidade considerável de código para testar aquela unica linha.

    Agora, com o FactoryMuff basta usar o comando:

    $existing_post = FactoryMuff::create(‘Post’);

    E toda a sequencia de objetos será realmente criada. Além disso essa notícia vai possuir valores “reais”. Ex:

    echo $existing_post->title; // Bacon
    echo $existing_post->content; // something table underrated blackboard
    echo $existing_post->author->email; // [email protected]
    echo $existing_post->author->name; // Streets
    echo Author::first()->name; // Streets (já que o objeto realmente existe no db)
    $existing_post->author->name = “John”;
    $existing_post->author->save(); // update no registro
    echo Author::first()->name; // John

    Os valores são gerados aleatoriamente, o que ajuda a concluír se sua aplicação está “pronta para o mundo real”.

    Outra utilidade interessante é para gerar parâmetros de POST e PUT:

    $input = FactoryMuff::attributesFor(‘Comment’); // Não salva no banco.
    echo $input[‘title’] // work
    echo $input[‘post_id’] // 2
    Post::find( $input[‘post_id’] ) // Esse post vai de fato existir

    Assim, para testar se um comentário é salvo (caso ele valide a presença de um post/author), você não vai precisar escrever código para instanciar/salvar o Post e o Author. Eles surgirão automaticamente no banco. E o id destes ficará presente nos atributos gerados.

    Não sei se deu pra entender… hehehe, mas depois posso escrever uns exemplos práticos mais elaborados.

    Por fim, se o Mockery (ou Mock em geral) é indispensável para testes de bibliotecas e pacotes. O FactoryMuff (ou equivalente) é indispensável para testes de aplicações finais (aonde muitas vezes é preciso testar a interação entre objetos reais).

    • http://brayanrastelli.com/ Brayan Rastelli

      Show.. vlw !

  • http://twitter.com/diegofelix Diego Felix

    Eu tento, tento, tento, mas não consigo fazer testes nos meus sistemas. Só consigo testar Libraries e Models, eu não consigo pensar em como testar o resto =/

    • Zizaco Zizuini

      Sim, Libraries e Models são mais simples de se testar. Acredito que o próximo artigo da série irá lhe ajudar a testar os seus Controllers :)

  • http://twitter.com/juliooxx Julio Fagundes

    Eu assino o nettuts e lendo esse post vi que você que escreveu o artigo la! que maneiro, to vindo do CI e to procurando bastante sobre o laravel 4, acho que encontrei uma bela fonte aqui rs
    Obrigado por compartilhar seu conhecimento.

  • http://twitter.com/Fuhrmanns Ricardo Fuhrmann

    Eu tava tendo um erro com esse jeito de testar utilizando o sqlite com :memory:, e consegui fazer funcionar adicionando no arquivo database.php da pasta testing isso:

    ‘migrations’ => ‘migrations’