Pressione enter para ver os resultados ou esc para cancelar.

Testes automatizados com Drupal 8

Testes automatizados sempre foram um terror no Drupal e nossa maior vergonha na Taller era a incapacidade de criar testes efetivamente com ele.

CSB46wYUEAArhrC

Fazer um teste unitário é testar uma unidade isolada da aplicação. Porém o Drupal 7 praticamente não tem unidades isoladas. Vindo de uma época procedural, o código é extremamente acoplado e para cada teste é preciso fazer o bootstrap do Drupal para que todas as dependências fiquem satisfeitas.

Ok, ainda podemos testar. A opção padrão era o simpletest, frameworks para testes funcionais e/ou abordagens mais voltadas para o negócio como BDD. O grande problema era que a demora para execução acabava inviabilizando seu uso diário e reduzindo a quantidade de feedback do sistema.

“Quando um teste demora muito para ser executado, ele inevitavelmente vai cair em desuso.”
(Autor Desconhecido :P)

Testes que não te deixam mais produtivo nem devem ser feitos.

Drupal 8 veio com a promessa de enfim executar testes unitários, muito mais leves, e isso deu uma nova vida para um desenvolvimento Drupal com qualidade, saindo do drupalismo e nos alinhando com a comunidade PHP.

Navegando nessa onda da comunidade PHP e do desenvolvimento de software como um todo, o Drupal 8 introduziu no seu core o PHPUnit, que é o framework padrão para testes no mundo PHP, e tem o potencial de trazer ganhos enormes no modo como testamos nossas aplicações. Apesar do nome, o PHPUnit pode fazer muito mais que apenas testes unitários.

No Drupal 8 com PHPUnit temos 3 tipos de testes (sim, temos outros, mas fica pra outra hora):

  1. Testes Unitários
  2. Testes de Integração
  3. Testes Funcionais

Nessa primeira parte do artigo vamos focar em testes unitários.

Testes Unitários

São os testes mais simples que podemos fazer. Usados para testar métodos e classes individuais, esse tipo de teste é super rápido de executar e não precisa de setup do sistema.

Com o teste unitário não se testa se a funcionalidade está se comportando como o esperado mas sim se aquele pequeno pedacinho de código responde como o esperado. Podemos testar se o retorno de um método responde ao que queremos dado um devido input, podemos testar se certos métodos são chamados e se eles são chamados apenas uma vez ou múltiplas.

A execução desses testes garantem o constante feedback de que essas pequenas partes continuam funcionando.

Para começar a brincar com teste unitários é preciso entender um conceito tão simples quanto importante: Injeção de Dependências.

Injeção de Dependências

Também conhecida como inversão de controle, não é nada mais que sua classe não se preocupar em instanciar os objetos que vai precisar durante a execução. É, só isso. As dependências são passadas no construtor.

