Advertisement
  1. Code
  2. PHP

Tudo sobre Mocking com PHPUnit

Scroll to top
Read Time: 23 min
This post is part of a series called Test-Driven PHP.
Deciphering Testing Jargon
Hands-On Unit Testing With PHPUnit

() translation by (you can also view the original English article)

Existem dois estilos de testes: "caixa preta" e "caixa branca". O estilo de teste caixa preta foca-se no estado do objeto; enquanto o estilo caixa branca foca-se no comportamento. Os dois estilos se complementam e podem ser combinados para testar todo o código. Os mocks nos permitem testar o comportamento, e esse tutorial combina o conceito de mocking (utilização de mocks) com TDD para construir uma classe de exemplo que usa diversos outros componentes para atingir seu objetivo.


Passo 1: Introdução ao Teste de Comportamento

Objetos são entidades que enviam mensagens umas para as outras. Cada objeto reconhece um conjunto de mensagens para as quais ele responde. Esses são os métodos públicos em um objeto. Métodos privados são exatamente o oposto. Eles são completamente internos ao objeto e não podem comunicar com nada fora do objeto. Se métodos públicos são análogos à mensagens, então métodos privados são similares à pensamentos.

O total de todos os métodos, públicos e privados, acessíveis através de métodos públicos, representam o comportamento de um objeto. Por exemplo, dizer a um objeto para mover faz com que aquele objeto não só interaja com seus métodos internos como também com outros objetos. Do ponto de vista do usuário, o objeto tem apenas um simples comportamento: ele se move.

Do ponto de vista do programador, no entanto, o objeto tem que realizar várias pequenas coisas para conseguir o movimento.

Por exemplo, imagine que nosso objeto é um carro. Para que ele se mova, é necessário ter o motor ligado, estar na primeira marcha (ou marcha ré), e as rodas precisam rodar. Este é um comportamento que nós precisamos testar e se basear para desenhar e escrever nosso código de produção.


Passo 2: Carro de Controle Remoto

Nossas classes testadas nunca usam esses objetos burros realmente.

Vamos imaginar que estamos construindo um programa para controlar remotamente um carro de brinquedo. Todos os comandos para a nossa classe vem do controle remoto. Nós temos que criar uma classe que entenda o que o controle remoto envia e demandar este comando para o carro.

Este será uma aplicação exercício, e assumimos que as demais classes que controlam as várias partes do carro já estão escritas. Nós sabemos exatamente a assinatura de todos essas classes, mas infelizmente, o fabricante do carro não pode nos enviar um protótimo, nem mesmo o código fonte. Tudo o que sabemos são os nomes das classes, os métodos que eles tem, e qual comportamento cada método encapsula. Os valores de retorno também são especificados.


Passo 3: Esquema da Aplicação

Eis o esquema completo da aplicação. Neste ponto não há explicação; simplesmente guarde na memória para futura referencia.


Passo 4: Dubles de Teste

Um test Stub é um objeto para controlar a entrada indireta do código testado.

Mocking é um estilo de teste que requer seu próprio conjunto de ferramentas, um conjunto de objetos especiais, que representam diferentes níveis de simulação do comportamento do objeto. São eles:

  • Objetos dummy (burros)
  • testes stubs 
  • testes spies
  • testes mocks
  • testes fakes

Cada um desses objetos tem seus respectivos escopo e comportamento especial. No PHPUnit, eles são criados com o método $this->getMock() A diferença é como e por quais razões os objetos são usados.

Para melhor entender esses objetos, Eu vou implementar o "Controle do Carro de brinquedo" passo a passo usando os tipos de objetos, em ordem, como foram listados. Cada objeto na lista é mais complexo que o seu antecessor. isso leva a uma implementação que é radicalmente diferente daquela do mundo real. Além disso, sendo uma aplicação imaginária, vou usar alguns cenários que podem não ser viáveis em um carro de brinquedo real. Mas Ei, vamos imaginar que nós precisamos deles para entender o quadro geral.


Step 5: Objetos Dummy

