Capítulo 2. Escrevendo Testes para o PHPUnit

Exemplo 2.1 mostra como podemos escrever testes usando o PHPUnit que exercita operações de vetor do PHP. O exemplo introduz as convenções básicas e passos para escrever testes com o PHPUnit:

  1. Os testes para uma classe Classe vão dentro de uma classe ClasseTest.

  2. ClasseTest herda (na maioria das vezes) de PHPUnit_Framework_TestCase.

  3. Os testes são métodos públicos que são nomeados test*.

    Alternativamente, você pode usar a anotação @test em um bloco de documentação de um método para marcá-lo como um método de teste.

  4. Dentro dos métodos de teste, métodos de confirmação como assertEquals() (veja Apêndice A) são usados para confirmar que um valor real equivale a um valor esperado.

Exemplo 2.1: Testando operações de vetores com o PHPUnit

<?php
class StackTest extends PHPUnit_Framework_TestCase
{
    public function testPushAndPop()
    {
        $stack = array();
        $this->assertEquals(0, count($stack));

        array_push($stack, 'foo');
        $this->assertEquals('foo', $stack[count($stack)-1]);
        $this->assertEquals(1, count($stack));

        $this->assertEquals('foo', array_pop($stack));
        $this->assertEquals(0, count($stack));
    }
}
?>


 

Sempre que você estiver tentado a escrever algo em uma declaração print ou uma expressão depuradora, escreva como um teste em vez disso.

 
 --Martin Fowler

Dependências de Testes

 

Testes Unitários são primeiramente escritos como uma boa prática para ajudar desenvolvedores a identificar e corrigir defeitos, refatorar o código e servir como documentação para uma unidade de programa sob teste. Para conseguir esses benefícios, testes unitários idealmente deveriam cobrir todos os caminhos possíveis em um programa. Um teste unitário geralmente cobre um caminho específico em uma função ou método. Porém um método de teste não é necessariamente uma entidade encapsulada e independente. Às vezes existem dependências implícitas entre métodos de teste, escondidas no cenário de implementação de um teste.

 
 --Adrian Kuhn et. al.

O PHPUnit suporta a declaração explícita de dependências entre métodos de teste. Tais dependências não definem a ordem em que os métodos de teste devem ser executados, mas permitem o retorno de uma instância do ambiente do teste por um produtor e a passagem dele para os consumidores dependentes.

  • Um produtor é um método de teste que produz a sua unidade sob teste como valor de retorno.

  • Um consumidor é um método de teste que depende de um ou mais produtores e seus valores retornados.

Exemplo 2.2 mostra como usar a anotação @depends para expressar dependências entre métodos de teste.

Exemplo 2.2: Usando a anotação @depends para expressar dependências

<?php
class StackTest extends PHPUnit_Framework_TestCase
{
    public function testEmpty()
    {
        $stack = array();
        $this->assertEmpty($stack);

        return $stack;
    }

    /**
     * @depends testEmpty
     */
    public function testPush(array $stack)
    {
        array_push($stack, 'foo');
        $this->assertEquals('foo', $stack[count($stack)-1]);
        $this->assertNotEmpty($stack);

        return $stack;
    }

    /**
     * @depends testPush
     */
    public function testPop(array $stack)
    {
        $this->assertEquals('foo', array_pop($stack));
        $this->assertEmpty($stack);
    }
}
?>


No exemplo acima, o primeiro teste, testEmpty(), cria um novo vetor e assegura que o mesmo é vazio. O teste então retorna o ambiente como resultado. O segundo teste, testPush(), depende de testEmpty() e lhe é passado o resultado do qual ele depende como um argumento. Finalmente, testPop() depende de testPush().

Para localizar defeitos rapidamente, queremos nossa atenção focada nas falhas relevantes dos testes. É por isso que o PHPUnit pula a execução de um teste quando um teste do qual ele depende falha. Isso melhora a localização de defeitos por explorar as dependências entre os testes como mostrado em Exemplo 2.3.

Exemplo 2.3: Explorando as dependências entre os testes

<?php
class DependencyFailureTest extends PHPUnit_Framework_TestCase
{
    public function testOne()
    {
        $this->assertTrue(FALSE);
    }

