Capítulo 9. Dublês de Testes

Gerard Meszaros introduz o conceito de Dublês de Testes em [Meszaros2007] desta forma:

 

Às vezes é muito difícil testar o sistema sob teste (SST - em inglês: system under test - SUT) porque isso depende de outros ambientes que não podem ser usados no ambiente de testes. Isso pode ser porque não estão disponíveis, não retornarão os resultados necessários para o teste, ou porque executá-los causaria efeitos colaterais indesejáveis. Em outros casos, nossa estratégia de testes requer que tenhamos mais controle ou visibilidade do comportamento interno do SST.

Quando estamos escrevendo um teste no qual não podemos (ou decidimos não) usar um componente dependente (depended-on component - DOC) real, podemos substitui-lo por um Dublê de Teste. O Dublê de Teste não precisa se comportar exatamente como o DOC real; apenas precisa fornecer a mesma API como o real, de forma que o SST pense que é o real!

 
 --Gerard Meszaros

O método getMockBuilder($type) fornecido pelo PHPUnit pode ser usado em um teste para gerar automaticamente um objeto que possa atuar como um dublê de teste para a classe original especificada. Esse objeto de dublê de teste pode ser usado em cada contexto onde um objeto da classe original é esperado ou requerido.

Por padrão, todos os métodos da classe original são substituídos com uma implementação simulada que apenas retorna null (sem chamar o método original). Usando o método will($this->returnValue()), por exemplo, você pode configurar essas implementações simuladas para retornar um valor quando chamadas.

Limitações

Por favor, note que os métodos final, private e static não podem ser esboçados (stubbed) ou falsificados (mocked). Eles são ignorados pela funcionalidade de dublê de teste do PHPUnit e mantêm seus comportamentos originais.

Aviso

Por favor atente para o fato de que a gestão de parâmetros foi mudada. A implementação anterior clona todos os parâmetros de objetos. Isso não permite verificar se o mesmo objeto foi passado para um método ou não. Exemplo 9.15 mostra onde a nova implementação pode ser útil. Exemplo 9.16 mostra como voltar para o comportamento anterior.

Esboços (stubs)

A prática de substituir um objeto por um dublê de teste que (opcionalmente) retorna valores de retorno configurados é chamada de delineamento. Você pode usar um esboço para "substituir um componente real do qual o SST depende de modo que o teste tenha um ponto de controle para as entradas indiretas do SST. Isso permite ao teste forçar o SST através de caminhos que não seriam executáveis de outra forma".

Exemplo 9.2 mostra como esboçar chamadas de método e configurar valores de retorno. Primeiro usamos o método getMockBuilder() que é fornecido pela classe PHPUnit_Framework_TestCase para configurar um esboço de objeto que parece com um objeto de SomeClass (Exemplo 9.1). Então usamos a Interface Fluente que o PHPUnit fornece para especificar o comportamento para o esboço. Essencialmente, isso significa que você não precisa criar vários objetos temporários e uni-los depois. Em vez disso, você encadeia chamadas de método como mostrado no exemplo. Isso leva a códigos mais legíveis e "fluentes".

Exemplo 9.1: A classe que queremos esboçar

<?php
class SomeClass
{
    public function doSomething()
    {
        // Faça algo.
    }
}
?>


Exemplo 9.2: Esboçando uma chamada de método para retornar um valor fixo

<?php
require_once 'SomeClass.php';

class StubTest extends PHPUnit_Framework_TestCase
{
    public function testStub()
    {
        // Cria um esboço para a classe AlgumaClasse.
        $stub = $this->getMockBuilder('SomeClass')
                     ->getMock();

        // Configura o esboço.
        $stub->method('doSomething')
             ->willReturn('foo');

        // Chamando $esboco->fazAlgumaCoisa() agora vai retornar 
        // 'foo'.
        $this->assertEquals('foo', $stub->doSomething());
    }
}
?>


