Hostingheaderbarlogoj
Join InMotion Hosting for $3.49/mo & get a year on Tuts+ FREE (worth $180). Start today.
Advertisement

Testando Controladores no Laravel

by

Portuguese (Português) translation by Erick Patrick (you can also view the original English article)

Testar controladores não é uma das tarefas mais fáceis. Bem, deixe-me reformular essa frase: Testar controladores é fácil, o problema, pelo menos inicialmente, é o que se deve testar

Um teste de controlador deve verificar textos em uma página? Ele deveria comunicar-se com a base de dados? Deve garantir que certas variáveis existem nas visões? Se essa é sua primeira vez com testes, todos esses pontos podem ser bem confusos! Deixe-me ajudá-lo.

Testes de controladores devem verificar respostas retornadas; garantir a chamada ao métodos corretos de acesso às bases de dados; e confirmar que as variáveis de instância corretas foram enviadas para a visão.

O processo de testar um controlador pode ser dividido em três partes:

  • Isolamento: Simule todas as dependências (exceto, talvez a classe View).
  • Chamada: Chame o método desejado do controlador.
  • Garantia: Execute os métodos de teste, verificando se tudo foi feito corretamente.

O "Hello World" dos Testes de Controladores

A melhor maneira de aprender algo é através de exemplos. Eis o equivalente a um "hello world" para testes de controladores no Laravel.

<?php

# app/tests/controllers/PostsControllerTest.php

class PostsControllerTest extends TestCase {

public function testIndex()
{
    $this->client->request('GET', 'posts');
}

}

O Laravel lança mão de vários componentes do Symfony para facilitar o processo de testar rotas e visões, incluindo o HttpKernel, DomCrawler e BrowserKit. É por isso que é importante seus testes com PHPUnit estendam a classe TestCase, não a classe PHPUnit\_Framework\_TestCase. Não se preocupe, a classe que o Laravel provê estende a classe original do PHPUnit. Além disso, ela ajuda a preparar sua aplicação em Laravel para os testes que você criará, além de prover uma variedade de métodos que auxiliarão em seus testes e que seria bom você utilizá-los. Nós falaremos mais sobre eles daqui a pouco.

No trecho de código acima, nós fazemos uma requisição GET para /posts ou localhost:8000/posts. Assumindo que esse código é adicionado a uma instalação recém criada do Laravel, o Symfony lançará uma exceção do tipo NotFoundHttpException. Se estiver testando os códigos, vá até a linha de comando e execute o comando phpunit.

$ phpunit
1) PostsControllerTest::testIndex
Symfony\Component\HttpKernel\Exception\NotFoundHttpException:

Traduzindo para algo que humanos entendam, isso, essencialmente, está dizendo: "Ei, tentei chamar aquela rota, mas você não registrou algo, ainda, bobo!"

Como pode imaginar, esse tipo de requisição é tão comum, que faz sentido ter um método auxiliar. Algo como $this->call(). Para falar a verdade, o Laravel faz isso! Isso significa que o exemplo anterior pode ser refatorado, dessa forma:

#app/tests/controllers/PostsControllerTest.php

public function testIndex()
{
  $this->call('GET', 'posts');
}

A Sobrecarga é sua Amiga

Embora nós nos atermos à funcionalidade básica nesse capítulo, em meus projetos pessoais vou além e permito a existência de métodos como $this->get(), $this->post(), etc. Graças à sobrecarga do PHP, isso só requer a adição de um único método, o qual você pode adicionar a app/tests/TestCase.php.

# app/tests/TestCase.php

public function __call($method, $args)
{
  if (in_array($method, ['get', 'post', 'put', 'patch', 'delete']))
  {
      return $this->call($method, $args[0]);
  }

  throw new BadMethodCallException;
}

Agora, você já pode escrever algo como $this->get('posts') e obter o mesmo resultado dos dois exemplos anteriores. Mas, como disse, vamos continuar só com as funcionalidades básicas da framework, para não complicar as coisas.

