Estrutura de Testes do JMine

O desenvolvimento e manutenção de um framework extenso e complexo como o Jmine, e das aplicações construídas a partir dele exigem a existência de uma ampla gama de testes de código, a fim de garantir o correto funcionamento dos diversos componentes do projeto.

Além de fornecer diversos testes abstratos que facilitam a validação de componenentes específicos do sistema, o JMine utiliza uma estrutura de TestSuites para executar os testes de cada módulo.

Esta estrutura consiste na criação de um TestSuite em cada módulo do projeto, que em seu método suite agrega os testes daquele módulo utilizando por exemplo os métodos da classe TestSuiteHelper, enquanto o build é configurado para executar, em sua fase de testes, apenas os TestSuites (de forma que os testes não sejam executados duas vezes).

Desta forma, alguns problemas são resolvidos:

  • Sobe-se a BeanFactory apenas uma vez a cada TestSuite, o que pode diminuir drasticamente o tempo de execução dos testes (executando-os isoladamente, a BeanFactory era criada e destruída para cada teste em que fosse necessária).
  • Problemas com a execução dos testes em uma determinada ordem também são descobertos/resolvidos mais facilmente, uma vez que os testes de cada módulo são executados separadamente.

Exemplo de configuração no pom do projeto para execução dos TestSuites:
<plugins>
        <plugin>
            <groupId>org.apache.maven.plugins</groupId>
            <artifactId>maven-surefire-plugin</artifactId>
            <inherited>true</inherited>
            <configuration>
                <includes>
                    <include>**/*TestSuite.java</include>
                </includes>
                <argLine>-Xmx512m</argLine>
                <systemPropertyVariables>
                    <jmine.test.categories>unit,local</jmine.test.categories>
                </systemPropertyVariables>
            </configuration>
        </plugin>
        (...)

Exemplo de TestSuite, do módulo de Persist do Jmine:

Além desta estrutura, o Jmine fornece duas outras formas de controlar os tipos de teste existentes e sua execução: A estrutura de Categorias de Teste e o UberTestSuite:

Categorias de Teste

Para o teste completo de uma aplicação ou mesmo um módulo de uma aplicação, podem ser necessários/desejados diversos tipos de teste: Unitários, Integrados, de Performance, entre outras variações. Ainda, devido a peculiaridades de cada tipo de teste, pode ser desejável que nem todos os testes sejam executados em todos os builds do projeto.

Por exemplo, testes caros (em tempo/recursos) de performance podem ser executados periodicamente, mas não a cada build em cada máquina de desenvolvedor.

Para facilitar esta segregação entre testes, o JMine oferece o conceito de Categoria de Teste, que permite o controle dos testes sendo executados a partir de propriedades do sistema.

Adicionando categorias

Para configurar a Categoria (ou categorias) de um dado teste, basta adicionar à classe de teste a anotação jmine.tec.unit.test.TestCategory passando como parâmetro as categorias desejadas, como por exemplo no ComponentStructureTest abaixo:

public class ComponentStructureTest {

    @Test
    public void checkComponentStructure() {
        coreComponent().assertCompliance();
    }
}

Controlando o build via Categorias

Para controlar quais categorias de testes devem ser executadas durante o build, os TestSuites nos módulos do Jmine observam duas propriedades do sistema:

  • jmine.test.categories: Lista as categorias que devem ser executadas. Caso a propriedade esteja vazia e/ou não tenha sido setada, testes de todas as categorias serão executados.
  • jmine.test.excludes: Lista as categorias dos testes que não serão executados. Esta propriedade tem maior prioridade que a anterior, isto é, caso um teste tenha duas categorias, uma listada em categories e outra em excludes, o teste não será executado. O mesmo vale para categorias que por algum motivo sejam listadas em ambas as propriedades.

Assim, para controlar quais testes devem ser executados durante o build, basta configurar estas propriedades com os valores desejados e, durante o build, executar apenas os TestSuites dos módulos. Cada TestSuite se encarregará de carregar e executar apenas os testes adequados do módulo correspondente.

Um local comum para a inclusão destes parãmetros é a configuração do plugin do Surefire no pom.xml do projeto, como pode ser visto no exemplo de configuração da seção Estrutura de Testes do JMine. Isto permite por exemplo que sejam criados diferentes perfis de build com Categorias de Teste diferentes configuradas.

Uber Test Suite

Nos módulos do Jmine existem diversos testes que dependem da existência de uma base de dados de referência, estes testes são realizados utilizando uma base H2 em memória, que é criada/destruída/apagada conforme a necessidade dos testes.

Com isso, o tempo gasto para execução dos testes aumenta muito quanto mais testes de integração (que dependem da base e/ou do Spring para serem executados) existirem no projeto. Para diminuir o impacto e tempo gasto por esses testes, foi criado o padrão de UberTestSuite, que consiste em um único TestSuite que executa todos os testes integrados do projeto, subindo a base de referência e a BeanFactory do spring uma única vez.

Criando o módulo para o teste

Embora não seja estritamente necessário, usualmente o UberTestSuite é criado em um módulo do projeto exclusivo para o TestSuite, devido às necessidades em relação a dependências e importação de XMLs do Spring e de BD.

Dependências

O módulo em que o UberTestSuite for criado deve ter dependência para os test-jars de todos os outros módulos do framework/projeto, para permitir a coleta de todos os testes integrados.

XML do Spring

Como o Suite executará testes de todos os módulos, é suficiente criar um XML do Spring que importe os XMLs de todos os projetos que tiverem testes de integração. Deve-se importar XMLs de teste caso haja a necessidade de que beans específicos do escopo de teste sejam importados.

De forma análoga, o bean que define o mapping para o módulo precisa referenciar os beans de MappingResources dos demais módulos.

XML de DB

De forma similar ao XML do Spring, o XML de DB necessita importar todos os dados necessários para execução dos testes, o que usualmente significa importar os XML de DB dos módulos que contém testes integrados. Para simplificar essa importação, pode-se importar o XML de DB do módulo de database do projeto, se este existir, pois este a princípio importa os XML de DB de todos os outros módulos.

O TestSuite

A título de exemplo, o UberTestSuite do Jmine pode ser visto a seguir:

public class UberTestSuite extends TestCase {

    public static final String SPRING_MAIN_XML = "jmine-tec-tests-all-test.xml";

    public static final String REFDB_XML_STRING = "jmine-tec-tests-all-db.xml";

    public static final String[] REFDB_XML = { REFDB_XML_STRING };

    public UberTestSuite() {
        super();
    }

    public UberTestSuite(String name) {
        super(name);
    }

    public static Test suite() throws Exception {
        System.setProperty("jmine.test.categories", "integration");
        System.setProperty("jmine.test.excludes", "local,earlyTest");
        System.setProperty("test.allow.cycles.in.bean.factory", "false");

        DBEnvironment.register("servicesPostscriptLoader");
        DBEnvironment.register("digesterPostscriptLoader");

        DBEnvironmentHolder.getInstance().instantiate(SPRING_MAIN_XML, REFDB_XML_STRING);
        DBEnvironmentHolder.getInstance().setIgnoreEnvironmentChange(true);

        return TestSuiteHelper.createDefaultSuite("jmine/tec");
    }

}

Existem três alterações que devem ser feitas no UberTestSuite em relação ao TestSuite dos outros módulos:

  • Carregamento da base de referência e do Spring via DBEnvironmentHolder.getInstance().instantiate e configuração para ignorar alterações posteriores no ambiente;
  • Configuração das categorias de teste que devem ser executadas:
    • A categoria ‘integration‘ é usualmente utilizada para indicar que se trata de um teste integrado e deve ser adicionada às categorias de teste (setando a propriedade do sistema jmine.test.categories)
    • Categorias que indiquem testes que devem ser executados separadamente, por precisar de bases de dados específicas por exemplo, devem ser excluídas do TestSuite, através da propriedade do sistema jmine.test.excludes;
  • O TestSuite deve ser criado utilizando-se o TestSuiteHelper, como nos demais módulos, porém passando um caminho que inclua os pacotes de todos os módulos do projeto. No caso do JMine, por exemplo, este caminho é ‘jmine/tec‘.

Casos comuns de Testes Locais

Como explicado na seção anterior, devem ser excluídos do UberTestSuite testes com necessidades específicas com relação a Spring ou base de referência. A serguir estão listados alguns dos casos comuns de testes com este tipo de peculiaridade. Esta lista não busca listar todos os casos possíveis, mas apenas fornecer uma base de testes comuns que frequentemente se encaixam neste caso.

  • Testes de Estrutura de Componente: Testes que validam a estrutura do módulo ou projeto, em especial os que se utilizam da estrutura de jmine.tec.component.structure.StructureComplianceRule para validações.
  • Testes de Performance: Estes testes, por serem mais caros/demorados, usualmente possuem uma categoria própria.
  • Teste de conflito de Postscript.
  • Testes de páginas e componentes web que precisem de configurações específicas no Spring.