← Voltar ao blog

Testes de integração no Magento 2: setup, fixtures e dicas

Rodrigo Biazzi

Testes unitários checam uma classe isolada. Testes de integração checam se o seu código funciona com o Magento de verdade por baixo: injeção de dependência, plugins, configuração, banco de dados e indexers. São mais lentos e exigem mais setup, mas pegam os bugs que os unitários não pegam, como um plugin que nunca dispara por causa de um erro de digitação no di.xml ou um repository que salva um atributo na store errada.

Este artigo cobre o setup do framework de testes de integração do Magento 2.4.8, como escrever testes com fixtures e como manter os tempos de execução razoáveis.

Como o framework funciona

O framework de integração fica em dev/tests/integration. Quando você o executa, ele instala um Magento completo num banco separado com as configurações que você fornece, e depois sobe a aplicação para cada teste. Os testes recebem um object manager real por meio do Magento\TestFramework\Helper\Bootstrap, e as fixtures criam os dados de que precisam.

Por causa dessa etapa de instalação, o banco de testes precisa ser separado do seu banco de desenvolvimento. O framework cria tabelas, insere dados e, dependendo das configurações, apaga tudo e reinstala.

Setup

Um banco dedicado

Crie um banco vazio para os testes. Num ambiente Docker como o do artigo sobre o Warden, crie no mesmo container de banco:

CREATE DATABASE magento_integration_tests;
GRANT ALL ON magento_integration_tests.* TO 'magento'@'%';

install-config-mysql.php

Copie o template e edite:

cd dev/tests/integration
cp etc/install-config-mysql.php.dist etc/install-config-mysql.php
cp phpunit.xml.dist phpunit.xml

O arquivo de config retorna as mesmas opções que você passaria ao setup:install. Uma versão para uma stack de containers com OpenSearch e RabbitMQ:

<?php
return [
    'db-host' => 'db',
    'db-user' => 'magento',
    'db-password' => 'magento',
    'db-name' => 'magento_integration_tests',
    'db-prefix' => '',
    'backend-frontname' => 'backend',
    'search-engine' => 'opensearch',
    'opensearch-host' => 'opensearch',
    'opensearch-port' => 9200,
    'admin-user' => \Magento\TestFramework\Bootstrap::ADMIN_NAME,
    'admin-password' => \Magento\TestFramework\Bootstrap::ADMIN_PASSWORD,
    'admin-email' => \Magento\TestFramework\Bootstrap::ADMIN_EMAIL,
    'admin-firstname' => \Magento\TestFramework\Bootstrap::ADMIN_FIRSTNAME,
    'admin-lastname' => \Magento\TestFramework\Bootstrap::ADMIN_LASTNAME,
    'amqp-host' => 'rabbitmq',
    'amqp-port' => '5672',
    'amqp-user' => 'guest',
    'amqp-password' => 'guest',
];

Se o projeto não usa RabbitMQ, remova as chaves AMQP. Use um prefixo de índice do OpenSearch diferente do de desenvolvimento se os dois compartilham o mesmo serviço, para que os testes não sobrescrevam seus índices de desenvolvimento.

phpunit.xml

Na sua cópia do phpunit.xml, duas configurações importam mais:

  • TESTS_CLEANUP: quando ativado, o framework reinstala o Magento a cada execução. Desative localmente para a instalação ser reaproveitada e as execuções começarem em segundos em vez de minutos. Ative no CI ou depois de mudar módulos ou schema.
  • Os diretórios da suíte de testes: aponte para as suas próprias pastas Test/Integration em vez da suíte inteira do core.
<testsuites>
    <testsuite name="Project">
        <directory>../../../app/code/Vendor/*/Test/Integration</directory>
    </testsuite>
</testsuites>
<php>
    <const name="TESTS_CLEANUP" value="disabled"/>
</php>

O que merece um teste de integração

Testes de integração custam mais para escrever e para rodar, então gaste-os onde o framework faz parte do risco:

  • Plugins e observers: o plugin realmente roda quando o método do core é chamado pelo repository?
  • Repositories e resource models: o atributo customizado sobrevive a um save e load numa store view diferente?
  • Caminhos e escopos de configuração, incluindo os defaults do config.xml
  • Extension attributes, que dependem de declarações XML e plugins trabalhando juntos
  • Totals collectors, fluxos de quote e pedido, onde vários módulos interagem

Se um teste não precisa de nada disso, provavelmente é um teste unitário disfarçado e vai rodar bem mais rápido como tal.

Rodando os testes

Rode o PHPUnit a partir da pasta de integração para que os caminhos relativos da configuração sejam resolvidos:

cd dev/tests/integration
../../../vendor/bin/phpunit -c phpunit.xml
../../../vendor/bin/phpunit -c phpunit.xml --filter FreeShippingThresholdTest

A primeira execução instala o Magento e pode demorar. O código gerado e os caches da instalação de teste ficam em dev/tests/integration/tmp. Se algo se comportar de forma estranha depois de mudar o di.xml ou adicionar um módulo, apague a pasta sandbox em tmp e rode de novo com o cleanup ativado.

Escrevendo um teste com annotations

Os testes de integração ficam em Test/Integration dentro do módulo. Este checa um serviço de frete grátis contra a configuração real, usando o escopo de store e a fiação real de DI:

<?php
declare(strict_types=1);

namespace Vendor\Shipping\Test\Integration\Model;

use Magento\TestFramework\Helper\Bootstrap;
use PHPUnit\Framework\TestCase;
use Vendor\Shipping\Model\FreeShippingThreshold;

class FreeShippingThresholdTest extends TestCase
{
    private FreeShippingThreshold $model;

    protected function setUp(): void
    {
        $this->model = Bootstrap::getObjectManager()->get(FreeShippingThreshold::class);
    }

    /**
     * @magentoConfigFixture current_store vendor_shipping/general/free_threshold 200
     */
    public function testRemainingUsesStoreConfig(): void
    {
        $this->assertSame(50.0, $this->model->getRemaining(150.0));
    }

    /**
     * @magentoConfigFixture current_store vendor_shipping/general/free_threshold 0
     */
    public function testDisabledWhenZero(): void
    {
        $this->assertSame(0.0, $this->model->getRemaining(150.0));
    }
}