    /**
     * @depends testOne
     */
    public function testTwo()
    {
    }
}
?>
phpunit --verbose DependencyFailureTest
PHPUnit 4.8.0 by Sebastian Bergmann and contributors.

FS

Time: 0 seconds, Memory: 5.00Mb

There was 1 failure:

1) DependencyFailureTest::testOne
Failed asserting that false is true.

/home/sb/DependencyFailureTest.php:6

There was 1 skipped test:

1) DependencyFailureTest::testTwo
This test depends on "DependencyFailureTest::testOne" to pass.


FAILURES!
Tests: 1, Assertions: 1, Failures: 1, Skipped: 1.


Um teste pode ter mais de uma anotação @depends. O PHPUnit não muda a ordem em que os testes são executados, portanto você deve se certificar de que as dependências de um teste podem realmente ser encontradas antes de executar o teste.

Um teste que tem mais de uma anotação @depends vai obter um ambiente a partir do primeiro produtor como o primeiro argumento, um ambiente a partir do segundo produtor como o segundo argumento, e assim por diante. Veja Exemplo 2.4

Exemplo 2.4: Teste com múltiplas dependências

<?php
class MultipleDependenciesTest extends PHPUnit_Framework_TestCase
{
    public function testProducerFirst()
    {
        $this->assertTrue(true);
        return 'first';
    }

    public function testProducerSecond()
    {
        $this->assertTrue(true);
        return 'second';
    }

    /**
     * @depends testProducerFirst
     * @depends testProducerSecond
     */
    public function testConsumer()
    {
        $this->assertEquals(
            array('first', 'second'),
            func_get_args()
        );
    }
}
?>
phpunit --verbose MultipleDependenciesTest
PHPUnit 4.8.0 by Sebastian Bergmann and contributors.

...

Time: 0 seconds, Memory: 3.25Mb

OK (3 tests, 3 assertions)


Provedores de Dados

Um método de teste pode aceitar argumentos arbitrários. Esses argumentos devem ser fornecidos por um método provedor de dados (additionProvider() em Exemplo 2.5). O método provedor de dados a ser usado é especificado usando a anotação @dataProvider.

Um método provedor de dados deve ser public e ou retornar um vetor de vetores ou um objeto que implementa a interface Iterator e produz um vetor para cada passo da iteração. Para cada vetor que é parte da coleção o método de teste será chamado com os conteúdos do vetor como seus argumentos.

Exemplo 2.5: Usando um provedor de dados que retorna um vetor de vetores

<?php
class DataTest extends PHPUnit_Framework_TestCase
{
    /**
     * @dataProvider additionProvider
     */
    public function testAdd($a, $b, $expected)
    {
        $this->assertEquals($expected, $a + $b);
    }

    public function additionProvider()
    {
        return array(
          array(0, 0, 0),
          array(0, 1, 1),
          array(1, 0, 1),
          array(1, 1, 3)
        );
    }
}
?>
phpunit DataTest
PHPUnit 4.8.0 by Sebastian Bergmann and contributors.

...F

Time: 0 seconds, Memory: 5.75Mb

There was 1 failure:

1) DataTest::testAdd with data set #3 (1, 1, 3)
Failed asserting that 2 matches expected 3.

/home/sb/DataTest.php:9

FAILURES!
Tests: 4, Assertions: 4, Failures: 1.


Exemplo 2.6: Usando um provedor de dados que retorna um objeto Iterador

<?php
require 'CsvFileIterator.php';

class DataTest extends PHPUnit_Framework_TestCase
{
    /**
     * @dataProvider additionProvider
     */
    public function testAdd($a, $b, $expected)
    {
        $this->assertEquals($expected, $a + $b);
    }

    public function additionProvider()
    {
        return new CsvFileIterator('data.csv');
    }
}
?>
phpunit DataTest
PHPUnit 4.8.0 by Sebastian Bergmann and contributors.

...F

Time: 0 seconds, Memory: 5.75Mb

There was 1 failure:

1) DataTest::testAdd with data set #3 ('1', '1', '3')
Failed asserting that 2 matches expected '3'.

/home/sb/DataTest.php:11

FAILURES!
Tests: 4, Assertions: 4, Failures: 1.


Exemplo 2.7: A classe CsvFileIterator