"Atrás dos bastidores" o PHPUnit automaticamente gera uma nova classe PHP que implementa o comportamento desejado quando o método getMock() é usado.

Exemplo 9.3 mostra um exemplo de como usar a interface fluente do Mock Builder para configurar a criação do dublê de teste.

Exemplo 9.3: Usando a API Mock Builder pode ser usada para configurar a classe de dublê de teste gerada

<?php
require_once 'SomeClass.php';

class StubTest extends PHPUnit_Framework_TestCase
{
    public function testStub()
    {
        // Cria um esboço para a classe SomeClass.
        $stub = $this->getMockBuilder('SomeClass')
                     ->disableOriginalConstructor()
                     ->getMock();

        // Configura o esboço.
        $stub->method('doSomething')
             ->willReturn('foo');

        // Chamar $stub->doSomething() agora vai retornar
        // 'foo'.
        $this->assertEquals('foo', $stub->doSomething());
    }
}
?>


Aqui está uma lista de métodos fornecidos pelo Mock Builder:

  • setMethods(array $methods) pode ser chamado no objeto Mock Builder para especificar os métodos que devem ser substituídos com um dublê de teste configurável. O comportamento dos outros métodos não muda. Se você chamar setMethods(null), então nenhum dos métodos serão substituídos.

  • setConstructorArgs(array $args) pode ser chamado para fornecer um vetor de parâmetros que é passado ao construtor da classe original (que por padrão não é substituído com uma implementação falsa).

  • setMockClassName($name) pode ser usado para especificar um nome de classe para a classe de dublê de teste gerada.

  • disableOriginalConstructor() pode ser usado para desabilitar a chamada ao construtor da classe original.

  • disableOriginalClone() pode ser usado para desabilitar a chamada ao construtor do clone da classe original.

  • disableAutoload() pode ser usado para desabilitar o __autoload() durante a geração da classe de dublê de teste.

Nos exemplos até agora temos retornado valores simples usando willReturn($value). Essa sintaxe curta é o mesmo que will($this->returnValue($value)). Podemos usar variações desta sintaxe longa para alcançar mais comportamento de esboço complexo.

Às vezes você quer retornar um dos argumentos de uma chamada de método (inalterada) como o resultado de uma chamada ao método esboçado. Exemplo 9.4 mostra como você pode conseguir isso usando returnArgument() em vez de returnValue().

Exemplo 9.4: Esboçando uma chamada de método para retornar um dos argumentos

<?php
require_once 'SomeClass.php';

class StubTest extends PHPUnit_Framework_TestCase
{
    public function testReturnArgumentStub()
    {
        // Cria um esboço para a classe SomeClass.
        $stub = $this->getMockBuilder('SomeClass')
                     ->getMock();

        // Configura o esboço.
        $stub->method('doSomething')
             ->will($this->returnArgument(0));

        // $stub->doSomething('foo') retorna 'foo'.
        $this->assertEquals('foo', $stub->doSomething('foo'));

        // $stub->doSomething('bar') retorna 'bar'.
        $this->assertEquals('bar', $stub->doSomething('bar'));
    }
}
?>


Ao testar uma interface fluente, às vezes é útil fazer um método esboçado retornar uma referência ao objeto esboçado. Exemplo 9.5 mostra como você pode usar returnSelf() para conseguir isso.

Exemplo 9.5: Esboçando uma chamada de método para retornar uma referência ao objeto esboçado

<?php
require_once 'SomeClass.php';

class StubTest extends PHPUnit_Framework_TestCase
{
    public function testReturnSelf()
    {
        // Cria um esboço para a classe SomeClass.
        $stub = $this->getMockBuilder('SomeClass')
                     ->getMock();

        // Configura o esboço.
        $stub->method('doSomething')
             ->will($this->returnSelf());

        // $stub->doSomething() retorna $stub
        $this->assertSame($stub, $stub->doSomething());
    }
}
?>