Para fazer o teste passar, só precisamos criar a rota apropriada.

<?php

# app/routes.php

Route::get('posts', function()
{
  return 'todos os posts';
});

Executar, novamente, phpunit fará com que voltemos ao estado verde.

Os Métodos Auxiliares do Laravel

Um teste que você criará bastante será o que garante que o controlador passou uma variável em particular para a visão. Por exemplo, o método index de PostsController deve passar uma variável $posts à sua visão correspondente, certo? Dessa forma, a visão pode percorrer por todos os posts e mostrá-los na página. Esse é um teste importante de se escrever!

E, se isso é tão comum, mais uma vez, não faria sentido o Laravel prover métodos auxiliares para que possamos realizar essa tarefa? Claro que faria. E, claro, o Laravel provê!

A classe Illuminate\Foundation\Testing\TestCase inclui uma série de métodos que reduzem, drasticamente, a quantidade de código necessária para executar esses métodos de testes básicas. A lista inclui:

  • assertViewHas
  • assertResponseOk
  • assertRedirectedTo
  • assertRedirectedToRoute
  • assertRedirectedToAction
  • assertSessionHas
  • assertSessionHasErrors

O exemplo a seguir faz uma requisição GET /posts e verifica se a visão recebe a variável $posts.

# app/tests/controllers/PostsControllerTest.php

public function testIndex()
{
  $this->call('GET', 'posts');

  $this->assertViewHas('posts');
}

Dica: Quando se trata de formatação dos testes, prefiro adicionar uma linha em branco entre a declaração do teste e o código que prepara o ambiente para o teste.

assertViewHas é uma espécie de "açúcar sintático" (atalho, se preferir) que inspeciona o objeto resposta - o retorno da chamada a $this->call() - e verifica se os dados associados à visão contem a variável posts.

Quando estiver inspecionando o objeto resposta, você tem duas alternativas.

  • $response->getOriginalContent(): Buscar o conteúdo original ou a View retornada. Opcionalmente, você pode acessar a propriedade original diretamente, ao invés de chamar o método getOriginalContent.
  • $response->getContent(): Buscar o resultado renderizado. Se uma instância de View é retornada de uma rota, então getContent() será igual ao HTML resultante. Isso pode ser útil em verificações em relação a DOM, como: "a visão deve conter essa string."

Assumamos que a rota posts seja da seguinte forma:

<?php

# app/routes.php

Route::get('posts', function()
{
  return View::make('posts.index');
});

Se executarmos o phpunit, ele reclamará e apontará o que devemos fazer:

1) PostsControllerTest::testIndex
Failed asserting that an array has the key 'posts'.

O phpunit apontou que não conseguiu encontrar qualquer variável chamada 'posts'. Para fazer com que o teste fique verde, temos que buscar os posts e enviá-los para a visão.


# app/routes.php

Route::get('posts', function()
{
  $posts = Post::all();

  return View::make('posts.index', ['posts', $posts]);
});

Uma coisa a se ter em mente é que, com o código que temos até agora, a única garantia que temos é que uma variável $posts é enviada para a visão. Ele não inspeciona o valor dessa variável. O método de teste assertViewHas, opcionalmente, aceita um segundo parâmetro contendo o valor esperado da variável, para, além de atestar a existência da variável, poder verificar o valor dela.


# app/tests/controllers/PostsControllerTest.php

public function testIndex()
{
  $this->call('GET', 'posts');

  $this->assertViewHas('posts', 'foo');
}

Com essa modificação no código, a menos que a variável tenha uma variável $posts com valor igual a foo, o teste falhará. Nessa situação, porém, é melhor não especificarmos um valor em si, mas declararmos que o valor será uma instância da classe Illuminate\Database\Eloquent\Collection do Laravel. Como podemos fazer isso? O PHPUnit nos fornece um método útil, chamado assertInstanceOf.


# app/tests/controllers/PostsControllerTest.php