<?php
class CsvFileIterator implements Iterator {
    protected $file;
    protected $key = 0;
    protected $current;

    public function __construct($file) {
        $this->file = fopen($file, 'r');
    }

    public function __destruct() {
        fclose($this->file);
    }

    public function rewind() {
        rewind($this->file);
        $this->current = fgetcsv($this->file);
        $this->key = 0;
    }

    public function valid() {
        return !feof($this->file);
    }

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

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

    public function next() {
        $this->current = fgetcsv($this->file);
        $this->key++;
    }
}
?>


Quando um teste recebe uma entrada tanto de um método @dataProvider quanto de um ou mais testes dos quais ele @depends, os argumentos do provedor de dados virão antes daqueles dos quais ele é dependente. Os argumentos dos quais o teste depende serão os mesmos para cada conjunto de dados. Veja Exemplo 2.8

Exemplo 2.8: Combinação de @depends e @dataProvider no mesmo teste

<?php
class DependencyAndDataProviderComboTest extends PHPUnit_Framework_TestCase
{
    public function provider()
    {
        return array(array('provider1'), array('provider2'));
    }

    public function testProducerFirst()
    {
        $this->assertTrue(true);
        return 'first';
    }

    public function testProducerSecond()
    {
        $this->assertTrue(true);
        return 'second';
    }

    /**
     * @depends testProducerFirst
     * @depends testProducerSecond
     * @dataProvider provider
     */
    public function testConsumer()
    {
        $this->assertEquals(
            array('provider1', 'first', 'second'),
            func_get_args()
        );
    }
}
?>
phpunit --verbose DependencyAndDataProviderComboTest
PHPUnit 4.8.0 by Sebastian Bergmann and contributors.

...F

Time: 0 seconds, Memory: 3.50Mb

There was 1 failure:

1) DependencyAndDataProviderComboTest::testConsumer with data set #1 ('provider2')
Failed asserting that two arrays are equal.
--- Expected
+++ Actual
@@ @@
Array (
-    0 => 'provider1'
+    0 => 'provider2'
1 => 'first'
2 => 'second'
)

/home/sb/DependencyAndDataProviderComboTest.php:31

FAILURES!
Tests: 4, Assertions: 4, Failures: 1.


Nota

Quando um teste depende de um teste que usa provedores de dados, o teste dependente será executado quando o teste do qual ele depende for bem sucedido em pelo menos um conjunto de dados. O resultado de um teste que usa provedores de dados não pode ser injetado dentro de um teste dependente.

Nota

Todos provedores de dados são executados antes da chamada ao método estático setUpBeforeClass e a primeira chamada ao método setUp. Por isso você não pode acessar quaisquer variáveis que criar ali de dentro de um provedor de dados. Isto é necessário para que o PHPUnit seja capaz de calcular o número total de testes.

Testando Exceções

Exemplo 2.9 mostra como usar a anotação @expectedException para testar se uma exceção é lançada dentro do código de teste.

Exemplo 2.9: Usando a anotação @expectedException

<?php
class ExceptionTest extends PHPUnit_Framework_TestCase
{
    /**
     * @expectedException InvalidArgumentException
     */
    public function testException()
    {
    }
}
?>
phpunit ExceptionTest
PHPUnit 4.8.0 by Sebastian Bergmann and contributors.

F

Time: 0 seconds, Memory: 4.75Mb

There was 1 failure:

1) ExceptionTest::testException
Expected exception InvalidArgumentException


FAILURES!
Tests: 1, Assertions: 1, Failures: 1.


Adicionalmente, você pode usar @expectedExceptionMessage, @expectedExceptionMessageRegExp e @expectedExceptionCode em combinação com @expectedException para testar a mensagem de exceção e o código de exceção como mostrado em Exemplo 2.10.

Exemplo 2.10: Usando as anotações @expectedExceptionMessage, @expectedExceptionMessageRegExp e @expectedExceptionCode

<?php
class ExceptionTest extends PHPUnit_Framework_TestCase
{
    /**
     * @expectedException        InvalidArgumentException
     * @expectedExceptionMessage Right Message
     */
    public function testExceptionHasRightMessage()
    {
        throw new InvalidArgumentException('Some Message', 10);
    }