Algumas vezes um método esboçado deveria retornar valores diferentes dependendo de uma lista predefinida de argumentos. Você pode usar returnValueMap() para criar um mapa que associa argumentos com valores de retorno correspondentes. Veja Exemplo 9.6 para ter um exemplo.

Exemplo 9.6: Esboçando uma chamada de método para retornar o valor de um mapa

<?php
require_once 'SomeClass.php';

class StubTest extends PHPUnit_Framework_TestCase
{
    public function testReturnValueMapStub()
    {
        // Cria um esboço para a classe SomeClass.
        $stub = $this->getMockBuilder('SomeClass')
                     ->getMock();

        // Cria um mapa de argumentos para valores retornados.
        $map = array(
          array('a', 'b', 'c', 'd'),
          array('e', 'f', 'g', 'h')
        );

        // Configura o esboço.
        $stub->method('doSomething')
             ->will($this->returnValueMap($map));

        // $stub->doSomething() retorna diferentes valores dependendo do 
        // argumento fornecido.
        $this->assertEquals('d', $stub->doSomething('a', 'b', 'c'));
        $this->assertEquals('h', $stub->doSomething('e', 'f', 'g'));
    }
}
?>


Quando a chamada ao método esboçado deve retornar um valor calculado em vez de um fixo (veja returnValue()) ou um argumento (inalterado) (veja returnArgument()), você pode usar returnCallback() para que o método esboçado retorne o resultado da função ou método callback. Veja Exemplo 9.7 para ter um exemplo.

Exemplo 9.7: Esboçando uma chamada de método para retornar um valor de um callback

<?php
require_once 'SomeClass.php';

class StubTest extends PHPUnit_Framework_TestCase
{
    public function testReturnCallbackStub()
    {
        // Cria um esboço para a classe AlgumaClasse.
        $stub = $this->getMockBuilder('SomeClass')
                     ->getMock();

        // Configura o esboço.
        $stub->method('doSomething')
             ->will($this->returnCallback('str_rot13'));

        // $stub->doSomething($argument) retorna str_rot13($argument)
        $this->assertEquals('fbzrguvat', $stub->doSomething('something'));
    }
}
?>


Uma alternativa mais simples para configurar um método callback pode ser especificar uma lista de valores de retorno desejados. Você pode fazer isso com o método onConsecutiveCalls(). Veja Exemplo 9.8 para ter um exemplo.

Exemplo 9.8: Esboçando uma chamada de método para retornar uma lista de valores na ordem especificada

<?php
require_once 'SomeClass.php';

class StubTest extends PHPUnit_Framework_TestCase
{
    public function testOnConsecutiveCallsStub()
    {
        // Cria um esboço para a classe SomeClass.
        $stub = $this->getMockBuilder('SomeClass')
                     ->getMock();

        // Configura o esboço.
        $stub->method('doSomething')
             ->will($this->onConsecutiveCalls(2, 3, 5, 7));

        // $stub->doSomething() retorna um valor diferente em cada vez
        $this->assertEquals(2, $stub->doSomething());
        $this->assertEquals(3, $stub->doSomething());
        $this->assertEquals(5, $stub->doSomething());
    }
}
?>


Em vez de retornar um valor, um método esboçado também pode causar uma exceção. Exemplo 9.9 mostra como usar throwException() para fazer isso.

Exemplo 9.9: Esboçando uma chamada de método para lançar uma exceção

<?php
require_once 'SomeClass.php';

class StubTest extends PHPUnit_Framework_TestCase
{
    public function testThrowExceptionStub()
    {
        // Cria um esboço para a classe SomeClass.
        $stub = $this->getMockBuilder('SomeClass')
                     ->getMock();

        // Configura o esboço.
        $stub->method('doSomething')
             ->will($this->throwException(new Exception));

        // $stub->doSomething() lança Exceção
        $stub->doSomething();
    }
}
?>