Objetos dummy são objetos dos quais dependem o System Under Test(SUT), mas eles nunca são usados realmente. Um objeto dummy pode ser um argumento passado para outro objeto ou pode ser retornado por um segundo objeto e então passado para um terceiro objeto. O ponto é, nossa classe testada realmente nunca usa esses objetos dummy. Ao mesmo tempo, o objeto deve assemelhar-se ao objeto real; de outro modo, o receptor pode recusá-lo.

O melhor jeito para exemplificar isso é imaginar um cenário; tal esquema encontra-se abaixo:

Dummy object in context schemaDummy object in context schemaDummy object in context schema

O objeto laranja é o RemoteControlTranslator. O meu propósito é receber sinais do controle remoto e traduzí-los em mensagens para nossas classes. Em algum ponto, o usuário irá realizar uma ação "Pronto para partida" no controle remoto. O tradutor irá receber a mensagem e criar as classes necessárias para fazer com que o carro esteja ponto para partida.

O fabricante disse que "Pronto para partida" significa que o motor está ligado, a marcha está em ponto porto, e as luzes estão ligadas ou desligadas conforme requisição do usuario.

Isso significa que o usuário pode definir previamente o estado das luzes antes que esteja pronto para partida, e ele vai ligar ou desligar baseado neste valor predefinido na ativação. RemoteControlTranslator então envia toda informação necessária para o método getReadyToGo($engine, $gearbox, $electronics, $lights) da classe CarControl. Eu sei que isso esta muito longe de um desenho perfeito e viola alguns princípios e padrões, mas está muito bom para este exemplo.

Comecamos nosso projeto com essa estrutura de arquivos inicial.

Initial file structure

Lembre-se, todas as classes na pasta CarInterface são de providencia do fabricante do carro; nós não sabemos suas implementações. Tudo o que sabemos são as assinaturas das classes, mas não nos importamos com isso neste ponto.

Nosso principal objetivo é implementar a classe CarController. Para realizar o teste dessa classe, nós precisamos imaginar como queremos usá-la. Em outras palavras, nós vamos nos colocar no lucar da classe RemoteControlTranslator e/ou qualquer outra classe futura que possa usar CarController. Vamos começar criando um case para nossa classe.

1
class CarControllerTest extends PHPUnit_Framework_TestCase {
2
}

Então adicionamos um método de teste.

1
  function testItCanGetReadyTheCar() {
2
	}

Agora pensaremos sobre o que é necessário para passar para o método getReadyToGo(): um motor(engine), uma marcha (gearbox), um controle elétrico (eletronics controller), e informações sobre a luz (light). Para fins desse exemplo, iremos mockar apenas as luzes.

1
require_once '../CarController.php';
2
include '../autoloadCarInterfaces.php';
3
4
class CarControllerTest extends PHPUnit_Framework_TestCase {
5
6
	function testItCanGetReadyTheCar() {
7
		$carController = new CarController();
8
9
		$engine = new Engine();
10
		$gearbox = new Gearbox();
11
		$electornics = new Electronics();
12
13
		$dummyLights = $this->getMock('Lights');
14
15
		$this->assertTrue($carController->getReadyToGo($engine, $gearbox, $electornics, $dummyLights));
16
	}
17
18
}

Obviamente o teste irá falhar:

1
PHP Fatal error:  Call to undefined method CarController::getReadyToGo()

A despeito da falha, o teste nós dá um ponto de partida para nossa implementação de CarController. Eu inclui um arquivo, chamado autoloadCarInterfaces.php, que não estava na lista inicial. Percebi que eu precisava de algo para fazer o load das classes, e eu escrevi uma solução bem básica. Nós podemos sempre reescrevê-la quando as classes reais forem disponibilizadas, mas isso é uma historia totalmente diferente. Por hora, nós ficaremos com a solução fácil.

1
foreach (scandir(dirname(__FILE__) . '/CarInterface') as $filename) {
2
	$path = dirname(__FILE__) . '/CarInterface/' . $filename;
3
4
	if (is_file($path)) {
5
		require_once $path;
6
	}
7
}

Eu presumo que essa classe loader é obvia para todos; então, vamos discutir o código do teste.

Primeiramente, nós criamos uma instancia de CarController, a classe que nós queremos testar. Em seguida, nós criamos instancias para todas as outras classes com as quais nos importamos: engine, gerbox, e eletronics.