public function testIndex()
{
  $response = $this->call('GET', 'posts');

  $this->assertViewHas('posts');

  // getData() retorna todas as variáveis associadas à resposta.
  $posts = $response->original->getData()['posts'];

  $this->assertInstanceOf('Illuminate\Database\Eloquent\Collection', $posts);
}

Com essa modificação, declaramos que o controlador deve enviar a variável $posts - uma instância da classe Illuminate\Database\Eloquent\Collection - para a visão. Excelente.

Simulando a Base de Dados

Há um problema bastante evidente com os nossos testes, até agora. Você percebeu?

Para cada teste, uma consulta SQL tem sido executada e acessado a base de dados. Embora isso seja útil em alguns tipos de testes (testes de aceitação e integração), para testes básicos dos controladores, isso só servirá para diminuir a performance.

Eu já devo ter encravado isso no seu cérebro de tanto falar (nota: esse texto faz parte do livro Laravel Testing Decoded, do próprio Jeffrey Way). Não estamos interessados em testar as capacidades do Eloquent em buscar os registros da base de dados. Ele tem seus próprios testes. Taylor (Otwell, criador da framework) sabe que ele funciona! Não percamos tempo nem poder de processamento repetindo todos esses testes.

Ao invés disso, é melhor simular a base de dados e verificar se os métodos apropriados são chamados com seus respectivos argumentos necessários. Ou, em outras palavras, nós queremos garantir que a chamada Post::all() nunca seja disparada de verdade nem que acesse a base de dados. Nós sabemos que ela funciona, logo não precisa de testes.

Essa seção dependerá bastante da biblioteca Mockery. Por favor, revise esse capítulo do meu livro, se você ainda não a conhece.

Refatoração Necessária

Infelizmente, até agora, nós estruturamos nosso código de uma forma que, virtualmente, é impossível de testar.

# app/routes.php

Route::get('posts', function()
{
  // Oh, não! Não podemos testar isso!!
  $posts = Post::all();

  return View::make('posts.index')
      ->with('posts', $posts);
});

É, exatamente, por isso que é considerado má prática inserir chamadas ao Eloquent em seus controladores. Não confunda com as facade do Laravel, que são testáveis e podem ser substituídas por simulacros (Queue::shouldReceive()), com seus modelos Eloquent. A solução é injetar a camada da base de dados no controlador através do método construtor. Isso requer refatoração.

Atenção: Manter a lógica de um projeto dentro das rotas é útil quando ele é pequeno ou quando se quer criar uma API, mas isso torna a testabilidade do projeto incrivelmente difícil. Para aplicações de tamanhos consideráveis, use controladores.

Registremos um novo recurso, substituindo a rota posts com:

# app/routes.php

Route::resource('posts', 'PostsController');

...e criemos o controlador inteligente utilizando o Artisan.

$ php artisan controller:make PostsController
Controller created successfully!

Agora, ao invés de referenciar o modelo Post diretamente, poderemos injetá-lo através do construtor do controlador. Eis um exemplo condensado, omitindo todos os métodos restful, exceto aquele que estamos interessados em testar.

<?php

# app/controllers/PostsController.php

class PostsController extends BaseController {

protected $post;

public function __construct(Post $post)
{
    $this->post = $post;
}

public function index()
{
    $posts = $this->post->all();

    return View::make('posts.index')
        ->with('posts', $posts);
}

}

Por favor, lembre-se que é melhor induzir o tipo de uma interface ao invés de referenciar diretamente um modelo do Eloquent. Mas, cada coisa a seu tempo, não é?

Essa é uma maneira muito melhor de estruturar seu código. Uma vez o modelo sendo injetado, como estamos fazendo agora, temos a capacidade de substituí-lo por uma versão simulada para testes. Veja um exemplo de como fazer isso:

<?php

# app/tests/controllers/PostsControllerTest.php