    /**
     * @expectedException              InvalidArgumentException
     * @expectedExceptionMessageRegExp #Right.*#
     */
    public function testExceptionMessageMatchesRegExp()
    {
        throw new InvalidArgumentException('Some Message', 10);
    }

    /**
     * @expectedException     InvalidArgumentException
     * @expectedExceptionCode 20
     */
    public function testExceptionHasRightCode()
    {
        throw new InvalidArgumentException('Some Message', 10);
    }
}
?>
phpunit ExceptionTest
PHPUnit 4.8.0 by Sebastian Bergmann and contributors.

FFF

Time: 0 seconds, Memory: 3.00Mb

There were 3 failures:

1) ExceptionTest::testExceptionHasRightMessage
Failed asserting that exception message 'Some Message' contains 'Right Message'.

2) ExceptionTest::testExceptionMessageMatchesRegExp
Failed asserting that exception message 'Some Message' matches '#Right.*#'.

3) ExceptionTest::testExceptionHasRightCode
Failed asserting that expected exception code 20 is equal to 10.


FAILURES!
Tests: 3, Assertions: 6, Failures: 3.


Mais exemplos de @expectedExceptionMessage, @expectedExceptionMessageRegExp e @expectedExceptionCode são mostrados em “@expectedExceptionMessage”, “@expectedExceptionMessageRegExp” e “@expectedExceptionCode” respectivamente.

Alternativamente, você pode usar o método setExpectedException() ou setExpectedExceptionRegExp() para definir a exceção esperada como mostrado em Exemplo 2.11.

Exemplo 2.11: Esperando uma exceção surgir do código de teste

<?php
class ExceptionTest extends PHPUnit_Framework_TestCase
{
    public function testException()
    {
        $this->setExpectedException('InvalidArgumentException');
    }

    public function testExceptionHasRightMessage()
    {
        $this->setExpectedException(
          'InvalidArgumentException', 'Right Message'
        );
        throw new InvalidArgumentException('Some Message', 10);
    }

    public function testExceptionMessageMatchesRegExp()
    {
        $this->setExpectedExceptionRegExp(
          'InvalidArgumentException', '/Right.*/', 10
        );
        throw new InvalidArgumentException('The Wrong Message', 10);
    }

    public function testExceptionHasRightCode()
    {
        $this->setExpectedException(
          'InvalidArgumentException', 'Right Message', 20
        );
        throw new InvalidArgumentException('The Right Message', 10);
    }
}
?>
phpunit ExceptionTest
PHPUnit 4.8.0 by Sebastian Bergmann and contributors.

FFFF

Time: 0 seconds, Memory: 3.00Mb

There were 4 failures:

1) ExceptionTest::testException
Expected exception InvalidArgumentException

2) ExceptionTest::testExceptionHasRightMessage
Failed asserting that exception message 'Some Message' contains 'Right Message'.

3) ExceptionTest::testExceptionMessageMatchesRegExp
Failed asserting that exception message 'The Wrong Message' contains '/Right.*/'.

4) ExceptionTest::testExceptionHasRightCode
Failed asserting that expected exception code 20 is equal to 10.


FAILURES!
Tests: 4, Assertions: 8, Failures: 4.


Tabela 2.1 mostra os métodos fornecidos para testar exceções.

Tabela 2.1. Métodos para testar exceções

MétodoSignificado
void setExpectedException(string $nomeExcecao[, string $mensagemExcecao = '', inteiro $codigoExcecao = NULL])Define o $exceptionName, $exceptionMessage e $exceptionCode esperados.
void setExpectedExceptionRegExp(string $exceptionName[, string $exceptionMessageRegExp = '', integer $exceptionCode = NULL])Define o $exceptionName, $exceptionMessageRegExp e $exceptionCode esperados.
String getExpectedException()Retorna o nome da exceção esperada.


Você também pode usar a abordagem mostrada em Exemplo 2.12 para testar exceções

Exemplo 2.12: Abordagem alternativa para testar exceções

<?php
class ExceptionTest extends PHPUnit_Framework_TestCase {
    public function testException() {
        try {
            // ... Código que se espera que lance uma exceção ...
        }

        catch (InvalidArgumentException $expected) {
            return;
        }

        $this->fail('Uma exceção esperada não foi criada.');
    }
}
?>