Nós então criamos um objeto dummy, Lights, atravez do método getMock() do PHPUnit e passamos o nome da classe Lights. Isso retorna uma instancia de Lights, mas todos os métodos retornam null -- um objeto dummy. Esse objeto dummy não pode fazer nada, mas dá ao nosso código a interface necessária para trabalhar com objetos Light.

É muito importante notar que $dummyLights é um objeto Lights, e qualquer usuário que espere um objeto Light pode usar o objeto dummy sem conhecimento que ele não é um objeto Light real.

Para evitar confusão, eu recomendo especificar os tipos de parâmetros quando definir uma função. Isso força o PHP runtime a checar os argumentos passados para a função. Sem especificar o tipo de dado, você pode passar qualquer objeto para qualquer parâmetro, o que pode resultar em uma falha no seu código. Com isto em mente, vamos examinar a classe Eletronics:

1
require_once 'Lights.php';
2
3
class Electronics {
4
5
	function turnOn(Lights $lights) {}
6
7
}

Vamos implementar um teste:

1
class CarController {
2
3
	function getReadyToGo(Engine $engine, Gearbox $gearbox, Electronics $electronics, Lights $lights) {
4
		$engine->start();
5
		$gearbox->shift('N');
6
7
		$electronics->turnOn($lights);
8
9
		return true;
10
	}
11
12
}

Como você pode ver, a função getReadyToGo() usa o objeto $lights apenas com o propósito de envia-lo para o método turnOn() do objeto $eletronics. Isto é a solução ideal para esta situação? Provavelmente não, mas você pode observar claramente como o objeto dummy, que não tem nenhuma relação com a função getReadyToGo(), é passada adiante para o método que realmente precisa dela.

Por favor, note que todas as classes contidas no diretório CarInterface proveem objetos dummy quando inicializadas.  Assuma também, para este exercício, que esperamos as classes reais que o fabricante irá prover futuramente. Não podemos confiar em sua atual falta de funcionalidade ; por isso , temos de assegurar que os nossos testes passam .


Passo 6: Status e Partida como "Stub"

Um teste Stub é um objeto para controlar a entrada indireta de dados do código testado. Mas o que é entrada indireta?  É uma fonte de informação que não pode ser diretamente especificada.

O exemplo mais comum de teste stub é quando um objeto pede a outro objeto por uma informação e então faz alguma coisa com aquele dado.

Spies (espiões), por definição, são stubs mais capazes.

O dado pode apenas ser obtido por meio de solititação a outro objeto especifico, e em muitos casos, esses objetos são usados para propósitos específicos na classe testada. Nós não queremos instanciar ( new AlgumaClasse() ) uma classe dentro de outra classe para fins de teste. Portanto, nós precisamos injetar uma instancia de uma classe que aja como AlgumaClasse sem injetar um objeto real de  AlgumaClass.

O que nós queremos é uma classe stup, o que nos leva a injeção de dependência. Injeção de dependencia (DI - Dependency Injection) é uma técnica que injeta um objeto em outro objeto, forçando-o a usar o objeto injetado. DI é comum em TDD, e é absolutamente necessário é quase todo projeto. Ele provê uma maneira simples para forçar um objeto a usar uma classe preparada para teste no lugar de uma classe real usada no ambiente de produção.

Vamos fazer com que nosso carro de brinquedo se mova.

Test stub in context schemaTest stub in context schemaTest stub in context schema

Queremos implementar um método chamado moveForward(). Este método primeiramente pede o status do motor e do combustivel para o objeto StatusPanel. Se o carro estiver pronto para partida, então o método instrui o eletronico para acelerar.

Para melhor compreender como um stub funciona, vou primeiramente escrever o código para a checagem do status e da aceleração.

1
	function goForward(Electronics $electronics) {
2
		$statusPanel = new StatusPanel();
3
		if($statusPanel->engineIsRunning() && $statusPanel->thereIsEnoughFuel())
4
			$electronics->accelerate ();
5
	}

O código é muito simples, mas nós não temos um engine ou fuel real para testar nossa implementação de goForward(). Nosso código nem mesmo entra no if porque não temos uma classe StatusPanel. Mas se nós continuarmos com o teste, uma solução lógica começa a emergir:

1
	function testItCanAccelerate() {
2
		$carController = new CarController();
3
4
		$electronics = new Electronics();
5
6
		$stubStatusPanel = $this->getMock('StatusPanel');
7
		$stubStatusPanel->expects($this->any())->method('thereIsEnoughFuel')->will($this->returnValue(TRUE));
8
		$stubStatusPanel->expects($this->any())->method('engineIsRunning')->will($this->returnValue(TRUE));
9
10
		$carController->goForward($electronics, $stubStatusPanel);
11
	}

Explicação linha a linha:

Eu amo recursão; é sempre mais fácil testar recursão do que laços.

  • criar um novo CarController
  • criar o objeto dependente Electronics
  • crear um mock para o StatusPanel
  • esperar que thereIsEnoughFuel() seja chamado zero ou mais vezes e retorne true
  • esperar que engineIsRunning() seja chamado zero ou mais vezes e retorne true
  • chamar goForward() com os objetos Electronics e StubbedStatusPanel

Este é o teste que queremos escrever, mas isso não vai funcionar com a nossa atual implementação de goForward(). Nós temos que modificá-la:

1
	function goForward(Electronics $electronics, StatusPanel $statusPanel = null) {
2
		$statusPanel = $statusPanel ? : new StatusPanel();
3
		if($statusPanel->engineIsRunning() && $statusPanel->thereIsEnoughFuel())
4
			$electronics->accelerate ();
5
	}

Nossa modificação usa injeção de dependencia adicionando um segundo parâmetro opcional do tipo StatusPanel. Nós determinamos se este parâmetro tem um valor e criamos um novo StatusPanel se $statusPanel for null. Isto assegura que o novo objeto StatusPanel seja criado em produção enquanto nos permite testar o método.

 É importante especificar o tipo do parâmetro $statusPanel. Isso assegura que apenas um objeto StatusPanel (ou um objeto que herde dessa classe) possa ser passado para o método. Mas mesmo com esta modificação, nosso teste ainda não esta completo.


Passo 7: Complete o Teste com um Mock Real

Nós temos que mockar um objeto Electronics para assegurar que nosso método do passo 6 chame accelerate(). Nós não podemos usar a classe real Electronics por diversas razões:

  • Nós não temos a classe.
  • Nós não podemos verificar seu comportamento.
  • Mesmo que podéssemos chamá-lo, nós teriamos que realizar o teste isolado.

Um mock é um objeto que é capaz de controlar tanto entradas indiretas como saidas, e possui um mecanismo de assertion automática nas espectativas e resultados. Essa definição pode soar um pouco confusa, mas é realmente simples de implementar:

1
	function testItCanAccelerate() {
2
		$carController = new CarController();
3
4
		$electronics = $this->getMock('Electronics');
5
		$electronics->expects($this->once())->method('accelerate');
6
7
		$stubStatusPanel = $this->getMock('StatusPanel');
8
		$stubStatusPanel->expects($this->any())->method('thereIsEnoughFuel')->will($this->returnValue(TRUE));
9
		$stubStatusPanel->expects($this->any())->method('engineIsRunning')->will($this->returnValue(TRUE));
10
11
		$carController->goForward($electronics, $stubStatusPanel);
12
	}

Nós simplesmente mudamos a variável $electronics. Ao invés de criar um objeto real Electronics, nós simplesmente mockamos um.

Na próxima linha, nós definimos as espectativas para a o objeto $electronics. Mais precisamente, nós esperamos que o método accelerate() seja chamado apenas uma vez ( $this->once() ). Agora o teste passa!

Sinta-se a vontade para brincar com este teste. Tente modificar o $this->once() para $this->exactly(2) e veja que mensagem legal de erro o PHPUnit devolve.

1
1) CarControllerTest::testItCanAccelerate
2
Expectation failed for method name is equal to <string:accelerate>; when invoked 2 time(s).
3
Method was expected to be called 2 times, actually called 1 times.

Passo 8: Usar um Teste Spy

Um teste spy é um objeto capaz de capturar entradas indiretas e prover uma entrada indireta quando necessário.

