Desenvolvimento:Testes de unidade

Fonte: MoodleDocs

O propósito dos testes de unidade é avaliar a parte individual de um programa (funções e métodos de classe) para ter certeza de que cada elemento individualmente faça a coisa certa. Testes de unidade podem ser um dos primeiros passos em um processo de controle de qualidade para desenvolver ou aprimorar o código do Moodle. Os próximos passos envolverão outras formas de teste para assegurar que esttas diferentes partes trabalharão juntas apropriadamente.

O framework dos testes de unidade é baseado no framework SimpleTest.

Rodando os testes de unidade no Moodle.

Rodando os testes básicos

  1. Faça login como administrador.
  1. Administração ► Desenvolvimento ► Unidades de teste (moodle >= 2.0, Administração ► Relatórios ► Testes de Unidade Moodle <= 1.9)
  1. Clique em Relatórios próximo ao rodapé da página.
  1. Cliquem em Rodar teste e aguarde.

Isto encontra todos os testes no Moodle e os roda. Você pode rodar um subconjunto de testes colocando um caminho (por exemplo questão/tipo) na opção 'Apenas rode testes em'. Da mesma forma, se o teste falhar, você tem alguns links na mensagem de erro para facilitar rodar novamente apenas os testes necessários.

Escrevendo novos testes

Como um exemplo, suponha que queremos escrever alguns testes para a classe string_manager em mod/quiz/editlib.php.

Onde colocar os testes

O relatório dos testes de unidade encontra testes procurando por arquivos chamados 'test....php' dentro de pastas chamadas 'simpletest'.

Então, para nosso exemplo, podemos criar algo como mod/quiz/simpletest/testeditlib.php. O esqueleto de nosso arquivo deve se parecer com:

<?php
/**
* Unit tests for (some of) mod/quiz/editlib.php.
*
* @license http://www.gnu.org/copyleft/gpl.html GNU Public License
* @package question
*/

 

if (!defined('MOODLE_INTERNAL')) {
   die('Direct access to this script is forbidden.'); //  It must be included from a Moodle page
}

 

// Make sure the code being tested is accessible.
require_once($CFG->dirroot . '/mod/quiz/editlib.php'); // Include the code to test

 

/** This class contains the test cases for the functions in editlib.php. */
class quiz_editlib_test extends UnitTestCase {
   function test_something() {
       // Do the test here.
   }

 

   // ... more test methods.
}
?>


Então, você tem uma classe chamada something_test, e nessa classe você tem vários métodos chamados test_something. Normalmente, você tem um método de teste para cada particularidade a ser testada, e você deve nomear a função para descrever o que está sendo testado – sem utilizar um nome grande demais!

Função teste

Uma função teste se parece com isso, normalmente:

function test_move_question_up() {
   // Setup fixture

 

   // Exercise SUT
   $newlayout = quiz_move_question_up('1,2,0', 2);

 

   // Validate outcome
   $this->assertEqual($newlayout, '2,1,0');

 

   // Teardown fixture

 

}

Esse é o padrão de teste em quatro fases. Os comentários utilizam muito jargão de teste. A fixação é o pano de fundo que precisa ser configurado antes de rodar o teste. SUT é a sigla para 'situation under test', 'situação em teste'. É onde você chama a função ou método a ser testado. Assim você pode checar se a função se comporta corretamente.

Finalmente, você precisa limpar a fixação que criou. Com sorte, não haverá nada a se fazer aqui.

Neste exemplo simples, não há instalação ou subdivisão a fazer. Nós apenas chamamos a função que testamos com um exemplo de entrada, e checamos o valor retornado esperado.


SetUp (Instalação) compartilhado e métodos de tearDown(subdivisão)

Se todos os seus casos de teste relatam para a mesma área de código, entao todos eles podem precisar que uma mesma parte seja fixada. Por exemplo, todos os testes em lib/simpletest/teststringmanager.php precisam de uma instância de string_manager class para testar.

Para evitar código duplicado, você pode sobrepor um método chamado setUp() que aponta os dados do teste. Se presente, este método irá ser chamado antes de cada método de teste. Você pode escrever um método tearDown() correspondente se existe alguma limpeza que precise ser feita após cada teste rodar. Por exemplo, em lib/simpletest/teststringmanager.php existem métodos setUp e tearDown que fazem algo como:

public function setUp() {
   // ...
   $this->stringmanager = new string_manager(...);
}

 

public function tearDown() {
   $this->stringmanager = null;
}

Então, cada teste pode usar $this->stringmanager sem precisar se preocupar com detalhes de como isso é configurado.

Informações adicionais

O documento SimpleTest se encontra em: [1].

Alterações para que seu código existente funcione com o teste de unidade

O ponto principal da unidade de teste é testar separadamente cada parte da funcionalidade. Você pode fazer isso apenas se for possível isolar a função e chamá-la individualmente, talvez após configurar algumas outras coisas.

