Philip Withnall philip.withnall@collabora.co.uk 2015 Projete software para ser testado e escreva testes de unidades para ele Rafael Fontenelle rafaelff@gnome.org 2017 Teste de unidade Resumo

Teste de unidade deve ser o método principal para testar o conjunto de código escrito porque um teste de unidade pode ser escrito uma vez e executado muitas outras — testes manuais têm que ser planejados uma vez e, então, executado manualmente cada vez.

O desenvolvimento de testes de unidade se inicia com o design de API e arquitetura do código a ser testado: o código deve ser projetado para ser facilmente testável, do contrário provavelmente ele será muito difícil de testar.

Escreva testes de unidade para serem tão pequenos quanto possível, mas não menores. ()

Use ferramentas de cobertura de código para escrever testes para obter alta cobertura de código. ()

Execute todos os testes de unidade sob Valgrind para verificar ocorrências vazamentos e outros problemas. ()

Use as ferramentas apropriadas para automaticamente gerar testes de unidade onde for possível. ()

Projete o código para ser testável desde o início. ()

Escrevendo testes de unidade

Testes de unidade devem ser escritos em conjunto com a análise das informações de cobertura de código obtidas por executar os testes. Geralmente isso significa escrever um conjunto inicial de testes de unidade, executando-os para obter dados de cobertura e, então, retrabalhar e expandi-los para aumentar os níveis de cobertura de código. A cobertura deve primeiro ser aumentada se certificando de que todas as funções estejam cobertas (pelo menos em parte) e, então, se certificando de que todas as linhas de código estejam cobertas. Ao cobrir as funções primeiro, problemas de API que vão evitar testes efetivos serão localizados rapidamente. Essas geralmente se manifestam como funções internas que não podem ser facilmente chamadas a partir de testes de unidade. Em geral, deve-se ter como meta os níveis de cobertura em cerca de 90%; não teste apenas os casos cobertos pelos requerimentos do projeto, teste tudo.

Assim como git commits, os testes de unidade devem ser “tão pequenos quanto possível, mas não menores”, testando especificamente uma única API ou um único comportamento. Cada caso de teste deve ser capaz de executar individualmente, sem depender do estado de outros casos de teste. Isso é importante para permitir depuração de falha em um teste, sem ter que também passar por todos os outros códigos de teste. Isso significa que a falha de um teste pode ser facilmente rastreada a uma API específica, em vez de uma mensagem genérica “testes de unidade falharam em algum lugar”.

O GLib oferece suporte a testes de unidade com seu framework GTest, permitindo que os testes sejam organizados em grupos e hierarquias. Isso significa que os grupos de testes relacionados podem ser executados juntos para melhorar a depuração também, executando o binário de teste com o argumento -p: ./test-suite-nome -p /caminho/para/grupo/de/teste.

Testes instalados

Todos os testes de unidade devem ser instalados para todo o sistema, seguindo o padrão de testes instalados.

Ao instalar os testes de unidade, integração contínua (CI) é facilitada, já que os testes para um projeto podem ser reexecutados após alterações em outros projetos no ambiente de CI, de forma a testar as interfaces entre módulos. Isso é útil para um conjunto altamente encaixado de projetos como o GNOME.

Para adicionar suporte para testes instalados, adicione o seguinte ao configure.ac:

# Testes instalados AC_ARG_ENABLE([modular_tests], AS_HELP_STRING([--disable-modular-tests], [Disable build of test programs (default: no)]),, [enable_modular_tests=yes]) AC_ARG_ENABLE([installed_tests], AS_HELP_STRING([--enable-installed-tests], [Install test programs (default: no)]),, [enable_installed_tests=no]) AM_CONDITIONAL([BUILD_MODULAR_TESTS], [test "$enable_modular_tests" = "yes" || test "$enable_installed_tests" = "yes"]) AM_CONDITIONAL([BUILDOPT_INSTALL_TESTS],[test "$enable_installed_tests" = "yes"])

Então, em tests/Makefile.am:

insttestdir = $(libexecdir)/installed-tests/[project] all_test_programs = \ test-program1 \ test-program2 \ test-program3 \ $(NULL) if BUILD_MODULAR_TESTS TESTS = $(all_test_programs) noinst_PROGRAMS = $(TESTS) endif if BUILDOPT_INSTALL_TESTS insttest_PROGRAMS = $(all_test_programs) testmetadir = $(datadir)/installed-tests/[project] testmeta_DATA = $(all_test_programs:=.test) testdatadir = $(insttestdir) testdata_DATA = $(test_files) testdata_SCRIPTS = $(test_script_files) endif EXTRA_DIST = $(test_files) %.test: % Makefile $(AM_V_GEN) (echo '[Test]' > $@.tmp; \ echo 'Type=session' >> $@.tmp; \ echo 'Exec=$(insttestdir)/$<' >> $@.tmp; \ mv $@.tmp $@)
Verificação de vazamento

Assim que os testes de unidade com alta cobertura de código forem escritos, eles podem ser executados sob várias ferramentas de análise dinâmica, tal como Valgrind para verificar por vazamentos, erros de threading, problemas de alocação, etc. por toda a base de código. Quanto maior a cobertura de código dos testes unitários, mais confiança haverá nos resultados do Valgrind. Veja para mais informações, incluindo instruções sobre integração de sistema de compilação.

Mais importante de tudo é que isso significa que os testes de unidade não devem eles mesmos resultar em vazamento de memória ou de outros recursos e, de forma similar, não devem ter qualquer problemas de threading. Qualquer um desses problemas seria efetivamente um falso positivo na análise do código projeto. (Falsos positivos que precisam ser resolvidos corrigindo os testes de unidade.)

Geração de testes

Certos tipos de códigos são bem repetitivos e exigem muitos testes de unidade para obter uma boa cobertura de código; mas são apropriados para geração de dados de teste, na qual uma ferramenta é usada para automaticamente gerar vetores de teste para o código. Isso pode reduzir drasticamente o tempo necessário para escrever testes de unidade, para código nesses domínio em específico.

JSON

Um exemplo de um domínio acessível para geração de dados de teste é análise, na qual exige-se que os dados a serem analisados sigam um esquema estrito — esse é o caso para documentos XML e JSON. Para JSON, uma ferramenta como Walbottle pode ser usado para gerar vetores de teste para todos tipos de entrada válida e inválida de acordo com o esquema.

Todo tipo de documento JSON deve ter um JSON Schema definido para ele, que pode então ser passado para Walbottle para gerar vetores de teste:

json-schema-generate --valid-only schema.json json-schema-generate --invalid-only schema.json

Esses vetores de teste podem, então, ser passados para o código sob teste em seus testes de unidade. As instâncias JSON geradas por --valid-only deve ser aceita; aqueles de --invalid-only deve ser rejeitado.

Escrevendo código testável

O código deve ser escrito com testabilidade em mente desde o estágio de design, já que isso afeta o design de API e arquitetura em formas fundamentais. Alguns princípios chaves:

Não use um estado global. Objetos “sigleton” são geralmente uma má ideia, pois eles não podem ser instanciados separadamente ou controlados nos testes de unidade.

Separe o uso de estado externo, tal como banco de dados, conectividade ou sistema de arquivos. Os testes de unidade podem, então, substituir os acessos a estado externo com objetos simulados (mock). Uma abordagem comum a isso é usar injeção de dependência para passar um objeto interfaceador de sistema de arquivos para o código sob teste. Por exemplo, uma classe não deve carregar um banco de dados global (de uma localização fixa no sistema de arquivos) porque os testes de unidade poderiam acabar sobrescrevendo a cópia do sistema em execução do banco de dados, e nunca poderia ser executado em paralelo. A eles deve ser passado um objeto que fornece uma interface para o banco de dados: em um sistema de produção, isso seria um interfaceador magro em volta da API de banco de dados; para teste, seria um objeto simulado que verifica as requisições fornecidas a ele e retorna respostas codificadas para vários testes.

Exponha funções utilitárias onde geralmente elas podem ser úteis.

Divida projetos em coleções de bibliotecas pequenas e privadas, que são então vinculadas com uma quantidade mínima de código colante no executável global.