Alternativamente, você mesmo pode escrever um esboço enquanto melhora o design. Recursos amplamente utilizados são acessados através de uma única fachada, então você pode substituir facilmente o recurso pelo esboço. Por exemplo, em vez de ter chamadas diretas ao banco de dados espalhadas pelo código, você tem um único objeto Database que implementa a interface IDatabase. Então, você pode criar um esboço de implementação da IDatabase e usá-la em seus testes. Você pode até criar uma opção para executar os testes com o esboço do banco de dados ou com o banco de dados real, então você pode usar seus testes tanto para testes locais durante o desenvolvimento quanto para integração dos testes com o banco de dados real.

Funcionalidades que precisam ser esboçadas tendem a se agrupar no mesmo objeto, aumentando a coesão. Por apresentar a funcionalidade com uma interface única e coerente, você reduz o acoplamento com o resto do sistema.

Objetos Falsos

A prática de substituir um objeto por um dublê de teste que verifica expectativas, por exemplo asseverando que um método foi chamado, é conhecido como falsificação (mocking).

Você pode usar um objeto falso "como um ponto de observação que é usado para verificar as saídas indiretas do SST durante seu exercício. Tipicamente, o objeto falso também inclui a funcionalidade de um esboço de teste que deve retornar valores para o SST se ainda não tiver falhado nos testes, mas a ênfase está na verificação das saídas indiretas. Portanto, um objeto falso é muito mais que apenas um esboço de testes mais asserções; é utilizado de uma forma fundamentalmente diferente".

Limitações

Somente objetos falsos gerados no escopo de um teste irá ser verificado automaticamente pelo PHPUnit. Objetos falsos gerados em provedores de dados, por exemplo, não serão verificados pelo PHPUnit.

Aqui está um exemplo: suponha que queiramos testar se o método correto, update() em nosso exemplo, é chamado em um objeto que observa outro objeto. Exemplo 9.10 mostra o código para as classes Subject e Observer que são parte do Sistema Sob Teste (SST).

Exemplo 9.10: As classes Subject e Observer que são parte do Sistema Sob Teste (SST)

<?php
class Subject
{
    protected $observers = array();
    protected $name;

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

    public function getName()
    {
        return $this->name;
    }

    public function attach(Observer $observer)
    {
        $this->observers[] = $observer;
    }

    public function doSomething()
    {
        // Faça algo.

        // ...

        // Notifica aos observadores que fizemos algo.
        $this->notify('something');
    }

    public function doSomethingBad()
    {
        foreach ($this->observers as $observer) {
            $observer->reportError(42, 'Something bad happened', $this);
        }
    }

    protected function notify($argument)
    {
        foreach ($this->observers as $observer) {
            $observer->update($argument);
        }
    }

    // Outros métodos.
}

class Observer
{
    public function update($argument)
    {
        // Faça algo.
    }

    public function reportError($errorCode, $errorMessage, Subject $subject)
    {
        // Faça algo.
    }

    // Outros métodos.
}
?>


Exemplo 9.11 mostra como usar um objeto falso para testar a interação entre os objetos Subject e Observer.

Primeiro usamos o método getMock() que é fornecido pela classe PHPUnit_Framework_TestCase para configurar um objeto falso para ser o Observer. Já que fornecemos um vetor como segundo parâmetro (opcional) para o método getMock(), apenas o método update() da classe Observer é substituído por uma implementação falsificada.

Porque estamos interessados em verificar se um método foi chamado, e com quais argumentos ele foi chamado, introduzimos os métodos expects() e with() para especificar como essa interação deve considerar.

Exemplo 9.11: Testando se um método é chamado uma vez e com o argumento especificado