Portanto, é bom se você puder escrever seu código para depender o mínimo de outras configurações.

Incluindo caminhos -

Inclusões como

require_once('../../config.php'); // Won't work.

não funcionam. Em vez disso, a opção mais robusta é

require_once(dirname(__FILE__) . '/../../config.php'); // Do this.

Acesso a variáveis globais

Como seu código foi incluído de dentro de uma função, você não pode acessar variáveis globais a menas que tenha uma declaração global.

require_once(dirname(__FILE__) . '/../../config.php');
require_once($CFG->libdir . '/moodlelib.php'); // Won't work.
require_once(dirname(__FILE__) . '/../../config.php');

 

global $CFG; // You need this.
require_once($CFG->libdir . '/moodlelib.php'); // Will work now.

Chamada a funções globais

Testar um método que chama funções globais pode ser problemático. No mínimo, é sempre complexo, porque não se pode controlar o que acontece nas funções globais. Não podemos sobrepo-las ou burla-las em nossos testes de unidade. Se a função global por si só foi bem testada, pode não ser um grande problema, mas a maioria das funções globais não é bem testada..

Padrão de Ponte

Caso seu código precise extensivamente de alguma API pública, você pode utilizar o padrão de ponte para decupar seu código dessa API. Dessa forma, quando você escreve testes de unidade, você pode sobrepor a classe da ponte ou burla-la, e controlar as saídas enquanto mantém seu foco exclusivamente em testar seu código.

Um exemplo básico: imagine que eu não confio na função global get_string() mas meu código precisa utiliza-la. Inicialmente meu código está fortemente atrelado ao get_string():

class myclass {
   public function print_stuff($stuff) {
       echo get_string($stuff);
   }
}

Agora vamos escrever uma classe de ponte para resolver essa questão e utiliza-la no lugar do get_string():

class languageBridge {
   public function get_string($stuff,$module='moodle') {
       echo get_string($stuff, $module);
   }
}
class myclass {
   public $lang_bridge;
   public function __construct() {
       $this->lang_bridge = new languageBridge();
   }
   public function print_stuff($stuff) {
       echo $this->lang_bridge->get_string($stuff);
   }
}

A seguir, outro exemplo utilizando um método de ponte para decupar a função da API do cerne do Moodle.

class workshop_api {
   /**
    * This is a method we want to unittest
    */
   public function get_peer_reviewers($context) {
       static $users=null;
       if (is_null($users)) {
           $users = $this->get_users_by_capability($context, 'mod/workshop:peerassess', 
                       'u.id, u.lastname, u.firstname', 'u.lastname,u.firstname', , , , , false, false, true);
       }
       return $users;
   }

 

   /**     - 
    * Bridging method to decouple from Moodle core API
    */
   protected function get_users_by_capability() {
       $args = func_get_args();
       return call_user_func_array('get_users_by_capability', $args);
   }
}

ATENÇÃO: seguem alguns comentários dos exemplos acima expressando que o padrão de ponte deve ser utilizado cuidadosamente.

  • "Eu acho que é o caso dos testes de unidade conduzirem ao pior design de software, em que você não utiliza a API padrão para algo. Mas se você realmente quer fazer o teste, não consigo pensar em uma solução melhor.”
  • "Eu acho que pode ser bom se utilizado com muito rigor de seleção. Infelizmente eu não acho que seja a solução que você queira para decupar as complexas e profundas funções do Moodle."
  • "...seu código foi desenhado para ser parte do Moodle, então decupa-lo de uma API padrão é perversidade."

Teste de unidade em 2.0

Moodle 2.0

Com a objetivação das bibliotecas de bancos de dados no Moodle 2.0, novas e melhores abordagens para testes de unidades podem ser utilizadas. Aqui o exemplo de um teste simples: (em curso/simpletest)

require_once($CFG->dirroot . '/course/lib.php');
global $DB;
Mock::generate(get_class($DB), 'mockDB');

 

class courselib_test extends UnitTestCase {
    var $realDB;

 

    function setUp() {
        global $DB;
        $this->realDB = $DB;
        $DB           = new mockDB();
    }

 

    function tearDown() {
        global $DB;
        $DB = $this->realDB;
    }

 

    function testMoveSection() {
        global $DB;
        $course = new stdClass();
        $course->id = 1;

 

        $sections = array();
        for ($i = 1; $i < 11; $i++) {
            $sections[$i]          = new stdClass();
            $sections[$i]->id      = $i;
            $sections[$i]->section = $i - 1;
        }

 

        $DB->expectOnce('get_records', array('course_sections', array('course' => $course->id)));
        $DB->setReturnValue('get_records', $sections);
        $this->assertFalse(move_section($course, 2, 3));
    }
}

