DEV Community

Michael de Oliveira Ferreira
Michael de Oliveira Ferreira

Posted on

Testando e Implementando Pacotes Laravel: Um Guia Passo a Passo

Quem nunca sofreu ao criar pacotes composer para serem integrados com Laravel e teve surpresas como quebra de dependências e afins. Esse é um problema que acontece e muitas vezes causam problemas sérios, pois devido a isso acabamos fazendo boa parte do código "testando na nossa cabeça" e torcendo para que funcione bem.Com este tutorial vamos criar passo-a-passo um pacote e testá-lo durante sua criação de forma eficiente.

Começando a diversão!

Vamos começar inicializando o nosso composer. Não vou me aprofundar muito nele, mas resumidamente ele é o nosso gerenciador de pacotes no PHP(na real acho que quase todos devs PHP o conhecem). Podemos criar uma pasta vazia com o nome do projeto. No meu caso estou criando um pacote para buscar ceps no via cep(eu sei que é simples rsrsrs). Vamos começar com o comando composer init, que nos mostrará uma tela como essa onde preencheremos algumas questões relacionadas ao nosso pacote e criará uma estrutura básica de diretórios(o padrão é usar a pasta src).

Image description

Ao criamos pacotes sempre ocorriam as seguintes perguntas. Como vou desenvolver um pacote para o Laravel em um ambiente que ele não está instalado? Como testar esse pacote sem as dependências do framework sendo que vou utilizar recursos do mesmo? Para responder essas perguntas costumo usar Orchestra Testbench, a instalação é feito pelo comando composer require orchestra/testbench --dev.

Agora que instalamos o Orchestra Testbench e como somos desenvolvedores preocupados com a qualidade do nosso código(eu sei que vocês se preocupam com a qualidade do código rsrsrs), já instalamos o PHPUnit ou o PestPHP, nesse exemplo vou usar o PHPUnit composer require phpunit/phpunit --dev e usar o ./vendor/bin/phpunit --generate-configuration.

Definindo nosso escopo

Vamos fazer um pacote simples. Queremos apenas uma forma mais fácil de consultar os ceps através do via cep que receba 1 ou mais ceps(esse foi um teste técnico que vi e achei interessante) e claro vamos fazer testes para garantir que está tudo ok e vermos o código passo-a-passo em um ciclo de TDD.

Configurando o ambiente para criação do pacote

Começaremos criando a pasta tests e no arquivo composer.json criaremos o autoload para nosso ambiente de dev, com algo próximo a isso:

"autoload-dev": {
        "psr-4": {
            "DominusDev\\LaravelViacep\\Tests\\": "tests/"
        }
},
Enter fullscreen mode Exit fullscreen mode

Também vou aproveitar para criar minha TestCase, você como um dev sagaz leu a documentação do Orchestra, certo?! Resumindo, normalmente as classes de teste estendem a classe TestCase. Com o Testbench, aconselho fortemente a criar uma TestCase como esta:

<?php

namespace DominusDev\LaravelViacep\Tests;

use Orchestra\Testbench\TestCase as BaseTestCase;

class TestCase extends BaseTestCase
{
}
Enter fullscreen mode Exit fullscreen mode

Notem que estou usando um TestCase diferente do PHPUnit. Pode parecer um comentário inútil, mas vez ou outra confundi a TestCase do PHPUnit e do Testbench, depois de um tempo eu ri de mim mesmo porque nada vai funcionar se isso acontecer...
Vou sobrescrever o método getPackageProviders, este método garante que o ViacepProvider seja carregado, então teremos um provider como este, que vai disponibilizar um macro no Http client com o base url setado para o viacep, nos ajudando a não ficar escrevendo toda hora a url que queremos chamar:

<?php

namespace DominusDev\LaravelViacep\Providers;

use DominusDev\LaravelViacep\Actions\QueryZipcode;
use Illuminate\Support\Facades\Http;
use Illuminate\Support\ServiceProvider;

class ViacepProvider extends ServiceProvider
{
    public function boot(): void
    {
        Http::macro('viaCep', function () {
            return Http::baseUrl('https://viacep.com.br/ws/');
        });
    }
}

Enter fullscreen mode Exit fullscreen mode

Nosso TestCase.php ficará desse jeito:

<?php

namespace DominusDev\LaravelViacep\Tests;

use DominusDev\LaravelViacep\Providers\ViacepProvider;
use Orchestra\Testbench\TestCase as BaseTestCase;

class TestCase extends BaseTestCase
{
    protected function getPackageProviders($app): array
    {
        return [
            ViacepProvider::class
        ];
    }
}
Enter fullscreen mode Exit fullscreen mode

Falhando antes de começar!

Agora podemos meter a mão na massa! E já começamos com uma pergunta clássica quando se fala em TDD:

  • Como posso ter um teste falhando se ainda não codei nada?

Inicialmente sei o que preciso. Preciso fazer uma requisição(e vou usar o Http client do Laravel) para a url do via e retornar um status 200, meu primeiro teste poderia ser algo como:

<?php

namespace DominusDev\LaravelViacep\Tests\Unit;

use DominusDev\LaravelViacep\Tests\TestCase;

class QueryZipcodeTest extends TestCase
{
    public function testQueryZipcode()
    {
        $result = new QueryZipcode();
        $this->assertTrue(true);
    }
}
Enter fullscreen mode Exit fullscreen mode

Em seguida podemos executar o teste ./vendor/bin/phpunit e assim falhamos no nosso primeiro teste, pois a classe que idealizei ainda nem existe(sim... estou sendo simplista).

Image description

Passando no primeiro teste

Falhamos no nosso teste e agora? Vamos fazer ele passar da forma mais simples possível, ou seja, criamos nossa classe para consultar cep.

<?php

namespace DominusDev\LaravelViacep\Actions;

class QueryZipcode
{
    public function handle()
    {

    }
}
Enter fullscreen mode Exit fullscreen mode

Executamos os testes de novo e passaremos com sucesso,mas calma ai puritanos... eu sei que o resultado é um assertTrue(true).

Image description

Implementando o código e teste

Criei minha classe e confirmei que os testes estão me ajudando, perfeito. Agora podemos seguir para o próximo passo! Vamos fazer a chamada para a url https://viacep.com.br/ws/01001000/json/ e vou ser bem direto na implementação.

<?php

namespace DominusDev\LaravelViacep\Actions;

use Illuminate\Support\Facades\Http;

class QueryZipcode
{
    public function handle(array $zipcode = [])
    {
        return Http::get('https://viacep.com.br/ws/01001000/json/')->object();
    }
}
Enter fullscreen mode Exit fullscreen mode

Testamos novamente e vai estar tudo ok e sem precisar mudar nosso teste! Ou seja, sucesso!
Aqui temos uma GRANDE pegadinha. Como estamos provavelmente conectado à internet o teste vai passar, só que meu teste vai depender da conexão com a internet tornando meu teste frágil.
Felizmente ao usarmos o Http do Laravel também podemos usar o Http::fake... ahhhhhh mas eu consigo testar isso sem o Laravel! E realmente consegue. Mas qual o sentido?! Vai ter gente que não gosta de framework e blá blá blá, mas sinceramente se uma ferramenta pode te ajudar, então porque não usar? Bem... vou deixar esse assunto polêmico para um próximo artigo.

Usando Facades Mocks

Voltando a questão da chamada Http. Ao realizarmos uma chamada podemos estar sem internet ou até mesmo o serviço em questão estar instável, porém isso fará testes que estão corretos retornarem falsos positivos!

Image description

Para conseguirmos torna nossos testes determinísticos precisamos tirar essa dependência de fatores externos ao código, como a minha conexão ou estado do serviço. Vamos usar 2 recursos do Http:

  • preventStrayRequests()
  • fake()

O preventStrayRequests vai garantir que estou usando apenas chamadas fakes, se eu executar o teste após colocar este método ele vai falhar, e para ser sincero normalmente já coloco este método no setUp do meu TestCase para não ter surpresas. Já o fake,como eu posso dizer... faz exatamente o que diz rsrsrs. Este método permite fazer uma chamada http falsa para a url que eu especificar com o retorno que eu especificar. Nossa classe de teste agora vai estar assim:

<?php

namespace DominusDev\LaravelViacep\Tests\Unit;