<?php
class SubjectTest extends PHPUnit_Framework_TestCase
{
    public function testObserversAreUpdated()
    {
        // Cria uma falsificação para a classe Observer,
        // apenas falsificando o método update().
        $observer = $this->getMockBuilder('Observer')
                         ->setMethods(array('update'))
                         ->getMock();

        // Configura a expectativa para o método update()
        // para ser chamado apenas uma vez e com a string 'something'
        // como seu parâmetro.
        $observer->expects($this->once())
                 ->method('update')
                 ->with($this->equalTo('something'));

        // Cria um objeto Subject e anexa a ele o objeto
        // Observer falsificado.
        $subject = new Subject('My subject');
        $subject->attach($observer);

        // Chama o método doSomething() no objeto $subject
        // no qual esperamos chamar o método update()
        // do objeto falsificado Observer, com a string 'something'.
        $subject->doSomething();
    }
}
?>


O método with() pode receber qualquer número de argumentos, correspondendo ao número de argumentos sendo falsos. Você pode especificar restrições mais avançadas do que uma simples igualdade no argumento do método.

Exemplo 9.12: Testando se um método é chamado com um número de argumentos restringidos de formas diferentes

<?php
class SubjectTest extends PHPUnit_Framework_TestCase
{
    public function testErrorReported()
    {
        // Create a mock for the Observer class, mocking the
        // reportError() method
        $observer = $this->getMockBuilder('Observer')
                         ->setMethods(array('reportError'))
                         ->getMock();

        $observer->expects($this->once())
                 ->method('reportError')
                 ->with(
                       $this->greaterThan(0),
                       $this->stringContains('Something'),
                       $this->anything()
                   );

        $subject = new Subject('My subject');
        $subject->attach($observer);

        // The doSomethingBad() method should report an error to the observer
        // via the reportError() method
        $subject->doSomethingBad();
    }
}
?>


O método withConsecutive() pode receber qualquer número de vetores de argumentos, dependendo das chamados que você quer testar contra. Cada vetor é uma lista de restrições correspondentes para os argumentos do método falsificado, como em with().

Exemplo 9.13: Testar que um método foi chamado duas vezes com argumentos especificados

<?php
class FooTest extends PHPUnit_Framework_TestCase
{
    public function testFunctionCalledTwoTimesWithSpecificArguments()
    {
        $mock = $this->getMockBuilder('stdClass')
                     ->setMethods(array('set'))
                     ->getMock();

        $mock->expects($this->exactly(2))
             ->method('set')
             ->withConsecutive(
                 array($this->equalTo('foo'), $this->greaterThan(0)),
                 array($this->equalTo('bar'), $this->greaterThan(0))
             );

        $mock->set('foo', 21);
        $mock->set('bar', 48);
    }
}
?>


A restrição callback() pode ser usada para verificação de argumento mais complexa. Essa restrição recebe um callback PHP como seu único argumento. O callback PHP receberá o argumento a ser verificado como seu único argumento e deverá retornar TRUE se o argumento passou a verificação e FALSE caso contrário.

Exemplo 9.14: Verificação de argumento mais complexa

<?php
class SubjectTest extends PHPUnit_Framework_TestCase
{
    public function testErrorReported()
    {
        // Create a mock for the Observer class, mocking the
        // reportError() method
        $observer = $this->getMockBuilder('Observer')
                         ->setMethods(array('reportError'))
                         ->getMock();

        $observer->expects($this->once())
                 ->method('reportError')
                 ->with($this->greaterThan(0),
                        $this->stringContains('Something'),
                        $this->callback(function($subject){
                          return is_callable(array($subject, 'getName')) &&
                                 $subject->getName() == 'My subject';
                        }));

        $subject = new Subject('My subject');
        $subject->attach($observer);

        // The doSomethingBad() method should report an error to the observer
        // via the reportError() method
        $subject->doSomethingBad();
    }
}
?>


Exemplo 9.15: Testar se um método foi chamado uma vez e com o objeto idêntico ao que foi passado

<?php
class FooTest extends PHPUnit_Framework_TestCase
{
    public function testIdenticalObjectPassed()
    {
        $expectedObject = new stdClass;

        $mock = $this->getMockBuilder('stdClass')
                     ->setMethods(array('foo'))
                     ->getMock();

        $mock->expects($this->once())
             ->method('foo')
             ->with($this->identicalTo($expectedObject));

        $mock->foo($expectedObject);
    }
}
?>