Ver também UnitTestCaseUsingDatabase em lib/simpletestlib.php.

Testando saídas HTML

(em progresso – a ser documentado apropriadamente uma vez que a API estabilize)

  • ContainsTagWithAttribute($tag, $attribute, $value)
  • ContainsTagWithAttributes($tag, $attributes)
  • ContainsTagWithContents($tag, $content)
  • ContainsEmptyTag($tag)

A sintaxe

$this->assert(new ContainsTagWithAttribute($tag, $attribute, $value), $html);

Análise de cobertura de código

Nota: Esta seção é um trabalho em progresso. Utilize os comentários da página ou fórum apropriado do moodle.org para recomendações e sugestões de melhoria.

Cobertura de código é uma técnica, fortemente ligada a testagem de software, que permite checar e aprimorar a qualidade dos testes através de medidas dos degraus dos código fonte que são cobertos por eles. Com suporte do Moodle e realizando testes diariamente (indo em direção a criação de um modelo de Desenvolvimento de Test Drive) nós precisamos integrar algumas ferramentas em nosso processo de desenvolvimento ajudando a analisar a qualidade de nossos testes.

Neste momento (no Moodle 2.0) estamos utilizando SimpleTest, uma simples e ótima ferramenta para realizar todos os testes. Infelizmente, não suporta completamente análise de cobertura de código. Por outro lado, outros testes de unidade em PHP como PHPUnit, mais complexos e poderosos, construíram suporte para essa técnica, mas alterando-se para um novo produto fora dos nosso atual planejamento de Roadmap.