class PostsControllerTest extends TestCase {

public function __construct()
{
    // Nós não temos qualquer interesse em testar o Eloquent
    $this->mock = Mockery::mock('Eloquent', 'Post');
}

public function tearDown()
{
    Mockery::close();
}

public function testIndex()
{
    $this->mock
         ->shouldReceive('all')
         ->once()
         ->andReturn('foo');

    $this->app->instance('Post', $this->mock);

    $this->call('GET', 'posts');

    $this->assertViewHas('posts');
}

}

O principal benefício nessa reestruturação é que, agora, a base de dados nunca mais será acessada desnecessariamente. Ao invés disso, ao usar a Mockery, simplesmente, verificamos se o método all foi chamado no modelo.

$this->mock
  ->shouldReceive('all')
  ->once();

Infelizmente, se você prefere não codificar usando interfaces e preferir injetar, diretamente, o modelo Post no seu controlador, é preciso usar algumas artimanhas para lidar com o uso de métodos estáticos do Eloquent, os quais podem chocar com a Mockery. É por isso que criamos versões falsas, tanto da classe/modelo Post quanto da classe Eloquent no construtor de nossos testes, antes que as versões verdadeiras sejam carregadas. Dessa forma, temos caminho limpo para declarar qualquer expectativa. O lado ruim, claro, é que não podemos lançar mão de qualquer método existente, através dos métodos da Mockery, como o makePartial().

O Recipente de Inversão de Controle

O recipiente de Inversão de Controle (IoC Container) do Laravel facilita o processo de injeção de dependencias em suas classes. Cada vez que um controlador é requisitado, ele é resolvido direto desse recipiente. Dessa forma, quando nós precisarmos declarar que uma versão simulada de Post dever ser usada nos testes, nós só precisamos prover ao Laravel a instância de Post que deverá ser usada.

$this->app->instance('Post', $this->mock);

Imagine esse código como, "Ei, Laravel, quando precisar de uma instância de Post, eu quero que você use essa minha versão simulada." Uma vez que a app estende a classe Container, temos acesso a todos os métodos do recipiente de inversão de controle, diretamente dela.

Durante a instanciação do controlador, o Laravel faz uso das capacidade reflexivas do PHP para ler a indução de tipo usada e injetar a dependência correta para você. É isso. Você não precisa escrever uma linha de código sequer para que isso aconteça. Tudo é automatizado!

Redirecionamentos

Outro teste que você criará bastante é aquele que garante que o usuário é redirecionado para a localização correta, como quando depois de adicionar um novo post. Como podemos fazer isso?

# app/tests/controllers/PostsControllerTest.php

public function testStore()
{
  $this->mock
       ->shouldReceive('create')
       ->once();

  $this->app->instance('Post', $this->mock);

  $this->call('POST', 'posts');

  $this->assertRedirectedToRoute('posts.index');

}

Assumindo que estamos seguindo o padrão restful, para adicionar um novo post, nós devemos fazer uma requisição POST para a coleção ou para posts (não confunda o método de requisição POST com o nome do recurso, que, nesse caso, são iguais).

$this->call('POST', 'posts');

Então, só precisamos usar outro método auxiliar do Laravel, o assertRedirectedToRoute.

Dica: Quando um recurso é registrado no Laravel (usando Route::resource()), a framework registrará, automaticamente, as rotas nomeadas necessárias. Execute php artisan routes se você, alguma vez, esquecer quais os nomes dessas rotas.

Talvez você também queira garantir que a variável superglobal $_POST é enviada para o método create. Embora não estejamos enviando um formulário de verdade, ainda assim podemos permitir isso, através do método Input::replace(), que nos permite preencher esse array. Segue o teste modificado, que usa o método with() da Mockery para verificar os argumentos passados para o método referenciado por shouldReceive.

# app/tests/controllers/PostsControllerTest.php

public function testStore()
{
  Input::replace($input = ['title' => 'My Title']);</p>

  $this->mock
       ->shouldReceive('create')
       ->once()
       ->with($input);

  $this->app->instance('Post', $this->mock);

  $this->call('POST', 'posts');

  $this->assertRedirectedToRoute('posts.index');
}