Exemplo 9.16: Cria um objeto falsificado com clonagem de parâmetros habilitada

<?php
class FooTest extends PHPUnit_Framework_TestCase
{
    public function testIdenticalObjectPassed()
    {
        $cloneArguments = true;

        $mock = $this->getMockBuilder('stdClass')
                     ->enableArgumentCloning()
                     ->getMock();

        // now your mock clones parameters so the identicalTo constraint
        // will fail.
    }
}
?>


Tabela A.1 mostra as restrições que podem ser aplicadas aos argumentos do método e Tabela 9.1 mostra os comparados que estão disponíveis para especificar o número de invocações.

Tabela 9.1. Comparadores

ComparadorSignificado
PHPUnit_Framework_MockObject_Matcher_AnyInvokedCount any()Retorna um comparador que corresponde quando o método que é avaliado for executado zero ou mais vezes.
PHPUnit_Framework_MockObject_Matcher_InvokedCount never()Retorna um comparador que corresponde quando o método que é avaliado nunca for executado.
PHPUnit_Framework_MockObject_Matcher_InvokedAtLeastOnce atLeastOnce()Retorna um comparador que corresponde quando o método que é avaliado for executado pelo menos uma vez.
PHPUnit_Framework_MockObject_Matcher_InvokedCount once()Retorna um comparador que corresponde quando o método que é avaliado for executado exatamente uma vez.
PHPUnit_Framework_MockObject_Matcher_InvokedCount exactly(int $count)Retorna um comparador que corresponde quando o método que é avaliado for executado exatamente $count vezes.
PHPUnit_Framework_MockObject_Matcher_InvokedAtIndex at(int $index)Retorna um comparador que corresponde quando o método que é avaliado for invocado no $index fornecido.


Nota

O parâmetro $index para o comparador at() se refere ao índice, iniciando em zero, em todas invocações de métodos para um objeto falsificado fornecido. Tenha cuidado ao usar este comparador, pois pode levar a testes frágeis que são muito intimamente ligados a detalhes de implementação específicos.

Profecia

Prophecy é um "framework PHP de falsificação de objetos muito poderoso e flexível, porém altamente opcional. Embora inicialmente criado para atender as necessidades do phpspec2, ele é flexível o suficiente para ser usado dentro de qualquer framework de teste por aí, com o mínimo de esforço".

O PHPUnit tem suporte nativo para uso do Prophecy para criar dublês de testes desde a versão 4.5. Exemplo 9.17 mostra como o mesmo teste mostrado no Exemplo 9.11 pode ser expressado usando a filosofia do Prophecy de profecias e revelações:

Exemplo 9.17: Testar que um método foi chamado uma vez e com um argumento específico

<?php
class SubjectTest extends PHPUnit_Framework_TestCase
{
    public function testObserversAreUpdated()
    {
        $subject = new Subject('My subject');

        // Create a prophecy for the Observer class.
        $observer = $this->prophesize('Observer');

        // Set up the expectation for the update() method
        // to be called only once and with the string 'something'
        // as its parameter.
        $observer->update('something')->shouldBeCalled();

        // Reveal the prophecy and attach the mock object
        // to the Subject.
        $subject->attach($observer->reveal());

        // Call the doSomething() method on the $subject object
        // which we expect to call the mocked Observer object's
        // update() method with the string 'something'.
        $subject->doSomething();
    }
}
?>


Por favor, referencie a documentação do Prophecy para mais detalhes sobre como criar, configurar, e usar esboços, espiões, e falsificações usando essa alternativa de framework de dublê de teste.

Falsificando Traits e Classes Abstratas

O método getMockForTrait() retorna um objeto falsificado que usa uma trait especificada. Todos métodos abstratos de uma dada trait são falsificados. Isto permite testar os métodos concretos de uma trait.