Então, após algumas leituras e comparações, o Moodle irá implementar suas próprias extensões para o SimpleTest, para completar o objetivo principal de ter uma homologação/linha de análise de cobertura de código funcionando de Moodle 2.0 em diante. Para alcançar isso, também Spike PHPCoverage, uma ferramenta básica de cobertura de código, será utilizada e extendida. Você pode encontrar os detalhes da implementação desta ferramenta em [https://tracker.moodle.org/browse/MDL-19579 MDL-19579].

Alterações

Para habilitar cobertura de código em seus testes, apenas algumas modificações precisam ser adicionadas em seu código atual:

  1. Altere suas classes de teste adicionando dois atributos: $includecoverage e $excludecoverage, ambos arrays (matrizes), utilizados para informar a ferramenta de cobertura de código (via reflexão) sobre quais arquivos de código fonte devem ser cobertos ou pulados pela análise.
  1. Utilize simpletestcoveragelib.php ao invés de simpletestlib.php em suas chamadas de script.
  1. Utilize a classe autogroup_test_coverage ao invés de AutoGroupTest (veja detalhes abaixo) nas suas chamadas de script.

(note que apenas o primeiro ponto acima é necessário para novas unidades de testes serem criadas porque 2 e 3 (alterações em chamadas de script) já foram implementadas no Moodle e estão esperando suas unidades de teste.

Isso é tudo! Com essas três alterações básicas, você terá um relatório de cobertura de código completo disponível para análise aprofundada.

API

Quando utilizando cobertura de código com Moodle, existem dois tipo de APIs disponíveis, ambas provendo a mesma cobertura de código, mas de maneiras diferentes. Veja a seguir:

  • Interna (oculta) API de cobertura: Esta API está completamente oculta atrás da API de unidade de teste e você não precisará saber os detalhes a respeito dela. Apenas efetue as alterações 1-2-3 descritas acima e, após rodar os testes, você terá o relatório final disponível para ser usado imediatamente, sem precisar executar mais nada em seu código. Sua maior desvantagem: só pode executar uma "sessão de cobertura de código" (instrumentação), então só é apropriada para testar scripts usando apenas uma execução de unidade de teste. Um exemplo desse tipo de teste é admin/report/unittest/index.php onde apenas um (grande) grupo de teste é executado.
  • Externa (explícita) API de cobertura: Esta API precisa de código extra enquanto instância de cobertura. Configuração e geração de relatórios acontecem no script principal. É um pouco mais complexo, mas, por outro lado, suporta instrumentações múltiplas para ser executado, e te dá mais controle sobre o processo de cobertura de código. Um exemplo desse tipo de unidade de teste é admin/report/unittest/dbtest.php onde múltiplos grupos de teste são executados (um para cada BD sendo testado).

Então, primeiramente (ponto 1 na seção anterior – modo de usar), nós precisamos definir, para cada unidade de teste, quais arquivos / diretórios (relacionados ao dirroot) nós queremos analisar com a ferramenta. Aqui tem um exemplo, para unidade de teste dml_test (/lib/dml/simpletest/testdml.php), tudo que precisamos é adicionar essas linhas para a declaração de classe :

public  static $includecoverage = array('lib/dml');
public  static $excludecoverage = array('lib/dml/somedir');

Feito isso, a ferramenta de cobertura de código saberá quais são os arquivos para executar análise de cobertura/relatórios e o fará para todos os arquivos (recursivamente) no diretório lib/dml' , mas excluindo o diretório lib/dml/somedir (também recursivamente). Note que os dois atributos são arrays, então caminhos múltiplos podem ser especificados em qualquer um deles. Note também que o diretório onde os testes de unidade estão armazenados é automaticamente excluído (geralmente diretórios simpletest).

E, como dito, isso é tudo que você precisa para completar os atuais e novos testes de unidade sendo analisados pela ferramenta de cobertura de código pelos scripts atuais do Moodle. A documentação abaixo só é interessante para desenvolvedores que pretendem criar novos scripts capazes de executar testes de unidade com cobertura de código. (pontos 2 e 3 na seção anterior – modo de usar).

Cobertura interna de API

$test = new autogroup_test_coverage($showsearch, $test_name, $performcoverage, $coveragename, $coveragedir);

Crie um novo objeto de teste autogroup com suporte a cobertura de código, então você pode especificar se quer executar cobertura (verdadeiro/falso), o nome do relatório (título) e o diretório onde o relatório final será criado (em moodledata/codecoverage). Opcionalmente você pode adicionar mais arquivos e diretórios (relacionados a dirroot) para a lista de arquivos a serem cobertos/ignorados utilizando essas funções (em caso o definido no ponto 1 não é o suficiente).

$test->add_coverage_include_path($path);
$test->add_coverage_exclude_path($path);

Então, após adicionar um grupo de testes de unidade ao grupo, você simplesmente invoca o teste de execução com suporte a cobertura de código para finalizar com um bom relatório de cobertura de código em dataroot/codecoverage/$coveragedir:

$test->run($unit_test_reporter);

(não esqueça que essa API suporta apenas uma instrumentação a ser executada)

E isso é tudo!

Cobertura externa de API

$covreporter = new moodle_coverage_reporter($coveragename, $coveragedir);
$covrecorder = new moodle_coverage_recorder($covreporter);

Crie um relatório de cobertura, passando seu título e diretório externo como parâmetros.

$test = new autogroup_test_coverage($showsearch, $test_name, $performcoverage);

Crie um novo objeto de teste autogroup com suporte a cobertura de código, você não precisa especificar o título e diretório aqui (como já foi definido no objeto moodle_coverage_reporter). Opcionalmente você pode adicionar mais arquivos e diretórios (relativos a dirroot) para a lista de arquivos a serem cobertos/ ignorados utilizando essas funções (no caso, o que foi definido no ponto 1 não é o bastante).

$test->add_coverage_include_path($path);
$test->add_coverage_exclude_path($path);

Então, após adicionar um grupo de testes de unidade, você simplesmente invoca a execução de teste com suporte a cobertura de código com:

$test->run_with_external_coverage($unit_test_reporter, $covrecorder);

(não esqueça que essa API suporta múltiplas instrumentações para ser executadas)

E finalmente, você gera o relatório de cobertura de código (under dataroot/codecoverage/$coveragedir) utilizando:

$covrecorder->generate_report();

Mais uma vez, isso é tudo!

Notas finais

  • Note que existem mais alguns métodos disponíveis na classe moodle_coverage_recorder. Eles permitirão controlar as instrumentações iniciais e finais manuealmente e outras questões menores, mas elas não deveriam ser usadas. Os métodos run() e run_with_external_coverage() devem ser o bastante em 99% dos casos.
  • Sem ser parte da API, mas utilizado por ela, existe um script overagefile.php em admin/report/unittest responsável por servir os arquivos de relatório de cobertura de dentro do Moodle. Veja os scripts atuais naquele diretório para saber como podem ser utilizados.
  • Todas as execuções de teste/ relatórios / utilitários de cobertura devem ser protegidos com a permissão 'moodle/site:config' .


Um alerta

Em 'xUnit Test Patterns' existe uma escala de dificuldade de teste que vai de 1. a 6. Moodle é certamente 6 nessa escala 'software não-orientado-a-objeto'. É recomendável que você não comece a aprender sobre testes de undade com esse tipo de software :-(

Informações adicionais sobre unidades de teste

PHP em Ação tem um excelente capítulo explicando testes de unidade em PHP com simpletest. (Embora o resto do livro traga um estilo de programação muito diferente do que é utilizado no Moodle.)

Unidades de teste pragmáticas em Java com JUnit também é uma introdução muito boa, a despeito de estar na linguagem de programação errada. JUnit e Simpletest são muito similares.

xUnit Test Patterns é o último livro de unidade de teste. Eu acredito que ensine a você tudo que é possível aprender a respeito de testes de unidade através de um livro. O único jeito de aprender mais seria adquirir experiência. O conselho para lidar com os tipos de problemas mais confusos é estar em um grande projeto real, como o Moodle.