use DominusDev\LaravelViacep\Actions\QueryZipcode;
use DominusDev\LaravelViacep\Tests\TestCase;
use Illuminate\Support\Facades\Http;

class QueryZipcodeTest extends TestCase
{
    public function testQueryZipcode()
    {
        Http::preventStrayRequests();
        Http::fake([
            'https://viacep.com.br/ws/01001000/json/' => Http::response([])
        ]);
        $query = new QueryZipcode();
        $result = $query->handle();
        $this->assertIsArray($result);
    }
}
Enter fullscreen mode Exit fullscreen mode

Seguido do teste:
Image description

Bem melhor! Agora podemos seguir para o caminho feliz! Nesse mundo perfeito o teste vai passar e nunca teremos problemas com conexão e o serviço que consumimos nunca ficará offline! Porém o teste ainda não verifica os dados que devem ser verificados, pois os testes passam mas não tem "valor" nenhum no que testamos.

Verificando valores

Agora vamos modificar nossa classe de teste, vamos aproveitar para fazer um data provider e verificar valores que devem retornar de uma consulta de CEP seguindo o caminho feliz de "pesquisei 1 cep e tive o retorno com x dados que preciso". Ao executarmos nosso primeiro caso de teste vamos verificar que vai estar tudo OK. Com isso podemos seguir para o próximo passo... retornar 1 ou vários CEPs!

<?php

namespace DominusDev\LaravelViacep\Tests\Unit;

use DominusDev\LaravelViacep\Actions\QueryZipcode;
use DominusDev\LaravelViacep\Tests\TestCase;
use Illuminate\Support\Facades\Http;
use PHPUnit\Framework\Attributes\DataProvider;

class QueryZipcodeTest extends TestCase
{
    #[DataProvider('zipcodeProvider')]
    public function testQueryZipcode($zipcode, $mockedResponse, $expectedResult)
    {
        Http::preventStrayRequests();
        Http::fake([
            $zipcode => Http::response($mockedResponse),
        ]);
        $query = new QueryZipcode();
        $result = $query->handle();
        $this->assertEquals($result->cep, $expectedResult['cep']);
        $this->assertEquals($result->logradouro, $expectedResult['logradouro']);
        $this->assertEquals($result->ibge, $expectedResult['ibge']);
    }

    public static function zipcodeProvider(): array
    {
        return [
            [
                'https://viacep.com.br/ws/01001000/json/',
                [
                    "cep" => "01001-000",
                    "logradouro" => "Praça da Sé",
                    "complemento" => "lado ímpar",
                    "unidade" => "",
                    "bairro" => "Sé",
                    "localidade" => "São Paulo",
                    "uf" => "SP",
                    "ibge" => "3550308",
                    "gia" => "1004",
                    "ddd" => "11",
                    "siafi" => "7107",
                ],
                [
                    'cep' => '01001-000',
                    'logradouro' => 'Praça da Sé',
                    'ibge' => '3550308',
                ]
            ]
        ];
    }
}
Enter fullscreen mode Exit fullscreen mode

Buscando 1 ou mais CEPS

Já temos nosso primeiro case, nossos testes passam e já criamos nossa estrutura de provider. Nos resta agora implementar nosso outro caso de uso seguindo nosso caminho feliz. Aqui vamos modificar o código, sendo que agora podemos passar uma string de CEPs separados por vírgula. Como estamos fazendo um pacote para o Laravel, não é pecado usar seus recursos, mas lembrando que se quiséssemos algo mais agnóstico usaríamos os recursos nativos do PHP para não ter nenhum tipo de acoplamento indesejado! No final do dia teríamos algo tipo isso no código:

<?php

namespace DominusDev\LaravelViacep\Actions;

use Illuminate\Support\Collection;
use Illuminate\Support\Facades\Http;
use Illuminate\Support\Str;

class QueryZipcode
{
    public function handle(string $zipcode = ''): Collection
    {
        $addresses = collect();
        Str::of($zipcode)->trim()->explode(',')->each(function ($zipcode) use (&$addresses) {
            $addresses->push(Http::viaCep()->get("$zipcode/json")->object());
        });
        return $addresses;
    }
}
Enter fullscreen mode Exit fullscreen mode