Class MyClass {
  public function __construct(ConfigFactoryInterface $config_factory) {
    $this->config = $config_factory->get(‘myconfig.settings');
  }
}

Então quando instanciar a classe:

$config = new MyClass($config_factory);

Esse simples conceito, que se discute até hoje se é um padrão ou não, resolve o grave problema da testabilidade no Drupal.

gotcha

Assim fica simples criarmos menores unidades independentes no Drupal, e isso significa que podemos criar estruturas falsas (dublês) para simular o comportamento do framework sem precisar realmente carrega-lo ou sequer instala-lo para isso. Mas já chegaremos lá.

Primeiro, vamos criar nosso primeiro teste:

TicketManager.php

<?php

namespace Drupal\taller_ticket; 
class TicketManager { 
 public function setStatus($status) { 
  $this->status = $status;
 }

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

}

TicketManagerTest.php

<?php 

namespace Drupal\Tests\taller_ticket\Unit; 

use Drupal\Tests\UnitTestCase; 
use Drupal\taller_ticket\TicketManager;

/** 
* 
* @group ticket 
* @covers TicketManager
*/ 
class TicketManagerTest extends UnitTestCase { 

 protected function setUp() { 
  parent::setUp(); 
} 

 public function testStatus() { 
  $ticket = new TicketManager(); 
  $ticket->setStatus('OPEN');
  $this->assertEquals('OPEN',  $ticket->getStatus());
 }
}

 

Para executar os testes temos duas abordagens:

A mais simples é chamar o phpunit passando como parâmetro o arquivo de testes:

$ phpunit modules/custom/mymodule/tests/Unit/MyUnitTest.php

Ou configurar o phpunit.xml. Na raiz do projeto:

$ cp core/phpunit.xml.dist ./phpunit.xml

Adicionar um test suite para os módulos custom:

./modules/custom/*/tests/*/

Você pode aproveitar e já configurar os dados do simpletest, que no momento é só sua conexão com o banco, pois vamos usar isso no futuro.

$ phpunit -c phpunit.xml --testsuite custom-unit

Dublês de Testes

Esses dados falsos são de extrema importância quando o assunto são testes unitários. Eles permitem que se possa manter o foco na unidade que se vai testar, sem precisar carregar várias dependências. A injeção de dependências ajuda a manter o código mais unitário, permitindo que se usem os dublês no lugar dos objetos reais. Muitas vezes nem precisamos de comportamento nesses dublês, outras precisamos que eles nos respondam um valor específico. No Brasil, é comum que se chame tudo de mocks. Mas nem tudo são mocks.

Existem alguns tipos e entre os mais conhecidos estão:

  • Dummy: dublês que não são usados
  • Stub: dublês com valor de retorno predefinidos
  • Mock: dublês com chamada de métodos esperada
  • Spy: no fim do processo, quantas vezes o método foi chamado?

Os mais usados e importantes para o primeiro momento são os Mocks e Stubs.

Mocks e Stubs

$mytestobject = $this->getMock(‘MyClassInterface’)
// isso é um MOCK
$mock->expects($this->at(0))
  ->method(’someMethod')
  ->with(‘param’)
  // isso já é um STUB
  ->will(‘returnValue’)

Resumindo, passamos dados de mentirinha para a class para saber se ela está se comportando como esperado ou não.

// Set up default test configuration Mock object.
$this->configFactory = $this->getConfigFactoryStub(array(
  'simplesamlphp_auth.settings' => array(
  'auth_source' => 'default-sp',
),
));

Ele é definido no core pela classe UnitTest, que se estende diretamente do PHPUnit. Ou seja, sem drupalismo.

Testes de Integração

No Drupal são chamado de KernelTests. Eles tem um comportamento interessante pois carregam o Drupal apenas na memória. Isso faz com que eles sejam mais lentos que os testes unitários mas ainda assim muito mais rápidos que testes funcionais.

Com um bootstrap completo do Drupal carregado, pode-se testar se os módulos estão integrando bem com o core e com outros módulos, custom ou contrib.

$entity = $this->container->get('entity_type.manager')
  ->getStorage(‘my_entity')
  ->create(array(
    'title' => $title,
    'message' => $message,
    'file' => $file->id(),
));

$entity->save();

$actual_tickets = entity_load_multiple_by_properties('ticket', ['title' => $title]);
$expected_length = 1;
$this->assertCount($expected_length, $actual_tickets);

Neste exemplo é criada uma entidade chamada my_entity, carregada na memória e testa-se a existência de apenas uma.

Nessa introdução a testes automatizados com Drupal foram mostrados apenas os testes mais rápidos e que tem maior diferença do padrão do Drupal 7. Apenas com esses testes unitários e de integração já podemos ter um Drupal muito mais seguro e robusto, melhorar nossa arquitetura e garantir a todo deploy que as peças básicas do software ainda funcionam.


***
Recado da Taller:
Criamos o Programa de Otimização da Gestão Ágil para quem quiser levar as práticas de eficiência de trabalho para dentro da sua empresa.

Conheça a Programa →

***

  • Júlio Hidemi Kamizato

    Muito bom Rafael!

    Só pra complementar, uma outra opção para rodar o teste, seria com o comando:

    php core/scripts/run-tests.sh --file NOME_DO_ARQUIVO

    Neste caso não precisaria do phpunit.xml para carregar o bootstrap do Drupal.
    Mas percebi uma coisa interessante, para este caso que eu citei, teria que colocar o arquivo dentro do mymodule/src e o teste dentro do mymodule/tests/src/Unit.

    Aguardo ansioso para ver como vocês pensaram no teste funcional. Estou pendendo a utilizar outras ferramentas fora do Drupal, justamente pela lentidão, mas minha preocupação é com o banco de dados :/

    Abraço!!