Exemplo 9.18: Testando os métodos concretos de uma trait

<?php
trait AbstractTrait
{
    public function concreteMethod()
    {
        return $this->abstractMethod();
    }

    public abstract function abstractMethod();
}

class TraitClassTest extends PHPUnit_Framework_TestCase
{
    public function testConcreteMethod()
    {
        $mock = $this->getMockForTrait('AbstractTrait');

        $mock->expects($this->any())
             ->method('abstractMethod')
             ->will($this->returnValue(TRUE));

        $this->assertTrue($mock->concreteMethod());
    }
}
?>


O método getMockForAbstractClass() retorna um objeto falso para uma classe abstrata. Todos os métodos abstratos da classe abstrata fornecida são falsificados. Isto permite testar os métodos concretos de uma classe abstrata.

Exemplo 9.19: Testando os métodos concretos de uma classe abstrata

<?php
abstract class AbstractClass
{
    public function concreteMethod()
    {
        return $this->abstractMethod();
    }

    public abstract function abstractMethod();
}

class AbstractClassTest extends PHPUnit_Framework_TestCase
{
    public function testConcreteMethod()
    {
        $stub = $this->getMockForAbstractClass('AbstractClass');

        $stub->expects($this->any())
             ->method('abstractMethod')
             ->will($this->returnValue(TRUE));

        $this->assertTrue($stub->concreteMethod());
    }
}
?>


Esboçando e Falsificando Serviços Web

Quando sua aplicação interage com um serviço web você quer testá-lo sem realmente interagir com o serviço web. Para tornar mais fáceis o esboço e falsificação dos serviços web, o getMockFromWsdl() pode ser usado da mesma forma que o getMock() (veja acima). A única diferença é que getMockFromWsdl() retorna um esboço ou falsificação baseado em uma descrição de um serviço web em WSDL e getMock() retorna um esboço ou falsificação baseado em uma classe ou interface PHP.

Exemplo 9.20 mostra como getMockFromWsdl() pode ser usado para esboçar, por exemplo, o serviço web descrito em GoogleSearch.wsdl.

Exemplo 9.20: Esboçando um serviço web

<?php
class GoogleTest extends PHPUnit_Framework_TestCase
{
    public function testSearch()
    {
        $googleSearch = $this->getMockFromWsdl(
          'GoogleSearch.wsdl', 'GoogleSearch'
        );

        $directoryCategory = new stdClass;
        $directoryCategory->fullViewableName = '';
        $directoryCategory->specialEncoding = '';

        $element = new stdClass;
        $element->summary = '';
        $element->URL = 'https://phpunit.de/';
        $element->snippet = '...';
        $element->title = '<b>PHPUnit</b>';
        $element->cachedSize = '11k';
        $element->relatedInformationPresent = TRUE;
        $element->hostName = 'phpunit.de';
        $element->directoryCategory = $directoryCategory;
        $element->directoryTitle = '';

        $result = new stdClass;
        $result->documentFiltering = FALSE;
        $result->searchComments = '';
        $result->estimatedTotalResultsCount = 3.9000;
        $result->estimateIsExact = FALSE;
        $result->resultElements = array($element);
        $result->searchQuery = 'PHPUnit';
        $result->startIndex = 1;
        $result->endIndex = 1;
        $result->searchTips = '';
        $result->directoryCategories = array();
        $result->searchTime = 0.248822;

        $googleSearch->expects($this->any())
                     ->method('doGoogleSearch')
                     ->will($this->returnValue($result));

        /**
         * $googleSearch->doGoogleSearch() will now return a stubbed result and
         * the web service's doGoogleSearch() method will not be invoked.
         */
        $this->assertEquals(
          $result,
          $googleSearch->doGoogleSearch(
            '00000000000000000000000000000000',
            'PHPUnit',
            0,
            1,
            FALSE,
            '',
            FALSE,
            '',
            '',
            ''
          )
        );
    }
}
?>