Entrada indireta é algo que não podemos observar diretamente. Por exemplo: quando a classe testada processa um valor e então o usa como um argumento para outro método de outro objeto. A única maneira de observar esta saída é perguntar ao objeto utilizado sobre a variável usada para acessar seu método.

Essa definição faz com que um spy seja quase um mock.

A principal diferença entre um mock e um spy é que um objeto mock possui assertions e expectations embutidas.

No caso, como nós podemos criar um teste spy usando o getMock() do PHPUnit? Não podemos (bem, nós não podemos criar um spy puro), mas nós podemos criar um mock capaz de espiar outros objetos.

Vamos implementar um sistema de freios para que o carro possa parar. Freiar é realmente simples; o controle remoto vai sentir a intensidade da freiada do usuário e enviar isso para o controlador. O controle remoto também provê um botão para "freio de emergência". Isso deve instantaneamente ativar os freios com intensidade máxima.

A intensidade do freio mede o valor variando de 0 a 100, com 0 significando nada e 100 significando intensidade máxima dos freios.  O comando "Freio de Emergencia!" vai receber as diferentes chamadas.

Test stub in context schemaTest stub in context schemaTest stub in context schema

O CarController vai enviar uma mensagem para o objeto Electronics para ativar o sistema de freios. O controle do carro também pode verificar informações com o StatusPanel sobre velocidade obtida por meio de sensores no carro. 

Implementação usando um Teste Spy Puro

Vamos primeiramente implementar um objeto spy puro, sem usar a infraestrutura de mocking do PHPUnit. Isso dará a você um melhor entendimento do conceito de teste spy. Vamos começar checando a assinatura do objeto Electronics.

1
class Electronics {
2
3
	function turnOn(Lights $lights) {}
4
	function accelerate(){}
5
	function pushBrakes($brakingPower){}
6
7
}

Nós estamos interessados no método pushBrakes(). Eu não usei brake() para evitar confusão com a palavra reservada break do PHP.

Para criar um spy real, nós temos que estendê-la de Electronics e sobrescrever o método pushBrakes(). O método sobrescrito não vai acionar os freios; Ao inves disso, ele vai apenas registrar a intensidade da freada.

1
class SpyingElectronics extends Electronics {
2
	private $brakingPower;
3
4
	function pushBrakes($brakingPower) {
5
		$this->brakingPower = $brakingPower;
6
	}
7
8
	function getBrakingPower() {
9
		return $this->brakingPower;
10
	}
11
}

O método getBrakingPower() nos dá  a habilidade de checkar a intensidade do freio em nosso teste. Este não é um método que iriamos usar em produção.

Agora nós podemos escrever um teste capaz de testar a intensidade dos freios. Seguindo principios TDD, iremos começar com um teste simplissimo e prover a implementação mais básica.

1
	function testItCanStop() {
2
		$halfBrakingPower = 50;
3
		$electronicsSpy = new SpyingElectronics();
4
5
		$carController = new CarController();
6
		$carController->pushBrakes($halfBrakingPower, $electronicsSpy);
7
8
		$this->assertEquals($halfBrakingPower, $electronicsSpy->getBrakingPower());
9
	}

O teste falha porque ainda não temos um método pushBrakes() em CarController. Vamos corrigir isso e escrever um:

1
	function pushBrakes($brakingPower, Electronics $electronics) {
2
		$electronics->pushBrakes($brakingPower);
3
	}

Agora o teste passa. efetivamente testando o método pushBrakes().

Nós podemos também usar os spy em chamadas de métodos. Testar a classe StatusPanel é nosso próximo passo lógico. Isso fornece ao usuário diferentes pedaços de informações sobre a carro de controle remoto. Vamos escrever um teste que checa se o objeto StatusPanel é consultado a respeito da velocidade do carro. Vamos criar um spy para isso:

1
class SpyingStatusPanel extends StatusPanel {
2
	private $speedWasRequested = false;
3
4
	function getSpeed() {
5
		$this->speedWasRequested = true;
6
	}
7
8
	function speedWasRequested() {
9
		return $this->speedWasRequested;
10
	}
11
}

Agora, nós modificamos nosso teste para usar o spy:

1
	function testItCanStop() {
2
		$halfBrakingPower = 50;
3
		$electronicsSpy = new SpyingElectronics();
4
		$statusPanelSpy = new SpyingStatusPanel();
5
6
		$carController = new CarController();
7
		$carController->pushBrakes($halfBrakingPower, $electronicsSpy, $statusPanelSpy);
8
9
		$this->assertEquals($halfBrakingPower, $electronicsSpy->getBrakingPower());
10
		$this->assertTrue($statusPanelSpy->speedWasRequested());
11
	}

Note que eu não escrevi um teste separado.

A recomendação de "um assertion por teste" é algo a ser seguido, mas quando seu teste descreve uma ação que requer muitos passos ou estados, usar mais de um assertion no mesmo teste é aceitavel.

Além disso, isso reune suas assertions sobre um único conceito em um lugar só. Isso ajuda a eliminar código duplicado por não requerer repetir as mesmas configurações para seu SUT.

E agora, a implementação:

1
	function pushBrakes($brakingPower, Electronics $electronics, StatusPanel $statusPanel = null) {
2
		$statusPanel = $statusPanel ? : new StatusPanel();
3
		$electronics->pushBrakes($brakingPower);
4
		$statusPanel->getSpeed();
5
	}

Existe apenas uma pequenissima coisa me aborrecendo: o nome deste teste é testItCanStop(). Isto claramente implica que nós acionamos o freio até o carro parar completamente. No entanto, nosso método se chama pushBrakes (acionaFreios), o que não é totalmente verdade. Hora de refatorar:

1
	function stop($brakingPower, Electronics $electronics, StatusPanel $statusPanel = null) {
2
		$statusPanel = $statusPanel ? : new StatusPanel();
3
		$electronics->pushBrakes($brakingPower);
4
		$statusPanel->getSpeed();
5
	}

Não se esqueça de modificar a chamada do método no teste também.

1
$carController->stop($halfBrakingPower, $electronicsSpy, $statusPanelSpy);

Saida indireta é algo que não podemos observar diretamente.

Neste ponto, nós precisamos pensar sobre nosso sitema de freios e como isso funciona. Existem muitas possibilidades, mas para este exemplo, vamos assumir que o fabricante do carro especificou que a freada ocorre em intervalos discretos. Chamar o método pushBrakes() do objeto Electronics aciona o freio por uma determinado tempo discreto e então o libera. O intervalo de tempo não é importante para nós, mas vamos imaginar que seja uma fração de segundo. Em pequenos intervalos de tempo, nós temos que enviar continuamente o comando pushBrakes() até que a velocidade seja zero.

Spies (Espiões), por definição, são mais capazes que stubs, e também podem controlar entradas indiretas se necessário. Vamos fazer nosso spy StatusPanel mais capaz e oferecer algum valor para a velocidade. Eu acho que a primeira chamada deve prover um valor positivo de velocidade-- vamos dizer o valor 1. A segunda chamada irá prover o valor 0.

1
class SpyingStatusPanel extends StatusPanel {
2
3
	private $speedWasRequested = false;
4
	private $currentSpeed = 1;
5
6
	function getSpeed() {
7
		if ($this->speedWasRequested) $this->currentSpeed = 0;
8
		$this->speedWasRequested = true;
9
		return $this->currentSpeed;
10
	}
11
12
	function speedWasRequested() {
13
		return $this->speedWasRequested;
14
	}
15
16
	function spyOnSpeed() {
17
		return $this->currentSpeed;
18
	}
19
20
}

O método sobrescrito getSpeed() retorna o valor apropriado de velocidade por meio do método spyOnSpeed(). Vamos adicionar um terceiro assertion ao nosso teste:

1
	function testItCanStop() {
2
		$halfBrakingPower = 50;
3
		$electronicsSpy = new SpyingElectronics();
4
		$statusPanelSpy = new SpyingStatusPanel();
5
6
		$carController = new CarController();
7
		$carController->stop($halfBrakingPower, $electronicsSpy, $statusPanelSpy);
8
9
		$this->assertEquals($halfBrakingPower, $electronicsSpy->getBrakingPower());
10
		$this->assertTrue($statusPanelSpy->speedWasRequested());
11
		$this->assertEquals(0, $statusPanelSpy->spyOnSpeed());
12
	}