Se o código que deve lançar uma exceção no Exemplo 2.12 não lançá-la, a chamada subsequente ao fail() vai parar o teste e sinalizar um problema com o teste. Se a exceção esperada é lançada, o bloco catch será executado, e o teste terminará com sucesso.

Testando Erros PHP

Por padrão, o PHPUnit converte os erros, avisos e notificações do PHP que são disparados durante a execução de um teste para uma exceção. Usando essas exceções, você pode, por exemplo, esperar que um teste dispare um erro PHP como mostrado no Exemplo 2.13.

Nota

A configuração em tempo de execução error_reporting do PHP pode limitar quais erros o PHPUnit irá converter para exceções. Se você está tendo problemas com essa funcionalidade, certifique-se que o PHP não está configurado para suprimir os tipos de erros que você esta testando.

Exemplo 2.13: Esperando um erro PHP usando @expectedException

<?php
class ExpectedErrorTest extends PHPUnit_Framework_TestCase
{
    /**
     * @expectedException PHPUnit_Framework_Error
     */
    public function testFailingInclude()
    {
        include 'not_existing_file.php';
    }
}
?>
phpunit -d error_reporting=2 ExpectedErrorTest
PHPUnit 4.8.0 by Sebastian Bergmann and contributors.

.

Time: 0 seconds, Memory: 5.25Mb

OK (1 test, 1 assertion)


PHPUnit_Framework_Error_Notice e PHPUnit_Framework_Error_Warning representam notificações e avisos do PHP, respectivamente.

Nota

Você deve ser o mais específico possível quando testar exceções. Testar por classes que são muito genéricas pode causar efeitos colaterais indesejáveis. Da mesma forma, testar para a classe Exception com @expectedException ou setExpectedException() não é mais permitido.

Ao testar com funções que dependem de funções php que disparam erros como fopen pode ser útil algumas vezes usar a supressão de erros enquanto testa. Isso permite a você verificar os valores retornados por suprimir notificações que levariam a uma PHPUnit_Framework_Error_Notice phpunit.

Exemplo 2.14: Testando valores de retorno de código que utiliza PHP Errors

<?php
class ErrorSuppressionTest extends PHPUnit_Framework_TestCase
{
    public function testFileWriting() {
        $writer = new FileWriter;
        $this->assertFalse(@$writer->write('/is-not-writeable/file', 'stuff'));
    }
}
class FileWriter
{
    public function write($file, $content) {
        $file = fopen($file, 'w');
        if($file == false) {
            return false;
        }
        // ...
    }
}

?>
phpunit ErrorSuppressionTest
PHPUnit 4.8.0 by Sebastian Bergmann and contributors.

.

Time: 1 seconds, Memory: 5.25Mb

OK (1 test, 1 assertion)



Sem a supressão de erros o teste teria relatado uma falha fopen(/is-not-writeable/file): failed to open stream: No such file or directory.

Testando Saídas

Às vezes você quer assegurar que a execução de um método, por exemplo, gere uma saída esperada (via echo ou print, por exemplo). A classe PHPUnit_Framework_TestCase usa a funcionalidade Output Buffering do PHP para fornecer a funcionalidade que é necessária para isso.

Exemplo 2.15 mostra como usar o método expectOutputString() para definir a saída esperada. Se essa saída esperada não for gerada, o teste será contado como uma falha.

Exemplo 2.15: Testando a saída de uma função ou método

<?php
class OutputTest extends PHPUnit_Framework_TestCase
{
    public function testExpectFooActualFoo()
    {
        $this->expectOutputString('foo');
        print 'foo';
    }

    public function testExpectBarActualBaz()
    {
        $this->expectOutputString('bar');
        print 'baz';
    }
}
?>
phpunit OutputTest
PHPUnit 4.8.0 by Sebastian Bergmann and contributors.

.F

Time: 0 seconds, Memory: 5.75Mb

There was 1 failure:

1) OutputTest::testExpectBarActualBaz
Failed asserting that two strings are equal.
--- Expected
+++ Actual
@@ @@
-'bar'
+'baz'


FAILURES!
Tests: 2, Assertions: 2, Failures: 1.


Tabela 2.2 mostra os métodos fornecidos para testar saídas.

Tabela 2.2. Métodos para testar a saída

