Testes
1Apresentação
Para evitar regressões durante o desenvolvimento, é aconselhável escrever testes automatizados. Existem vários frameworks de teste em PHP, sendo o mais usado o PHPUnit.
Você pode usar o PHPUnit diretamente para fazer testes unitários dos seus objetos. Pode ser necessário então criar um ambiente especial para executar esses testes.
O Temma facilita a escrita de testes de integração. Um teste de integração verifica se a saída de uma ação está correta, de acordo com os parâmetros fornecidos como entrada.
2Instalação do PHPUnit
Existem várias formas de instalar o PHPUnit (veja a documentação). Aqui, vamos baixar o arquivo PHAR, copiá-lo para o diretório bin/ do projeto e torná-lo executável:
$ wget https://phar.phpunit.de/phpunit-10.phar -O bin/phpunit
$ chmod +x bin/phpunit
Você pode verificar se tudo correu bem digitando o seguinte comando:
$ bin/phpunit --version
PHPUnit 10.5.2 by Sebastian Bergmann and contributors.
3Como funciona
O objeto \Temma\Web\Test pode ser usado para lançar requisições que executarão diretamente as ações dos controladores solicitados, sem passar por um servidor HTTP. Dependendo do tipo de requisição, vários tipos de retorno são possíveis:
- O stream de saída gerado pela visão. Por padrão, será o feed HTML gerado pela visão Smarty, mas também pode ser um stream JSON, um arquivo CSV, um feed RSS…
- Variáveis de template definidas pelo controlador e pelos plugins.
- Um objeto do tipo \Temma\Web\Response, que recupera informações precisas sobre a execução (redirecionamento, código de erro HTTP, visão, template).
- O componente de injeção de dependências (o "loader"), que contém todos os objetos instanciados pelo Temma e pelo código da aplicação.
Na maioria das vezes, as variáveis de template são recuperadas para verificar se o código da aplicação funcionou como esperado.
Usar o stream de saída pode ser útil para verificar a não regressão de elementos-chave de uma página HTML.
O objeto de resposta pode ser necessário para verificações avançadas.
Por fim, o componente de injeção de dependências oferece controle total sobre o resultado da execução da consulta.
4Controlador de exemplo
Vamos imaginar um controlador usado para exibir uma lista de artigos e seu conteúdo (arquivo controllers/Article.php):
/** Controlador de artigos. */
class Articles extends \Temma\Web\Controller {
/** Exibe a lista de artigos. */
public function list() {
$this['articles'] = $this->_loader->ArticleDao->getList();
}
/** Exibe o conteúdo de um artigo. */
public function show(int $articleId) {
$this['article'] = $this->_loader->ArticleDao->get($articleId);
if (!$this['article'])
$this->_redirect('/article/list');
}
}
Para a ação list, temos o seguinte template (arquivo templates/articles/list.tpl):
<html>
<body>
<h1>Articles</h1>
<ul>
{foreach $articles as $article}
<li>{$article.title|escape}</li>
{/foreach}
</ul>
</body>
Para a ação show, temos o seguinte template (arquivo templates/articles/show.tpl):
<html>
<body>
<h1>{$article.title|escape}</h1>
{$article.html}
</body>
5Criando um teste
Vamos criar um objeto contendo dois testes de integração (um teste por ação do controlador) no arquivo tests/ArticlesTest.php:
<?php
class ArticlesTest extends \PHPUnit\Framework\TestCase {
/** Objeto de gerenciamento de testes do Temma. */
private \Temma\Web\Test $_test;
/** Inicialização. */
public function setUp() : void {
$this->_test = new \Temma\Web\Test();
}
/** Teste da ação 'list'. */
public function testList() {
$data = $this->_test->execData('/articles/list');
$this->assertIsArray($data['articles'] ?? null);
$this->assertNotEmpty($data['articles']);
}
/** Teste da ação 'show'. */
public function testShow() {
$data = $this->_test->execData('/articles/show/1');
$this->assertEquals(1, ($data['article']['id'] ?? null));
}
}
- Linha 3: o nome do objeto deve ter o sufixo Test, e ele deve herdar do objeto \PHPUnit\Framework\TestCase.
- Linhas 8 a 10: o método setUp() é chamado na inicialização do objeto, antes da execução dos testes. Aqui, instanciamos o objeto de teste fornecido pelo Temma, que será usado mais adiante.
-
Linhas 12 a 16: testa a ação list.
- Linha 13: lançamos uma consulta na URL /articles/list, que normalmente deve exibir a lista de artigos, e recuperamos um array contendo todas as variáveis de template.
- Linha 14: verificamos que a variável de template articles existe e que é um array.
- Linha 15: verificamos que essa variável não está vazia.
-
Linhas 18 a 21: testa a ação show.
- Linha 19: lançamos uma consulta na URL /articles/show/1, que normalmente deve exibir o conteúdo de um artigo, e recuperamos um array contendo todas as variáveis de template.
- Linha 20: verificamos que a variável de template article existe, que ela contém uma chave id, e que o valor associado a essa chave é 1.
6Execução de testes na linha de comando
Para executar todos os testes escritos no diretório tests/, basta executar o seguinte comando:
$ bin/phpunit --bootstrap tests/autoload.php tests
Em troca, você deve obter uma exibição parecida com esta:
PHPUnit 10.5.2 by Sebastian Bergmann and contributors.
Runtime: PHP 8.1.2-1ubuntu2.14
.. 2 / 2 (100%)
Time: 00:00.003, Memory: 22.57 MB
OK (2 tests, 5 assertions)
Também é possível executar um único teste específico:
$ bin/phpunit --bootstrap tests/autoload.php tests/ArticlesTest.php
7Chamando URLs
Quatro métodos estão disponíveis para iniciar a execução de testes:
- execOutput() retorna o stream de saída da visão.
- execData() retorna as variáveis de template ou a string de redirecionamento.
- execResponse() retorna um objeto \Temma\Web\Response.
- execLoader() retorna o componente de injeção de dependências criado para executar a requisição.
Esses métodos podem receber, todos, quatro parâmetros:
- string $url: (obrigatório) URL a ser chamada, começando com uma barra (/).
- string $httpMethod: (opcional) O método HTTP da requisição. Por padrão, é o método GET.
- ?array $data: (opcional) Array associativo contendo os parâmetros GET ou POST a serem transmitidos. O tipo (GET ou POST) é determinado pelo parâmetro $httpMethod.
- ?array $cookies: (opcional) Array associativo contendo os cookies a serem transmitidos.
7.1Obtendo o stream de saída
O método execOutput() é usado para recuperar o stream de saída gerado pela visão. Esse método pode retornar uma string vazia.
Exemplo:
$html = $this->_test->execOutput('/articles/show/1');
// verificação simples
$this->assertStringContainsString('<h1>', $html);
// verificação com expressão regular
$this->assertMatchesRegularExpression('/<h1>.+<\/h1>/', $html);
7.2Obtendo dados
Vimos acima o método execData(), que retorna as variáveis de template definidas pelo controlador e pelos plugins.
Na verdade, esse método pode retornar três tipos diferentes de dados:
- Um array associativo, contendo as variáveis de template.
- Uma string, se um redirecionamento tiver sido definido. Nesse caso, a URL de redirecionamento é retornada.
- O valor null, se a execução tiver sido interrompida.
Exemplo:
$data = $this->_test->execData('/articles/show/1');
// não deve ser null
$this->assertNotNull($data, "Stopped processing.");
// se for um redirecionamento, o artigo não existe
$this->assertIsNotString($data, "Unknown article.");
// verifica o identificador do artigo
$this->assertEquals(1, ($data['article']['id'] ?? null));
7.3Obtendo a resposta
O método execResponse() recupera o objeto \Temma\Web\Response criado durante a execução da consulta. Esse objeto oferece os seguintes getters:
- getRedirection(): retorna a string de redirecionamento, ou null se nenhum redirecionamento tiver sido solicitado.
- getRedirectionCode(): retorna o código de redirecionamento (301 ou 302).
- getHttpError(): retorna o código de erro HTTP, ou null.
- getHttpCode(): retorna o código HTTP da resposta (200 por padrão).
- getView(): retorna o nome da visão, ou null se não estiver definido.
- getTemplatePrefix(): retorna o prefixo adicionado ao início dos caminhos de template, ou null.
- getTemplate(): retorna o caminho do template usado, ou null.
- getHeaders(): retorna a lista de cabeçalhos HTTP definidos.
- getData(): retorna um array associativo contendo as variáveis de template definidas pelo controlador e pelos plugins.
Também é possível acessar diretamente uma variável de template usando a sintaxe de array.
Exemplo:
$response = $this->_test->execResponse('/articles/show/1');
// se for um redirecionamento, o artigo não existe
$redir = $response->getRedirection();
$this->assertNull($redir, "Unknown article.");
// se o código HTTP não for 200, há um erro
$code = $response->getHttpCode();
$this->assertEquals(200, $code, "Processing error.");
// a visão deve ser a visão Smarty
$view = $response->getView();
$this->assertEquals('\Temma\Views\Smarty', $view, "Incorrect view.");
// devemos recuperar a variável de template contendo o artigo
$this->assertEquals(1, ($response['article']['id'] ?? null));
7.4Usando o componente de injeção de dependências
Cada vez que uma requisição é executada, um componente de injeção de dependências é criado, contendo os objetos instanciados pelo Temma, assim como os usados pelo código da sua aplicação (se você usou o componente).
O método execLoader() retorna o objeto \Temma\Base\Loader criado durante a execução da consulta. Esse componente contém, no mínimo, os seguintes objetos:
- config: o objeto de configuração (tipo \Temma\Web\Config) gerado a partir do arquivo de configuração etc/temma.php.
- request: o objeto de requisição (tipo \Temma\Web\Request) gerado a partir da URL solicitada.
- response: o objeto de resposta (tipo \Temma\Web\Response), descrito na documentação do método execResponse() acima.
- session: o objeto de gerenciamento de sessão (tipo \Temma\Base\Session), que em particular recupera o identificador de sessão, armazenado como cookie.
7.5Parâmetros GET e POST
Para enviar parâmetros GET ou POST, especifique o método a ser usado como segundo parâmetro, e passe um array associativo contendo os parâmetros como terceiro parâmetro.
Exemplo:
$html = $this->_test->execOutput('/articles/create', 'POST', [
'title' => "New article",
'html' => "<p>blah blah blah</p>",
]);
8Configuração
8.1Definindo o arquivo de configuração
Por padrão, o Temma encontra o caminho do arquivo de configuração etc/temma.php. Isso permite usar a mesma configuração dos seus ambientes de desenvolvimento (mesmo banco de dados, mesmos plugins, etc.).
Mas às vezes pode ser desejável usar um arquivo de configuração diferente, por exemplo para se conectar
a um banco de dados de testes específico, ou para ativar/desativar determinados plugins.
Nesse caso, dois parâmetros devem ser fornecidos na criação do objeto \Temma\Web\Test:
- ?string $appPath: caminho para a raiz do projeto.
- ?string $configPath: caminho para o arquivo de configuração.
Exemplo:
$appPath = '/opt/my_projet';
$configPath = "$appPath/etc/temma-test.php";
$this->_test = new \Temma\Web\Test($appPath, $configPath);
8.2Definindo o componente de injeção de dependências
Ao testar, você pode querer "mockar" alguns objetos, ou seja, forçar o uso de um objeto substituto quando o código da aplicação quiser chamar um determinado objeto. Isso pode ser útil para evitar que certos processos sejam executados, como a conexão a um serviço externo.
Nesse caso, você precisa preparar um componente de injeção de dependências ao qual terá fornecido o objeto substituto, dando a ele o nome sob o qual o objeto original é chamado. Em seguida, o componente deve ser fornecido como parâmetro ao construtor do objeto \Temma\Web\Test (terceiro parâmetro, ou parâmetro nomeado loader).
Exemplo:
// objeto "ArticleDao" falso
class MockArticleDao {
/** Retorna uma lista falsa de artigos. */
public function getList() {
return [
['id' => 1, 'title' => 'Title 1'],
['id' => 2, 'title' => 'Title 2'],
['id' => 3, 'title' => 'Title 3'],
];
}
/** Retorna um artigo falso. */
public function get(int $id) {
return [
'id' => $id,
'title' => "Title $id",
'html' => "<p>blah blah blah</p>",
];
}
}
// criação do objeto loader
$loader = new \Temma\Base\Loader([
'ArticleDao' => new MockArticleDao(),
]);
// realiza um teste usando esse loader
$this->_test = new \Temma\Web\Test(loader: $loader);
Observe que é possível sobrescrever o componente de injeção de dependências no arquivo etc/temma.php, usando a diretiva loader (veja a documentação de configuração).
Nesse caso, lembre-se de usar o objeto definido na configuração, ou outro objeto sobrescrito que se comporte de forma semelhante quando usado pelo código da aplicação.
9Gerenciamento de usuários
9.1Sessões
As sessões de usuário são criadas automaticamente pelo Temma, por meio do registro de um cookie de sessão no navegador. Para testes automatizados, se você quiser testar uma sequência de páginas que exigem uma sessão, será necessário recuperar o identificador de sessão gerado na chamada da primeira página, e depois repassá-lo às páginas seguintes.
$test = new \Temma\Web\Test();
// chama a primeira página
$loader = $test->execLoader('/page1');
// obtém o ID de sessão
$sessionId = $loader->session->getSessionId();
// cria o array de cookies
$cookies = ['TemmaSession' => $sessionId];
// chama as próximas páginas
$data = $test->execData('/page2', 'GET', null, $cookies);
...
O nome do cookie de sessão ("TemmaSession" no exemplo acima) deve ser o mesmo especificado na variável sessionName do arquivo de configuração etc/temma.php (veja a documentação de configuração).
9.2Autenticação de usuário para testes
Seu site pode ter páginas acessíveis apenas a usuários autenticados. Para isso, você pode usar o plugin/controlador Auth e o atributo Auth fornecidos pelo Temma.
Nesse caso, primeiro autentique um usuário, depois repasse o cookie de sessão de página em página.
$appPath = '/opt/my_projet';
$configPath = "$appPath/etc/temma-test.php";
$test = new \Temma\Web\Test($appPath, $configPath);
// autenticação
$loader = $test->execLoader('/auth/authentication', 'POST', [
'email' => $email,
]);
$token = $loader->response['token'];
$sessionId = $loader->session->getSessionId();
// validação do token
$cookies = ['TemmaSession' => $sessionId];
$loader = $test->execLoader("/auth/check/$token", 'GET', null, $cookies);
// testa uma página acessível apenas a usuários autenticados
$html = $test->execOutput("/account", 'GET', null, $cookies);
$this->assertStringContainsString('My account', $html);
Para que essa autenticação funcione corretamente, o arquivo de configuração deve conter a diretiva robotCheckDisabled. Também é recomendável desativar o envio de mensagens de conexão:
<?php
return [
'x-security' => [
'auth' => [
'robotCheckDisabled' => true
]
],
'x-email' => [
'disabled' => true
]
];