Caminhos

Algo que não consideramos nesse teste foi a validação. Devemos ter dois caminhos diferentes, como resultado do método store, dependendo se a validação de dados passa ou não:

  1. Redirecione de volta para o formulário de "criação de post" e mostre os erros de validação.
  2. Redirecione para a coleção ou para a rota nomeada posts.index.

As melhores práticas indicam que cada teste deve representar somente um caminho em seu código.

O primeiro caminho será para erro na validação.

# app/tests/controllers/PostsControllerTest.php

public function testStoreFails()
{
  // Prepara para a validação falhar
  Input::replace(['title' => '']);

  $this->app->instance('Post', $this->mock);

  $this->call('POST', 'posts');

  // Erro na validação deve recarregar o formulário de criação de post
  $this->assertRedirectedToRoute('posts.create');

  // Os erros devem ser enviados para a visão
  $this->assertSessionHasErrors(['title']);
}

O código acima declara, explicitamente, quais erros devem existir. Alternativamente, você pode omitir o argumento de assertSessionHasErrors, e, nesse caso, ele verificará se uma mensagem de erro foi enviada junto (traduzindo, se seu redirecionamento com Redirection inclui a chamada ao método withErrors($errors)).

Agora, o teste que lida com validação correta.

# app/tests/controllers/PostsControllerTest.php

public function testStoreSuccess()
{
  // Prepara para a validação ocorrer corretamente
  Input::replace(['title' => 'Foo Title']);</p>

  $this->mock
       ->shouldReceive('create')
       ->once();

  $this->app->instance('Post', $this->mock);

  $this->call('POST', 'posts');

  // Deve redirecionar para a coleção com uma mensagem de sucesso
  $this->assertRedirectedToRoute('posts.index', ['flash']);
}

O código produção de um possível aplicativo que valide esses dois testes seria mais ou menos assim:

# app/controllers/PostsController.php

public function store()
{
  $input = Input::all();

  // Nós executaremos a validação no controlador por conveniência
  // Você deveria exportar isso para um modelo ou serviço
  $v = Validator::make($input, ['title' => 'required']);

  if ($v->fails())
  {
      return Redirect::route('posts.create')
          ->withInput()
          ->withErrors($v->messages());
  }

  $this->post->create($input);

  return Redirect::route('posts.index')
      ->with('flash', 'Seu post foi criado');
}

Você percebeu como o Validator foi colocado dentro do controlador? Geralmente, recomendamos que você abstraia esse trecho em um serviço. Dessa forma, você pode testar a validação de forma isolada, longe dos controladores e rotas. No entanto, deixemos as coisas como estão, para facilitar. Algo que você tem de ter em mente é que não estamos simulando a classe Validator, embora você, certamente, possa faze-lo. Como essa classe é uma facade, ela pode, facilmente, ser substituida por uma versão simulada, através do método shouldReceive da classe Facade, sem nos preocuparmos em injetar uma instância através do construtor. Vitória!

# app/controllers/PostsController.php

Validator::shouldReceive('make')
  ->once()
  ->andReturn(Mockery::mock(['fails' => 'true']));

De tempos em tempos, você descobrirá que um método que precisa ser simulado deve retornar um objeto: ele próprio. Felizmente, com Mockery, isso é muito fácil: só precisamos criar uma simulação anônima e passar um array, contendo o nome do método e o valor de resposta, respectivamente. Dessa forma:

Mockery::mock(['fails' => 'true'])

Isso preparará um objeto com um método fails() que retornatrue.

Respositórios

Para permitir flexibilidade total, ao invés de criarmos um link direto entre o controlador e um ORM, como o Eloquent, é melhor codificarmos usando interfaces. A vantagem em seguir essa abordagem é que, se precisar substituir o Eloquent por, digamos, Mongo ou Redis, poderá fazê-lo através da alteração de uma única linha de código. E o que é melhor: o controlador não precisa ser alterado.