Devemos observar que mudamos o retorno da função para um Collection. A cada CEP retornamos o resultado para a Collection, adicionando os dados como um stdClass. Após a iteração retornamos esta Collection como resultado.
Junto com o código mudamos nossos testes, aqui temos a classe de teste(sim... para não prolongar dei uma tapeada no baby step rsrsrsrs). Nosso caminho feliz está ai... podemos pesquisar por 1 ou vários CEPs.

<?php

namespace DominusDev\LaravelViacep\Tests\Unit;

use DominusDev\LaravelViacep\Actions\QueryZipcode;
use DominusDev\LaravelViacep\Tests\TestCase;
use Illuminate\Support\Facades\Http;
use PHPUnit\Framework\Attributes\DataProvider;

class QueryZipcodeTest extends TestCase
{
    #[DataProvider('zipcodeProvider')]
    public function testSearchZipCode($zipcode, $mockedResponse, $expectedResult)
    {
        $url = "https://viacep.com.br/ws/*";
        Http::preventStrayRequests();
        Http::fake([
            $url => Http::sequence($mockedResponse),
        ]);
        $query = new QueryZipcode();
        $result = $query->handle($zipcode);
        $this->assertEquals(count($expectedResult), $result->count());
        $result->each(function ($item, $key) use ($expectedResult) {
            $this->assertEquals($item->cep, $expectedResult[$key]['cep']);
            $this->assertEquals($item->logradouro, $expectedResult[$key]['logradouro']);
            $this->assertEquals($item->ibge, $expectedResult[$key]['ibge']);
        });
    }

    public static function zipcodeProvider(): array
    {
        return [
            'busca 1 cep' => [
                '01001000',
                [
                    [
                        "cep" => "01001-000",
                        "logradouro" => "Praça da Sé",
                        "complemento" => "lado ímpar",
                        "unidade" => "",
                        "bairro" => "Sé",
                        "localidade" => "São Paulo",
                        "uf" => "SP",
                        "ibge" => "3550308",
                        "gia" => "1004",
                        "ddd" => "11",
                        "siafi" => "7107",
                    ],
                ],
                [
                    [
                        'cep' => '01001-000',
                        'logradouro' => 'Praça da Sé',
                        'ibge' => '3550308',
                        "ddd" => "11",
                    ],
                ]
            ],
            'busca 2 cep' => [
                '01001000,28981-546',
                [
                    [
                        "cep" => "01001-000",
                        "logradouro" => "Praça da Sé",
                        "complemento" => "lado ímpar",
                        "unidade" => "",
                        "bairro" => "Sé",
                        "localidade" => "São Paulo",
                        "uf" => "SP",
                        "ibge" => "3550308",
                        "gia" => "1004",
                        "ddd" => "11",
                        "siafi" => "7107",
                    ],
                    [
                        "cep" => "28981-546",
                        "logradouro" => "Rua Senador Pompeu",
                        "complemento" => "",
                        "unidade" => "",
                        "bairro" => "Parque Hotel",
                        "localidade" => "Araruama",
                        "uf" => "RJ",
                        "ibge" => "3300209",
                        "gia" => "",
                        "ddd" => "22",
                        "siafi" => "5803",
                    ]
                ],
                [
                    [
                        'cep' => '01001-000',
                        'logradouro' => 'Praça da Sé',
                        'ibge' => '3550308',
                        "ddd" => "11",
                    ],
                    [
                        'cep' => '28981-546',
                        'logradouro' => 'Rua Senador Pompeu',
                        'ibge' => '3300209',
                        "ddd" => "22",
                    ],
                ]
            ],
        ];
    }
}
Enter fullscreen mode Exit fullscreen mode

Ao testarmos o teste passará. Nosso pacote poderá ser integrado diretamente ao composer via git e etc e ou no próprio packgist!

Conclusão

O resultado desse tutorial será um pacote com um bom teste realizado. Mas não se enganem, os testes até então não garantem que não haverá falhas no pacote e nunca haverá forma de garantir que nenhum software será infalível. Como disse várias vezes seguimos o caminho feliz onde nada falha para não nos prolongarmos. O código vai ficar disponível no github. Espero conseguir escrever mais sobre testes com Laravel em um momento próximo. No mais, torço para que tenham gostado da leitura e que ela seja útil!
Até a próxima!

Top comments (0)