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 Class vão dentro de uma classe ClassTest.

  2. ClassTest 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 asserção tal como assertEquals() (veja Apêndice A) são usados para asseverar que um valor real equivale a um valor esperado.

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

<?php
use PHPUnit\Framework\TestCase;

class StackTest extends TestCase
{
    public function testPushAndPop()
    {
        $stack = [];
        $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-a 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. Frequentemente 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 de dependências explícitas 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
use PHPUnit\Framework\TestCase;

class StackTest extends TestCase
{
    public function testEmpty()
    {
        $stack = [];
        $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().

Nota

O valor de retorno produzido por um produtor é passado "como está" para seus consumidores por padrão. Isso significa que quando um produtor retorna um objeto, uma referência para esse objeto é passada para os consumidores. Quando uma cópia deve ser usada ao invés de uma referência, então @depends clone deve ser usado ao invés de @depends.

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
use PHPUnit\Framework\TestCase;

class DependencyFailureTest extends TestCase
{
    public function testOne()
    {
        $this->assertTrue(false);
    }

    /**
     * @depends testOne
     */
    public function testTwo()
    {
    }
}
?>
phpunit --verbose DependencyFailureTest
PHPUnit 6.5.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
use PHPUnit\Framework\TestCase;

class MultipleDependenciesTest extends 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(
            ['first', 'second'],
            func_get_args()
        );
    }
}
?>
phpunit --verbose MultipleDependenciesTest
PHPUnit 6.5.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 retornar um vetor de vetores ou um objeto que implemente a interface Iterator e produza 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
use PHPUnit\Framework\TestCase;

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

    public function additionProvider()
    {
        return [
            [0, 0, 0],
            [0, 1, 1],
            [1, 0, 1],
            [1, 1, 3]
        ];
    }
}
?>
phpunit DataTest
PHPUnit 6.5.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.


Quando usar um número grande de conjuntos de dados é útil nomear cada um com uma chave string ao invés do padrão numérico. Output will be more verbose as it'll contain that name of a dataset that breaks a test.

Exemplo 2.6: Usando um provedor de dados com conjuntos de dados nomeados

<?php
use PHPUnit\Framework\TestCase;

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

    public function additionProvider()
    {
        return [
            'adding zeros'  => [0, 0, 0],
            'zero plus one' => [0, 1, 1],
            'one plus zero' => [1, 0, 1],
            'one plus one'  => [1, 1, 3]
        ];
    }
}
?>
phpunit DataTest
PHPUnit 6.5.0 by Sebastian Bergmann and contributors.

...F

Time: 0 seconds, Memory: 5.75Mb

There was 1 failure:

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

/home/sb/DataTest.php:9

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


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

<?php
use PHPUnit\Framework\TestCase;

require 'CsvFileIterator.php';

class DataTest extends 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 6.5.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.8: A classe CsvFileIterator

<?php
use PHPUnit\Framework\TestCase;

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.9

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

<?php
use PHPUnit\Framework\TestCase;

class DependencyAndDataProviderComboTest extends TestCase
{
    public function provider()
    {
        return [['provider1'], ['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(
            ['provider1', 'first', 'second'],
            func_get_args()
        );
    }
}
?>
phpunit --verbose DependencyAndDataProviderComboTest
PHPUnit 6.5.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.10 mostra como usar a anotação expectException() para testar se uma exceção é lançada dentro do código de teste.

Exemplo 2.10: Usando o método expectException()

<?php
use PHPUnit\Framework\TestCase;

class ExceptionTest extends TestCase
{
    public function testException()
    {
        $this->expectException(InvalidArgumentException::class);
    }
}
?>
phpunit ExceptionTest
PHPUnit 6.5.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.


Além do método expectException() os métodos expectExceptionCode(), expectExceptionMessage(), e expectExceptionMessageRegExp() existem para configurar expectativas de exceções lançadas pelo código sob teste.

Alternativamente, você pode usar as anotações @expectedException, @expectedExceptionCode, @expectedExceptionMessage, e @expectedExceptionMessageRegExp para configurar expectativas de exceções lançadas pelo código sob teste. Exemplo 2.11 mostra um exemplo.

Exemplo 2.11: Usando a anotação @expectedException

<?php
use PHPUnit\Framework\TestCase;

class ExceptionTest extends TestCase
{
    /**
     * @expectedException InvalidArgumentException
     */
    public function testException()
    {
    }
}
?>
phpunit ExceptionTest
PHPUnit 6.5.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.


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.12.

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.12: Esperando um erro PHP usando @expectedException

<?php
use PHPUnit\Framework\TestCase;

class ExpectedErrorTest extends TestCase
{
    /**
     * @expectedException PHPUnit\Framework\Error
     */
    public function testFailingInclude()
    {
        include 'not_existing_file.php';
    }
}
?>
phpunit -d error_reporting=2 ExpectedErrorTest
PHPUnit 6.5.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 algo que dependa 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.13: Testando valores de retorno de código que utiliza PHP Errors

<?php
use PHPUnit\Framework\TestCase;

class ErrorSuppressionTest extends 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 6.5.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.14 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.14: Testando a saída de uma função ou método

<?php
use PHPUnit\Framework\TestCase;

class OutputTest extends TestCase
{
    public function testExpectFooActualFoo()
    {
        $this->expectOutputString('foo');
        print 'foo';
    }

    public function testExpectBarActualBaz()
    {
        $this->expectOutputString('bar');
        print 'baz';
    }
}
?>
phpunit OutputTest
PHPUnit 6.5.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.1 mostra os métodos fornecidos para testar saídas.

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

MétodoSignificado
void expectOutputRegex(string $regularExpression)Configura a expectativa de que a saída combine com uma $regularExpression.
void expectOutputString(string $expectedString)Configura a expectativa de que a saída é igual a uma $expectedString.
bool setOutputCallback(callable $callback)Define um callback que é usado, por exemplo, para normalizar a saída real.
string getActualOutput()Recupera 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.15: Saída de erro gerada quando uma comparação de vetores falha

<?php
use PHPUnit\Framework\TestCase;

class ArrayDiffTest extends TestCase
{
    public function testEquality() {
        $this->assertEquals(
            [1, 2,  3, 4, 5, 6],
            [1, 2, 33, 4, 5, 6]
        );
    }
}
?>
phpunit ArrayDiffTest
PHPUnit 6.5.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.16: Saída de erro quando uma comparação de um vetor longo falha

<?php
use PHPUnit\Framework\TestCase;

class LongArrayDiffTest extends TestCase
{
    public function testEquality() {
        $this->assertEquals(
            [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 2,  3, 4, 5, 6],
            [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 2, 33, 4, 5, 6]
        );
    }
}
?>
phpunit LongArrayDiffTest
PHPUnit 6.5.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.17: Caso extremo na geração de diferenciação quando se usa uma comparação fraca

<?php
use PHPUnit\Framework\TestCase;

class ArrayWeakComparisonTest extends TestCase
{
    public function testEquality() {
        $this->assertEquals(
            [1, 2, 3, 4, 5, 6],
            ['1', 2, 33, 4, 5, 6]
        );
    }
}
?>
phpunit ArrayWeakComparisonTest
PHPUnit 6.5.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!