Repositórios representam a camada de acesso aos dados da sua aplicação.

Como será que é uma interface para administrar a camada de base de dados de um Post? O código a seguir deve dar uma ideia.

<?php

# app/repositories/PostRepositoryInterface.php

interface PostRepositoryInterface {

  public function all();

  public function find($id);

  public function create($input);

}

Obviamente, podemos adicionar outras coisas, mas nós adicionamos o mínimo necessário para nossa demonstração: all, find e create. Perceba que as interfaces de repositórios estão salvas no diretório app/repositories. Como esse diretório não é carregado automaticamente, precisamos atualizar nosso arquivo composer.json para que a aplicação possa referenciá-lo.

// composer.json

"autoload": {
"classmap": [
  // ....
  "app/repositories"
]
}

Quando uma nova classe for adicionada a esse diretório, não esqueça de executar composer dump-autoload -o. O parâmetro -o (optimize) é opcional, mas sempre deve ser utilizado, uma vez que é uma boa prática.

Se você tentar injetar essa interface em seu controlador, o Laravel reclamará com você, lançando uma exceção. Vá em frente, tente por conta própria e veja. Eis uma versão modificada do PostController atualizada para injetar a interface ao invés de um modelo Eloquent do tipo Post.

<?php

# app/controllers/PostsController.php

use Repositories\PostRepositoryInterface as Post;

class PostsController extends BaseController {

  protected $post;

  public function __construct(Post $post)
  {
      $this->post = $post;
  }

  public function index()
  {
      $posts = $this->post->all();

      return View::make('posts.index', ['posts' => $posts]);
  }

}

Se você executar o servidor e visualizar o resultado, se deparará com a temida (mas, bonita) página de erro "Whoops", informando que "PostRepositoryInterface is not instantiable." (em português: "PostRepositoryInterface não é instanciável").

Not Instantiable

Se você parar para analisar, ficará claro o porque da framework reclamar. Laravel é inteligente, mas não lê mentes. Ela precisa saber qual a implementação da interface ela precisa usar em seu controlador.

Por hora, adicionaremos essa ligação no arquivo app/routes.php. Futuramente, lançaremos mão dos provedores de serviços para guardar esse tipo de código.

# app/routes.php

App::bind(
  'Repositories\PostRepositoryInterface',
  'Repositories\EloquentPostRepository'
);

Essa chamada de método pode ser verbalizada como: "Laravel, querido, quando precisar de uma instância de PostRepositoryInterface, quero que use a EloquentPostRepository."

app/repositories/EloquentPostRepository será um invólucro usando o Eloquent que implementará a PostRepositoryInterface. Dessa forma, não estamos restringindo a API (ou qualquer outra implementação) ao uso da versão da Eloquent. Podemos nomear os métodos como quisermos.

<?php namespace Repositories;

# app/repositories/EloquentPostRepository.php

use Repositories\PostRepositoryInterface;
use Post;

class EloquentPostRepository implements PostRepositoryInterface {

public function all()
{
    return Post::all();
}

public function find($id)
{
    return Post::find($id);
}

public function create($input)
{
    return Post::create($input);
}

}

Alguns argumentam que o modelo Post deveria ser injetado nessa implementação para os propósitos dos testes. Se você concordar com isso, simplesmente injete-o através do construtor, como normalmente.

E é isso tudo que precisa ser feito! Atualize seu navegador e veja como tudo voltou ao normal. A diferença é que, agora, sua aplicação está muito melhor estruturada e os controladores não estão mais ligados ao Eloquent.

Imaginemos que, daqui alguns meses, seu chefe diga para trocar sua implementação com Eloquent por uma que implemente com Redis. Bem, já que você estruturou sua aplicação de uma maneira à prova de mudanças, você só precisa criar a nova implementação app/repositories/RedisPostRepository:

<?php namespace Repositories;

# app/repositories/RedisPostRepository.php

use Repositories\PostRepositoryInterface;