De acordo com o ultimo assert, a velocidade deve ter valor 0 depois do método stop() terminar de executar. Rodar este teste para nosso código de produção resulta em uma falha com uma mensagem crítica.

1
1) CarControllerTest::testItCanStop
2
Failed asserting that 1 matches expected 0.

Vamos adicionar nossa mensagem customizada para este assert.

1
$this->assertEquals(0, $statusPanelSpy->spyOnSpeed(),
2
	'Expected speed to be 0 (zero) after stopping but it actually was ' . $statusPanelSpy->spyOnSpeed());

Isso produz uma mensagem de falha muito mais legível.

1
1) CarControllerTest::testItCanStop
2
Expected speed to be 0 (zero) after stopping but it actually was 1
3
Failed asserting that 1 matches expected 0.

Chega de Falhas! Vamos fazer isso passar.

1
	function stop($brakingPower, Electronics $electronics, StatusPanel $statusPanel = null) {
2
		$statusPanel = $statusPanel ? : new StatusPanel();
3
		$electronics->pushBrakes($brakingPower);
4
		if ($statusPanel->getSpeed()) $this->stop($brakingPower, $electronics, $statusPanel);
5
	}

Eu amo recursão; É sempre mais fácil testar recursão do que laços. Teste mais fácil significa código mais simples, o que por sua vez significa um algoritmo melhor. Veja The Transformation Priority Premisse para saber mais.

Voltando para o framework de mock do PHPUnit

Basta de classes extras. Vamos reescrever isso usando o frameword do PHPUnit para mocks e eliminar esses spys puros. Por que?

Porque PHPUnit oferece uma sintaxe mais simples e melhor, menos código, e alguns métodos predefinidos legais.

Algumas vezes eu crio spys puros e stubs apenas quando mocká-los com getMock() poderia ser complicado. Se suas classes são tão complexas que getMock() não pode lidar com eles, então você tem problemas com a produção do seu código--não com os seus testes.

1
	function testItCanStop() {
2
		$halfBrakingPower = 50;
3
		$electronicsSpy = $this->getMock('Electronics');
4
		$electronicsSpy->expects($this->exactly(2))->method('pushBrakes')->with($halfBrakingPower);
5
		$statusPanelSpy = $this->getMock('StatusPanel');
6
		$statusPanelSpy->expects($this->at(0))->method('getSpeed')->will($this->returnValue(1));
7
		$statusPanelSpy->expects($this->at(1))->method('getSpeed')->will($this->returnValue(0));
8
9
		$carController = new CarController();
10
		$carController->stop($halfBrakingPower, $electronicsSpy, $statusPanelSpy);
11
	}

O total de todos os métodos, publicos e privados, acessíveis por métodos públicos representam o comportamento de um objeto.

Uma explicação linha a linha do código acima:

  • atribuir metade da intensidade do freio = 50
  • Criar um mock Electronics
  • esperar o método pushBrakes() ser executado exatamente duas vezes com a intensidade dos freios especificada acima.
  • criar o mock StatusPanel
  • retornar 1 na primeira chamada a getSpeed()
  • retornar 0 na segunda execução de getSpeed()
  • chamar o método testado stop() em um objeto CarController real.

Provavelmente a coisa mais interessante neste código é o método $this->at($someValue). PHPUnit conta as chamadas feitas àquele mock. A contagem acontece a nivel de mock; então, chamar múltiplos métodos em $statusPanelSpy irá incrementar a contagem. Isto pode paracer um pouco contra-intuitivo de primeira; vamos então dar uma olhada em um exemplo.

Presuma que nós queremos checar o nivel de combustível a cada chamada de stop(); O código se pareceria com isto:

1
	function stop($brakingPower, Electronics $electronics, StatusPanel $statusPanel = null) {
2
		$statusPanel = $statusPanel ? : new StatusPanel();
3
		$electronics->pushBrakes($brakingPower);
4
		$statusPanel->thereIsEnoughFuel();
5
		if ($statusPanel->getSpeed()) $this->stop($brakingPower, $electronics, $statusPanel);
6
	}

Isto irá quebrar nosso teste. Você pode estar se perguntando porquê, mas você receberá a seguinte mensagem:

1
1) CarControllerTest::testItCanStop
2
Expectation failed for method name is equal to <string:pushBrakes> when invoked 2 time(s).
3
Method was expected to be called 2 times, actually called 1 times.

está realmente obvio que pushBrakes() deveria ser chamado duas vezes. Por que então nós recebemos esta mensagem? Por causa da expectativa $this->at($sameValue). O contador é incrementado como se segue:

  • Primeira chamada a stop() -> primeira chamada a thereIsEnoughFuel() => contador interno em 0
  • Primeira chamada a stop() -> primeira chamada a getSpeed() => contador interno em 1 e retorna 0
  • Segunda chamada a stop() nunca acontece => segunda chamada a getSpeed() nunca acontece.

Cada chamada para qualquer método mockado em $statusPanelSpy incrementa o contador interno do PHPUnit.


Passo 9: Um Teste Fake

Se métodos publicos são análogos a mensagens, então métodos privados são similares a pensamentos.

Um teste fake é uma implementação simplificada de um objeto do código de produção. Isto é uma definição muito semelhante a testes stubs. Na verdade, Fakes e Stubs são muito semelhantes em seus comportamentos. Ambos são objetos imitando o comportamento de algum outro objeto real, e ambos implementam um método para controlar entradas indiretas. A diferença é que Fakes são muito mais próximos do objeto real que um objeto dummy.

Um Stub é basicamente um objeto dummy cujos métodos retornam valores predefinidos. Um Fake, no entanto, faz uma implementação completa de um objeto real de uma forma simplificada. Provavelmente o exemplo mais comum esta em InMemoryDatabase para perfeitamente simular uma classe de banco de dados real sem realmente salvar no banco de dados. Dessa forma, o teste se torna mais rápido.

Testes Fakes não devem implementar nenhum método para controlar diretamente as entradas ou retornar estados observaveis. Eles não são usados para responder; eles são usados para prover-- não observar. Os usos mais comuns para Fakes são quando um Componente de Dependencia (DOC) real ainda não está codificado, é muito lento (como um banco de dados), ou o DOC real não está disponível no ambiente de teste.


Passo 10: Conclusões

A funcionalidade mais importante do mock é controlar o DOC. Também provê uma forma excelente de controlar I/O indireto com ajuda de técnicas de injeção de dependencia.

Existe duas opiniões principais sobre mocking:

Alguns dizem que mocking é ruim...

  • Alguns dizem que mocking é ruim, e eles estão certos. Mocking faz algo sutil e feio: isso  leva muito dos testes para a implementação Sempre que possível, o teste deve ser tão independente na implementação quanto possível. Testes de caixa preta é sempre preferível a testes de caixa branca. Sempre teste estados se você pode; não mocke comportamentos. Ser anti-mockista encoraja o desenvolvimento bottom-up e design. Isto significa que pequenas partes do componente do sistema são criados primeiro e então combinados em uma estrutura harmoniosa.
  • Alguns dizem que mocking é bom, e eles estão certos. Mockar é algo sutil e bonito; ele define o comportamento. Ele nos faz pensar muito mais do ponto de vista do usuário. Mockistas geralmente usam uma abordagem top-down de implementação e design. Eles começam com a classe mais importante do sistema e escreve o primeiro teste mockando alguns outros DOCs imaginários que ainda não estão implementados. Os componentes do sistema aparecem e se desenvolvem baseados nos mocks criados em um nivel mais elevado.

Digo isto, a decisão é sua sobre que caminho pegar.

Alguns preferem ser mockistas enquanto outros preferem testes de estados. Cada abordagem tem suas vantagens e desvantagens. Um sistema baseado em mocks oferece informação comportamental extra nos testes. um sistema baseado em estados oferece mais detalhes sobre os componentes, mas também pode esconder alguns comportamentos.


Livros e referencias adicionais

Seja o primeiro a saber sobre novas traduções–siga @tutsplus_pt no Twitter!

Advertisement
Did you find this post useful?
Want a weekly email summary?
Subscribe below and we’ll send you a weekly email summary of all new Code tutorials. Never miss out on learning about the next big thing.
Advertisement
Looking for something to help kick start your next project?
Envato Market has a range of items for sale to help get you started.