Esboçando o Sistema de Arquivos

vfsStream é um stream wrapper para um sistema de arquivos virtual que pode ser útil em testes unitários para falsificar um sistema de arquivos real.

Simplesmente adicione a dependência mikey179/vfsStream ao seu arquivo composer.json do projeto se você usa o Composer para gerenciar as dependências do seu projeto. Aqui é um exemplo simplório de um arquivo composer.json que apenas define uma dependência em ambiente de desenvolvimento para o PHPUnit 4.7 e vfsStream:

{
    "require-dev": {
        "phpunit/phpunit": "~4.6",
        "mikey179/vfsStream": "~1"
    }
}

Exemplo 9.21 mostra a classe que interage com o sistema de arquivos.

Exemplo 9.21: Uma classe que interage com um sistema de arquivos

<?php
class Example
{
    protected $id;
    protected $directory;

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

    public function setDirectory($directory)
    {
        $this->directory = $directory . DIRECTORY_SEPARATOR . $this->id;

        if (!file_exists($this->directory)) {
            mkdir($this->directory, 0700, TRUE);
        }
    }
}?>


Sem um sistema de arquivos virtual tal como o vfsStream não poderíamos testar o método setDirectory() isolado de influências externas (veja Exemplo 9.22).

Exemplo 9.22: Testando uma classe que interage com o sistema de arquivos

<?php
require_once 'Example.php';

class ExampleTest extends PHPUnit_Framework_TestCase
{
    protected function setUp()
    {
        if (file_exists(dirname(__FILE__) . '/id')) {
            rmdir(dirname(__FILE__) . '/id');
        }
    }

    public function testDirectoryIsCreated()
    {
        $example = new Example('id');
        $this->assertFalse(file_exists(dirname(__FILE__) . '/id'));

        $example->setDirectory(dirname(__FILE__));
        $this->assertTrue(file_exists(dirname(__FILE__) . '/id'));
    }

    protected function tearDown()
    {
        if (file_exists(dirname(__FILE__) . '/id')) {
            rmdir(dirname(__FILE__) . '/id');
        }
    }
}
?>


A abordagem acima tem várias desvantagens:

  • Assim como um recurso externo, podem haver problemas intermitentes com o sistema de arquivos. Isso deixa os testes, com os quais interage, esquisitos.

  • Nos métodos setUp() e tearDown() temos que assegurar que o diretório não existe antes e depois do teste.

  • Quando a execução do teste termina antes do método tearDown() ser invocado, o diretório permanece no sistema de arquivos.

Exemplo 9.23 mostra como o vfsStream pode ser usado para falsificar o sistema de arquivos em um teste para uma classe que interage com o sistema de arquivos.

Exemplo 9.23: Falsificando o sistema de arquivos em um teste para a classe que interage com o sistema de arquivos

<?php
require_once 'vfsStream/vfsStream.php';
require_once 'Example.php';

class ExampleTest extends PHPUnit_Framework_TestCase
{
    public function setUp()
    {
        vfsStreamWrapper::register();
        vfsStreamWrapper::setRoot(new vfsStreamDirectory('exampleDir'));
    }

    public function testDirectoryIsCreated()
    {
        $example = new Example('id');
        $this->assertFalse(vfsStreamWrapper::getRoot()->hasChild('id'));

        $example->setDirectory(vfsStream::url('exampleDir'));
        $this->assertTrue(vfsStreamWrapper::getRoot()->hasChild('id'));
    }
}
?>


Isso tem várias vantagens:

  • O próprio teste fica mais conciso.

  • O vfsStream concede ao desenvolvedor de testes controle total sobre a aparência do ambiente do sistema de arquivos para o código testado.

  • Já que as operações do sistema de arquivos não operam mais no sistema de arquivos real, operações de limpeza em um método tearDown() não são mais exigidas.

Por favor, abra um chamado no GitHub para sugerir melhorias para esta página. Obrigado!