class RedisPostRepository implements PostRepositoryInterface {

public function all()
{
    // Retorne tudo usando Redis
}

public function find($id)
{
    // Retorne um item usando Redis
}

public function create($input)
{
    // Retorne a resposta do método crete usando Redis
}

}

E atualizar a ligação:

# app/routes.php

App::bind(
  'Repositories\PostRepositoryInterface',
  'Repositories\RedisPostRepository'
);

Instantanemanete, você estará usando Redis em seu controlador. Percebeu como o controlador app/controllers/PostsController.php não foi modificado? Aí que está a beleza da coisa toda!

Estrutura

Até agora, nessa lição, não temos estruturado muito bem nosso projeto. Ligações de injeção de dependência no arquivo routes.php? Todos os respositórios agrupados em um único diretório? Claro, isso pode funcionar no começo, mas, rapidamente, será perceptível que isso não é escalável.

Na parte final desse artigo, nós tornaremos nosso código compatível com PSR, e lançaremos mão de provedores de serviço para registrar quaisquer ligações necessárias em nossa aplicação.

PSR-0 define o requerimentos obrigatórios para que haja interoperabilidade de autocarregamento de arquivos.

Um carregador PSR-0 pode ser registrado com o Composer, através do objeto psr-0.

// composer.json

"autoload": {
  "psr-0": {
      "Way": "app/lib/"
  }
}

A sintaxe é meio confusa no início. Para mim, com certeza, foi. Uma maneira simples de entender a linha "Way": "app/lib/" é pensar mais ou menos assim: "O diretório raiz para o namespace Way está localizado em app/lib". Claro, substitua meu sobrenome pelo nome do seu projeto. A esturtura de diretórios que combinará com o que indicamos, será:

  • app/
    • lib/
    • Way/

Assim, ao invés de agrupar todos os repositórios em um único diretório repositories, uma abordagem mais elegante seria categorizá-los em vários diretórios. Dessa forma:

  • app/
    • lib/
    • Way/
      • Storage/
      • Post/
        • PostRepositoryInterface.php
        • EloquentPostRepository.php

É vital que adiramos a essa nomenclatura e convenção de diretórios, caso queiramos que a função de autocarregamento funcione como esperado. A única coisa faltando é atualizar o namespace de PostRepositoryInterface e EloquentPostRepository.

<?php namespace Way\Storage\Post;

# app/lib/Way/Storage/Post/PostRepositoryInterface.php

interface PostRepositoryInterface {

  public function all();

  public function find($id);

  public function create($input);

}

E para a implementação:

<?php namespace Way\Storage\Post;

# app/lib/Way/Storage/Post/EloquentPostRepository.php

use Post;

class EloquentPostRepository implements PostRepositoryInterface {

  public function all()
  {
      return Post::all();
  }

  public function find($id)
  {
      return Post::find($id);
  }

  public function create($input)
  {
      return Post::create($input);
  }

}

E é isso. Muito mais claro. Mas estão faltando as ligações, não? O arquivo de rotas pode até ser um lugar conveniente para experimentar as coisas, mas não faz sentido manter essas ligações permanentemente por lá. Ao invés disso, usaremos provedores de serviços.

Provedores de serviço, nada mais são que classes de inicialização que podem ser usadas para fazer qualquer coisa que quisermos: registrar uma ligação, criar um gancho para um evento, importar um arquivo de rotas, etc.

O método register() de um provedor de serviço será chamado automaticamente pelo Laravel:

<?php namespace Way\Storage;

# app/lib/Way/Storage/StorageServiceProvider.php

use Illuminate\Support\ServiceProvider;

class StorageServiceProvider extends ServiceProvider {

  // Chamado, automaticamente, pelo Laravel
  public function register()
  {
      $this->app->bind(
          'Way\Storage\Post\PostRepositoryInterface',
          'Way\Storage\Post\EloquentPostRepository'
      );
  }

}

