Página inicial: https://github.com/mity/acutest
Acutest é um recurso de teste de unidade C/C++ que visa ser o mais simples possível, não atrapalhar o desenvolvedor e minimizar quaisquer dependências externas.
Para conseguir isso, a implementação completa reside em um único arquivo de cabeçalho C e seu núcleo depende apenas de algumas funções padrão da biblioteca C.
Principais características:
acutest.h
.main()
).--tap
).--xml-output=FILE
).Recursos específicos do C++:
std::exception
, what()
será escrito na mensagem de erro.Recursos específicos do Unix/Posix:
clock_gettime()
), o usuário poderá medir os tempos de execução dos testes com --time=real
(o mesmo que --time
) e --time=cpu
.Recursos específicos do Linux:
Recursos específicos do Windows:
--time
.Recursos específicos do macOS:
Qualquer módulo C/C++ que implemente um ou mais testes unitários e incluindo acutest.h
, pode ser construído como um programa independente. Chamamos o binário resultante de "conjunto de testes" para os fins deste documento. O conjunto é então executado para executar os testes, conforme especificado em suas opções de linha de comando.
Dizemos que qualquer teste unitário é bem-sucedido se e somente se:
Para usar o Acutest, basta incluir o arquivo de cabeçalho acutest.h
no início do arquivo de origem C/C++ implementando um ou mais testes de unidade. Observe que o cabeçalho fornece a implementação da função main()
.
#include "acutest.h"
Todo teste deve ser implementado como uma função com o seguinte protótipo:
void test_example ( void );
Os testes podem usar algumas macros de pré-processador para validar as condições de teste. Eles podem ser usados várias vezes e, se alguma dessas condições falhar, o teste específico será considerado reprovado.
TEST_CHECK
é a macro de teste mais comumente usada que simplesmente testa uma condição booleana e falha se a condição for avaliada como falsa (ou zero).
Por exemplo:
void test_example ( void )
{
void * mem ;
int a , b ;
mem = malloc ( 10 );
TEST_CHECK ( mem != NULL );
mem = realloc ( mem , 20 );
TEST_CHECK ( mem != NULL );
}
Observe que os testes devem ser completamente independentes entre si. Sempre que o conjunto de testes for invocado, o usuário poderá executar qualquer número de testes no conjunto, em qualquer ordem. Além disso, por padrão, nas plataformas com suporte, cada teste de unidade é executado como um (sub)processo independente.
Finalmente, o arquivo fonte do conjunto de testes deve listar os testes unitários, usando a macro TEST_LIST
. A lista especifica o nome de cada teste (deve ser único) e o ponteiro para uma função que implementa o teste. Eu recomendo nomes que sejam fáceis de usar na linha de comando: evite especialmente espaços e outros caracteres especiais que possam exigir escape no shell; evite também o traço ( -
) como primeiro caractere do nome, pois ele poderia ser interpretado como uma opção de linha de comando e não como um nome de teste.
TEST_LIST = {
{ "example" , test_example },
...
{ NULL, NULL } /* zeroed record marking the end of the list */
};
Observe que a lista de testes deve ser finalizada com registro zerado.
Para conjuntos de testes básicos, isso é mais ou menos tudo que você precisa saber. No entanto, o Acutest fornece mais algumas macros que podem ser úteis em algumas situações específicas. Nós os cobrimos nas subseções a seguir.
Existe uma macro TEST_ASSERT
que é muito semelhante a TEST_CHECK
mas, se falhar, aborta instantaneamente a execução do teste de unidade atual.
Por exemplo:
void test_example ( void )
{
void * mem ;
int a , b ;
mem = malloc ( 10 );
TEST_ASSERT ( mem != NULL );
mem = realloc ( mem , 20 );
TEST_ASSERT ( mem != NULL );
}
O aborto em caso de falha é realizado chamando abort()
(se o teste for executado como um processo filho) ou via longjmp()
(se não for).
Portanto, só deve ser usado se você compreender os custos associados a um aborto tão brutal do teste. Dependendo do que o seu teste de unidade faz, ele pode incluir descritores de arquivos não liberados, vazamentos de memória, objetos C++ destruídos sem que seus destruidores sejam chamados e muito mais.
Em geral, TEST_CHECK
deve ser preferido a TEST_ASSERT
, a menos que você saiba exatamente o que faz e por que escolheu TEST_ASSERT
em alguma situação específica.
Para C++, há uma macro adicional TEST_EXCEPTION
para verificar se o código fornecido (normalmente apenas uma função ou uma chamada de método) lança o tipo de exceção esperado.
A verificação falhará se a função não lançar nenhuma exceção ou se lançar algo incompatível.
Por exemplo:
void test_example ( void )
{
TEST_EXCEPTION ( CallSomeFunction (), std:: exception );
}
Se uma verificação de condição falhar, geralmente é útil fornecer algumas informações adicionais sobre a situação para que o problema seja mais fácil de depurar. Acutest fornece as macros TEST_MSG
e TEST_DUMP
para esta finalidade.
O primeiro gera qualquer mensagem semelhante printf
, o outro gera um dump hexadecimal de um bloco de memória fornecido.
Então, por exemplo:
void test_example ( void )
{
char produced [ 100 ];
char expected [] = "Hello World!" ;
SomeSprintfLikeFunction ( produced , "Hello %s!" , "world" );
TEST_CHECK ( strcmp ( produced , expected ) == 0 );
TEST_MSG ( "Expected: %s" , expected );
TEST_MSG ( "Produced: %s" , produced );
/* Or, if the function could provide some binary stuff, we might rather
* use TEST_DUMP instead in order to output a hexadecimal dump of the data.
*/
TEST_DUMP ( "Expected:" , expected , strlen ( expected ));
TEST_DUMP ( "Produced:" , produced , strlen ( produced ));
}
Observe que ambas as macros geram qualquer coisa somente quando a macro de verificação mais recente falhou. Em outras palavras, esses dois são equivalentes:
if (! TEST_CHECK ( some_condition != 0 ))
TEST_MSG ( "some message" );
TEST_CHECK ( some_condition != 0 );
TEST_MSG ( "some message" );
(Observe que TEST_MSG
requer o compilador com suporte a macros variadas.)
Às vezes, é útil projetar sua função de teste como um loop sobre dados, fornecendo uma coleção de vetores de teste e seus respectivos resultados esperados. Por exemplo, imagine que nosso teste de unidade deva verificar algum tipo de implementação de função hash e temos uma coleção de vetores de teste para isso na especificação de hash.
Nesses casos, é muito útil obter algum nome associado a cada vetor de teste e exibir o nome no log de saída para que, se alguma verificação falhar, seja fácil identificar o vetor de teste culpado. No entanto, o corpo do loop pode executar dezenas de macros de verificação e, portanto, pode ser impraticável adicionar esse nome para personalizar cada mensagem de verificação no loop.
Para resolver isso, Acutest fornece a macro TEST_CASE
. A macro especifica uma string que serve como nome do vetor de teste. Quando usado, o Acutest garante que no log de saída o nome fornecido preceda qualquer mensagem de verificações de condição subsequentes.
Por exemplo, vamos supor que estamos testando SomeFunction()
que supostamente, para um determinado array de bytes de algum tamanho, retorna outro array de bytes em um buffer recém-editado em malloc
. Então podemos fazer algo assim:
struct TestVector {
const char * name ;
const uint8_t * input ;
size_t input_size ;
const uint8_t * expected_output ;
size_t expected_output_size ;
};
struct TestVector test_vectors [] = {
/* some data */
};
void test_example ( void )
{
int i ;
const uint8_t * output ;
size_t output_size ;
for ( i = 0 ; i < sizeof ( test_vectors ) / sizeof ( test_vectors [ 0 ]); i ++ ) {
struct TestVector * vec = & test_vectors [ i ];
/* Output the name of the tested test vector. */
TEST_CASE ( vec . name );
/* Now, we can check the function produces what it should for the
* current test vector. If any of the following checking macros
* produces any output (either because the check fails, or because
* high `--verbose` level is used), Acutest also outputs the currently
* tested vector's name. */
output = SomeFunction ( vec -> input , vec -> input_size , & output_size );
if ( TEST_CHECK ( output != NULL )) {
TEST_CHECK ( output_size == vec -> expected_output_size );
TEST_CHECK ( memcmp ( output , vec -> expected_output , output_size ) == 0 );
free ( output );
}
}
}
O nome especificado se aplica a todas as verificações executadas após o uso de TEST_CASE
TEST_CASE
seja usado novamente para especificar outro nome; ouTEST_CASE
com NULL
como argumento.Muitas das macros mencionadas nas seções anteriores têm uma contraparte que permite a saída de mensagens personalizadas em vez de algumas mensagens padrão.
Todas elas têm o mesmo nome das macros mencionadas, apenas com o sufixo de sublinhado adicionado. Com o sufixo, eles esperam um formato de string semelhante ao printf
e argumentos adicionais correspondentes.
Então, por exemplo, em vez da simples verificação de macros
TEST_CHECK (a == b);
TEST_ASSERT (x < y);
TEST_EXCEPTION (SomeFunction(), std::exception);
podemos usar suas respectivas contrapartes com mensagens personalizadas:
TEST_CHECK_ (a == b, " %d is equal to %d " , a, b);
TEST_ASSERT_ (x < y, " %d is lower than %d " , x, y);
TEST_EXCEPTION_ (SomeFunction(), std::exception, "SomeFunction() throws std::exception");
Você deve usar algumas palavras neutras para elas porque, com a opção de linha de comando --verbose
, as mensagens são desconectadas mesmo que a respectiva verificação seja aprovada com êxito.
(Se você precisar gerar algumas informações de diagnóstico caso a verificação falhe, use a macro TEST_MSG
. Esse é exatamente o seu propósito.)
Da mesma forma, em vez de
TEST_CASE ( "name" );
podemos usar mais rico
TEST_CASE_ ( "iteration #%d" , 42 );
No entanto, observe que tudo isso só pode ser usado se o seu compilador suportar macros de pré-processador variadas. As macros variáveis tornaram-se uma parte padrão da linguagem C com o C99.
Quando terminarmos de implementar os testes, podemos simplesmente compilá-lo como um programa C/C++ simples. Por exemplo, assumindo que cc
é o seu compilador C:
$ cc test_example.c -o test_example
Quando o conjunto de testes é compilado, o binário de teste resultante pode ser usado para executar os testes.
O código de saída do conjunto de testes é 0 se todos os testes de unidade executados forem aprovados, 1 se algum deles falhar ou qualquer outro número se ocorrer um erro interno.
Por padrão (sem nenhuma opção de linha de comando), ele executa todos os testes unitários implementados. Ele também pode executar apenas um subconjunto de testes de unidade conforme especificado na linha de comando:
$ ./test_example # Runs all tests in the suite
$ ./test_example test1 test2 # Runs only tests specified
$ ./test_example --exclude test3 # Runs all tests but those specified
Observe que um único argumento de linha de comando pode selecionar um grupo inteiro de unidades de teste porque o Acutest implementa vários níveis de seleção de teste de unidade (é usado o primeiro que corresponda a pelo menos uma unidade de teste):
Correspondência exata : quando o argumento corresponde exatamente ao nome completo de um teste de unidade, apenas o teste fornecido é selecionado.
Correspondência de palavra : quando o argumento não corresponde a nenhum nome de teste completo, mas corresponde à palavra inteira em um ou mais nomes de teste, todos esses testes são selecionados.
Os seguintes caracteres são reconhecidos como delimitadores de palavras: espaço
, tabulador t
, traço -
, sublinhado _
, barra /
, ponto .
, vírgula ,
, dois pontos :
, ponto e vírgula ;
.
Correspondência de substring : se mesmo a correspondência de palavra falhar ao selecionar qualquer teste, todos os testes com um nome que contenha o argumento como sua substring serão selecionados.
Ao adotar uma estratégia de nomenclatura de teste apropriada, isso permite ao usuário executar (ou pular se --exclude
for usado) facilmente toda a família de testes relacionados com um único argumento de linha de comando.
Por exemplo, considere o conjunto de testes test_example
que implementa os testes foo-1
, foo-2
, foomatic
, bar-1
e bar-10
:
$ ./test_example bar-1 # Runs only the test 'bar-1' (exact match)
$ ./test_example foo # Runs 'foo-1' and 'foo-2' (word match)
$ ./test_example oo # Runs 'foo-1', 'foo-2' and 'foomatic' (substring match)
$ ./test_example 1 # Runs 'foo-1' and 'bar-1' (word match)
Você pode usar --list
ou -l
apenas para listar todos os testes de unidade implementados por um determinado conjunto de testes:
$ ./test_example --list
Para ver a descrição de todas as opções de linha de comando suportadas, execute o binário com a opção --help
:
$ ./test_example --help
P: Este projeto não era conhecido como "CUTest"?
R: Sim. Ele foi renomeado porque o nome original estava muito sobrecarregado.
P: Preciso distribuir o arquivo README.md
e/ou LICENSE.md
?
R: Não. O cabeçalho acutest.h
inclui o URL do nosso repositório, uma nota de direitos autorais e os termos da licença do MIT. Contanto que você os deixe intactos, estaremos perfeitamente bem se você adicionar apenas o cabeçalho ao seu projeto. Afinal, seu uso simples e sua natureza completa são nosso objetivo principal.
Acutest é coberto pela licença MIT, consulte o arquivo LICENSE.md
ou início de acutest.h
para obter o texto completo.
O projeto reside no github:
Você pode encontrar a versão mais recente do Acutest lá, contribuir com melhorias ou reportar bugs.