A config fixture grava o valor durante o teste e o restaura depois. current_store se refere à store padrão da instalação de teste. A mesma classe tem um teste unitário puro no artigo sobre testes unitários com PHPUnit. O teste de integração dá mais confiança de que o caminho de config, o escopo e o DI estão ligados corretamente.

Data fixtures com attributes

Versões recentes do 2.4 adicionaram PHP attributes para fixtures, e os novos testes do core os usam. Data fixtures são classes que criam entidades por service contracts e as limpam depois. Um teste que precisa de um produto:

<?php
declare(strict_types=1);

namespace Vendor\Catalog\Test\Integration\Model;

use Magento\Catalog\Api\ProductRepositoryInterface;
use Magento\Catalog\Test\Fixture\Product as ProductFixture;
use Magento\TestFramework\Fixture\DataFixture;
use Magento\TestFramework\Fixture\DataFixtureStorageManager;
use Magento\TestFramework\Fixture\DbIsolation;
use Magento\TestFramework\Helper\Bootstrap;
use PHPUnit\Framework\TestCase;

class ProductLabelTest extends TestCase
{
    #[
        DbIsolation(true),
        DataFixture(ProductFixture::class, ['sku' => 'label-test', 'price' => 49.9], 'product')
    ]
    public function testProductIsSavedWithPrice(): void
    {
        $fixture = DataFixtureStorageManager::getStorage()->get('product');
        $repository = Bootstrap::getObjectManager()->get(ProductRepositoryInterface::class);

        $product = $repository->get($fixture->getSku());

        $this->assertSame(49.9, (float) $product->getPrice());
    }
}

O terceiro argumento do DataFixture é um alias, usado para ler a entidade criada a partir do storage de fixtures. Os módulos do core trazem classes de fixture nas pastas Test/Fixture para produtos, categorias, customers, carts e pedidos. Olhe essas antes de escrever as suas, e quando escrever, implemente a mesma interface de fixture revertible para a limpeza acontecer automaticamente.

Testes mais antigos usam fixtures baseadas em arquivo com @magentoDataFixture Magento/Catalog/_files/product_simple.php. Elas ainda funcionam, mas são mais difíceis de parametrizar. Mantenha um estilo só por classe de teste.

Isolamento

Existem dois níveis de isolamento, disponíveis tanto como annotations quanto como attributes:

  • DB isolation roda o teste dentro de uma transaction e faz rollback. Os dados criados pelo teste somem. Use por padrão para tudo que grava.
  • App isolation reseta o estado da aplicação depois do teste, incluindo as instâncias do object manager. É necessário quando um teste muda singletons compartilhados, a store atual ou a area. É caro, então use só quando o teste provar que precisa.

Algum código não consegue rodar dentro de uma transaction, por exemplo código que dispara DDL ou depende de um callback de commit, como certas operações de indexer. Esses testes precisam de DB isolation desativado e limpeza explícita.

Areas e controllers

Por padrão os testes rodam na area global. Use @magentoAppArea frontend ou adminhtml quando o código depende de DI ou plugins específicos da area. Para testes de controller, estenda Magento\TestFramework\TestCase\AbstractController, que oferece o dispatch() e helpers para fazer asserts na resposta e nos redirects. Para controllers do admin, o AbstractBackendController faz login de um usuário admin. Endpoints GraphQL e REST têm o próprio framework de testes funcionais de API em dev/tests/api-functional, que precisa de um servidor web rodando.

Mantendo rápido

  • Rode só as suas suítes de teste. A suíte completa do core leva horas.
  • Mantenha o TESTS_CLEANUP desativado localmente e ative quando o schema mudar.
  • Prefira DB isolation a app isolation.
  • Teste a lógica em testes unitários e use os de integração para a fiação e o banco. Uma boa divisão é muitos testes unitários e um número menor de testes de integração focados.
  • No CI, rode os testes de integração num job separado com containers de serviço de banco e OpenSearch, para um job lento não bloquear as checagens rápidas. Um layout de pipeline está no artigo sobre GitHub Actions.

FAQ

Por que a primeira execução dos testes de integração demora tanto?

O framework instala um Magento completo no banco de testes antes de rodar qualquer coisa. Com o TESTS_CLEANUP desativado, as execuções seguintes reaproveitam essa instalação e começam bem mais rápido.

Os testes de integração podem usar meu banco de desenvolvimento?

Não devem. O framework cria e modifica dados à vontade e pode reinstalar o schema. Sempre aponte o install-config-mysql.php para um banco separado e vazio.

Qual a diferença entre DbIsolation e AppIsolation?

O DbIsolation envolve o teste numa transaction que sofre rollback, então as mudanças nos dados somem. O AppIsolation reseta o estado da aplicação, como o object manager e o registry, depois do teste. Use AppIsolation só quando necessário, já que é mais lento.

Devo usar annotations ou PHP attributes para fixtures?

As versões recentes do 2.4 suportam PHP attributes como DataFixture e DbIsolation, e os novos testes do core os usam. As annotations ainda funcionam. Escolha um estilo por classe de teste e mantenha a consistência.

Atendo como freelancer em Magento e Adobe Commerce, incluindo suítes de testes para módulos customizados. Se você precisa de ajuda com isso, fale comigo.

Artigos relacionados