Para fazer com que o Laravel saiba desse arquivo, precisamos incluí-lo em outro arquivo, o app/config/app.php, na array providers.

# app/config/app.php

'providers' => array(
  'Illuminate\Foundation\Providers\ArtisanServiceProvider',
  'Illuminate\Auth\AuthServiceProvider',
  // ...
  'Way\Storage\StorageServiceProvider'
)

Ótimo, agora temos um arquivo dedicado para registrarmos novas ligações.

Atualizando os Testes

Com a nossa estrutura, ao invés de simular o modelo Eloquent, poderemos simular a PostRepositoryInterface. Abaixo segue como ficará um teste com essa nova estrutura:

# app/tests/controllers/PostsControllerTest.php

public function testIndex()
{
  $mock = Mockery::mock('Way\Storage\Post\PostRepositoryInterface');
  $mock->shouldReceive('all')->once();

  $this->app->instance('Way\Storage\Post\PostRepositoryInterface', $mock);

  $this->call('GET', 'posts');

  $this->assertViewHas('posts');
}

Contudo, podemos aprimorar ainda mais. É um tanto claro que todo método de PostsControllerTest precisará de uma versão simulada do repositório. Assim, é melhor extrairmos essa preparação para seu próprio método, dessa forma:

# app/tests/controllers/PostsControllerTest.php

public function setUp()
{
  parent::setUp();

  $this->mock('Way\Storage\Post\PostRepositoryInterface');
}

public function mock($class)
{
  $mock = Mockery::mock($class);

  $this->app->instance($class, $mock);

  return $mock;
}

public function testIndex()
{
  $this->mock->shouldReceive('all')->once();

  $this->call('GET', 'posts');

  $this->assertViewHas('posts');
}

Nada mal, hein?

Agora, se você quer seguir a moda e adicionar testes ao seu código de produção, você pode, até mesmo, criar suas simulações dentro de um modelo do Eloquent! Isso permitirá fazer algo como isso:

Post::shouldReceive('all')->once();

Por baixo dos panos, isso simulará a PostRepositoryInterface e atualizará a ligação da inversão de controle. Você não conseguirá ser mais legível que isso!

Para usar essa sintaxe, você só precisa atualizar o modelo Post, ou melhor, o modelo BaseModel que todos os modelos Eloquent estendem. Eis um exemplo:

<?php

# app/models/Post.php

class Post extends Eloquent {

  public static function shouldReceive()
  {
      $class = get_called_class();
      $repo = "Way\\Storage\\{$class}\\{$class}RepositoryInterface";
      $mock = Mockery::mock($repo);

      App::instance($repo, $mock);

      return call_user_func_array([$mock, 'shouldReceive'], func_get_args());
  }

}

Se você consegue administrar a batalha interna de "adicionar ou não adicionar lógica de testes em código de produção", você perceberá que ela permite testes muito mais legíveis.

<?php

# app/tests/controllers/PostsControllerTest.php

class PostsControllerTest extends TestCase {

  public function tearDown()
  {
      Mockery::close();
  }

  public function testIndex()
  {
      Post::shouldReceive('all')->once();

      $this->call('GET', 'posts');

      $this->assertViewHas('posts');
  }

  public function testStoreFails()
  {
      Input::replace($input = ['title' => '']);

      $this->call('POST', 'posts');

      $this->assertRedirectedToRoute('posts.create');
      $this->assertSessionHasErrors();
  }

  public function testStoreSuccess()
  {
      Input::replace($input = ['title' => 'Foo Title']);

      Post::shouldReceive('create')->once();

      $this->call('POST', 'posts');

      $this->assertRedirectedToRoute('posts.index', ['flash']);
  }

}

Isso está bom, não está? Espero que esse artigo não tenha sobrecarregado demais você. A chave é aprender a organizar seus repositórios de tal maneira que facilite a simulação e injeção delas em seus controladores. Como resultado desse esforço, seus testes serão ultra rápidos!

Esse artigo é um extrato do livro Laravel Testing Decoded.

Advertisement