MétodoSignificado
void expectOutputRegex(string $regularExpression)Define a saída que se espera combinar com a $regularExpression.
void expectOutputString(string $expectedString)Define a saída que se espera ser igual a uma $expectedString.
booleano setOutputCallback(callable $callback)Define um callback que é usado, por exemplo, para normalizar a saída real.


Nota

Um teste que emite saída irá falhar no modo estrito.

Saída de Erro

Sempre que um teste falha o PHPUnit faz o melhor para fornecer a você o máximo possível de conteúdo que possa ajudar a identificar o problema.

Exemplo 2.16: Saída de erro gerada quando uma comparação de vetores falha

<?php
class ArrayDiffTest extends PHPUnit_Framework_TestCase
{
    public function testEquality() {
        $this->assertEquals(
            array(1,2,3 ,4,5,6),
            array(1,2,33,4,5,6)
        );
    }
}
?>
phpunit ArrayDiffTest
PHPUnit 4.8.0 by Sebastian Bergmann and contributors.

F

Time: 0 seconds, Memory: 5.25Mb

There was 1 failure:

1) ArrayDiffTest::testEquality
Failed asserting that two arrays are equal.
--- Expected
+++ Actual
@@ @@
 Array (
     0 => 1
     1 => 2
-    2 => 3
+    2 => 33
     3 => 4
     4 => 5
     5 => 6
 )

/home/sb/ArrayDiffTest.php:7

FAILURES!
Tests: 1, Assertions: 1, Failures: 1.


Neste exemplo apenas um dos valores dos vetores diferem e os outros valores são exibidos para fornecer o contexto onde o erro ocorreu.

Quando a saída gerada for longa demais para ler o PHPUnit vai quebrá-la e fornecer algumas linhas de contexto ao redor de cada diferença.

Exemplo 2.17: Saída de erro quando uma comparação de um vetor longo falha

<?php
class LongArrayDiffTest extends PHPUnit_Framework_TestCase
{
    public function testEquality() {
        $this->assertEquals(
            array(0,0,0,0,0,0,0,0,0,0,0,0,1,2,3 ,4,5,6),
            array(0,0,0,0,0,0,0,0,0,0,0,0,1,2,33,4,5,6)
        );
    }
}
?>
phpunit LongArrayDiffTest
PHPUnit 4.8.0 by Sebastian Bergmann and contributors.

F

Time: 0 seconds, Memory: 5.25Mb

There was 1 failure:

1) LongArrayDiffTest::testEquality
Failed asserting that two arrays are equal.
--- Expected
+++ Actual
@@ @@
     13 => 2
-    14 => 3
+    14 => 33
     15 => 4
     16 => 5
     17 => 6
 )


/home/sb/LongArrayDiffTest.php:7

FAILURES!
Tests: 1, Assertions: 1, Failures: 1.


Casos Extremos

Quando uma comparação falha o PHPUnit cria uma representação textual da entrada de valores e as compara. Devido a essa implementação uma diferenciação pode mostrar mais problemas do que realmente existem.

Isso só acontece quando se usa assertEquals ou outra função de comparação 'fraca' em vetores ou objetos.

Exemplo 2.18: Caso extremo na geração de diferenciação quando se usa uma comparação fraca

<?php
class ArrayWeakComparisonTest extends PHPUnit_Framework_TestCase
{
    public function testEquality() {
        $this->assertEquals(
            array(1  ,2,3 ,4,5,6),
            array('1',2,33,4,5,6)
        );
    }
}
?>
phpunit ArrayWeakComparisonTest
PHPUnit 4.8.0 by Sebastian Bergmann and contributors.

F

Time: 0 seconds, Memory: 5.25Mb

There was 1 failure:

1) ArrayWeakComparisonTest::testEquality
Failed asserting that two arrays are equal.
--- Expected
+++ Actual
@@ @@
 Array (
-    0 => 1
+    0 => '1'
     1 => 2
-    2 => 3
+    2 => 33
     3 => 4
     4 => 5
     5 => 6
 )


/home/sb/ArrayWeakComparisonTest.php:7

FAILURES!
Tests: 1, Assertions: 1, Failures: 1.


Neste exemplo a diferença no primeiro índice entre 1 e '1' é relatada ainda que o assertEquals considere os valores como uma combinação.

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