Testes de unidade e funcionais em poucas palavras

Inspirando-me no trabalho que fiz no Deformetrica , vou mostrar a diferença entre testes unitários e funcionais .
Testes unitários e funcionais podem ser feitos com / ou / sem o uso de um framework, mas para simplificar e dar uma infraestrutura sólida ao projeto, o uso de frameworks é altamente recomendado caso você não queira reinventar a roda .

Para os exemplos, usarei o C ++ 11 Google Test Framework .

Testes Unitários

Testa os resultados produzidos por uma função ou método de classe .

Você pode pensar em um teste de unidade como uma função matemática :

The f() function (or method) for a given INPUT X, always return the OUTPUT Y.
Y
= f(X)

Do ponto de vista da programação, você pode considerar algo como estes exemplos:

  • ASSERT_EQ(5*2, 10) afirmar igual: as duas expressões, esquerda e direita devem ser iguais
  • ASSERT_EQ(multiply(5,2), 10)
  • ASSERT_TRUE(is_db_connected_to(host)) assert true: a condição dentro do assert deve ser true
  • ASSERT_GT(new_value, old_value) afirme ótimo então: a expressão esquerda deve ser ótima, então a direita
  • ASSERT_GE(random(0.0f, 1.0f), 0.0f) afirmar grande igual: a expressão esquerda deve ser grande ou igual à direita

O objetivo do teste de unidade é garantir que as funções (ou métodos) retornarão sempre os mesmos valores para a entrada fornecida!

Você sabe por que a programação funcional está tão na moda?
Só porque o próprio conceito dá confiança aos usuários (programadores & empresas): o comportamento de uma função é fortemente definido .
Se você está familiarizado com programação OO, sabe que, devido à complexidade e à interação entre as classes, o mesmo método de um objeto
pode retornar valores diferentes com base no contexto complexo e é difícil testar todos os contextos possíveis.

Assim, os testes de unidade permitem validar a definição matemática de funções (ou métodos).

Vejamos dois exemplos:

1. Verifique a entrada XML do arquivo

Para verificar se o leitor de XML funciona bem, um código simples como esse pode torná-lo muito seguro.

TEST_F(TestReadParametersXML, SparseDiffeoParameters) {

auto paramDiffeos = readSparseDiffeoParametersXML("fake_file");
ASSERT_TRUE
(paramDiffeos.IsNull());

paramDiffeos
= readSparseDiffeoParametersXML(UNIT_TESTS_DIR"/io/data/paramDiffeos.xml");
ASSERT_FALSE
(paramDiffeos.IsNull());

ASSERT_EQ
(paramDiffeos->GetKernelWidth(), 1.5);
ASSERT_EQ
(paramDiffeos->GetKernelType(), "Exact");
ASSERT_EQ
(paramDiffeos->GetNumberOfTimePoints(), 20);
}
2. Verifique as diferenças de duas matrizes

Usando as afirmações básicas fornecidas pela estrutura do Google, podemos criar novas afirmações capazes de verificar dados complexos.

void AbstractTestKernelPrecision::ASSERT_MATRIX_EQ(const MatrixType &X,
const MatrixType &Y,
const std::string &msg,
const ScalarType error) {
ASSERT_EQ
(X.rows(), Y.rows()) << msg;
ASSERT_EQ
(X.cols(), Y.cols()) << msg;

for (int i = 0; i < X.rows(); ++i) {
for (int j = 0; j < X.cols(); ++j) {
ScalarType x = X(i, j);
ScalarType y = Y(i, j);
ASSERT_LE
(std::min(std::abs(x - y) / std::abs(x), std::abs(x - y)), error) << msg << " on position (" << i << "," << j << ")";
}
}
}

Testes Funcionais

Testa os resultados produzidos por um subgrupo de funcionalidades de um software.

Ao contrário dos testes de unidade , não há uma maneira comum de codificar testes funcionais .
Aqui, eu mostro a estratégia que implementei Deformetrica, a ideia é:
1. apoiar-se na estrutura de teste do Google para ter uma maneira automática de verificar e executar asserções
2. usar a estratégia unix para clonar o processo em execução e atribuir ao filho a tarefa de executar um subgrupo específico de funcionalidades 3. criar um arquivo para definir o subgrupo de funcionalidades que deve ser verificado 4. coletar os resultados de cada processo filho e criar um relatório finalfork()
config.ini

1. Aprecie o Google Test Framework

Como para os testes de unidade , o código pode ser construído e executado usando a forma padrão que a estrutura de teste do Google oferece.
O LoadAndRunteste será gerenciado automaticamente pelo framework google.

TEST_F(TestFunctional, LoadAndRun) {

/* CODE HERE */

}
2. estratégiafork()

Como muitos softwares, Deformetricaler arquivos xml e executar processos diferentes depende da entrada.
Esses diferentes processos representam o subgrupo de funcionalidades que devem ser verificadas.
A ideia é rodar deformetricaem um childsubprocesso conforme é executado na linha de comando.

auto deformetrica_run = [](const std::string& args, const std::string& path, const std::string& stdout_file, const std::string& stderr_file) {
pid_t pid;
auto child = [&]() {
std
::vector<std::string> strs;
boost
::split(strs, args, boost::is_any_of("t "));

int argc = strs.size();
char *argv[argc + 1];

int i = 0;
for (auto &str: strs)
argv
[i++] = (char *) str.c_str();

/* CHANGING THE CURRENT WORKING DIRECTORY */
bfs
::current_path(path);

/* Redirect std::cout streaming to empty/fake streaming */
std
::ofstream stdout(stdout_file);
std
::ofstream stderr(stderr_file);
std
::streambuf *coutbuf = std::cout.rdbuf();
std
::streambuf *cerrbuf = std::cerr.rdbuf();
std
::cout.rdbuf(stdout.rdbuf());
std
::cerr.rdbuf(stderr.rdbuf());

/* Run deformetrica */
deformetrica
(argc, argv);

/* Restore stdout/stderr to previous streaming */
std
::cout.rdbuf(coutbuf);
std
::cout.rdbuf(cerrbuf);
exit(0);
};

auto father = [&]() {
int returnStatus;
waitpid
(pid, &returnStatus, 0);
};

pid
= fork();
pid
? father() : child();
};
3. arquivoconfig.ini

Usando o arquivo é possível definir os argumentos passados ​​para o programa em tempo de execução a partir da linha de comando.config.iniargc,argv

[Test1]
use_cuda
= YES
use_double_precision
= YES
tolerance
= 1e-5
path
= atlas/image/2d/digits
exec = deformetrica atlas 2D model.xml data_set.xml optimization_parameters.xml
state
-compare = deformetrica-state.bin

[Test2]
use_cuda
= NO
use_double_precision
= YES
tolerance
= 1e-7
path
= registration/image/2d/snowman
exec = deformetrica registration 2D model.xml data_set.xml optimization_parameters.xml
state
-compare = deformetrica-state.bin

#..etc..

O código que lê o arquivo ini é tão fácil de usar a biblioteca.boost::ptree

...
bpt
::ptree pt;
bpt
::ini_parser::read_ini(FUNCTIONAL_TESTS_DIR"/configure.ini", pt);

std
::size_t test_counter = 0;

while(++test_counter < 1000) {
/* EXTRACTION INI DATA */
std
::stringstream ss;
ss
<< "Test" << test_counter;
if (pt.find(ss.str()) == pt.not_found()) continue;
...
}
4. Colete os resultados e o relatório final

No whileloop, a função lambda deformetrica_runé chamada para executar o teste definido no config.ini.

Todos os dados gerados são verificados e coletados para testar se os resultados são equivalentes (usando uma precisão delta) com os dados que já foram pré-calculados.

...
try {

try {
/* Running deformetrica in a fork */
deformetrica_run
(cmdline.str(), working_directory, log_file, err_file);
} catch(...) {
TEST_COUT
<< "Current test has crashed" << std::endl;
good_run
= false;
continue;
}

if (!file_exist(output_state_file)) {
TEST_COUT
<< "Current test has not produced the state-file [" << output_state_file + "]" << std::endl;
good_run
= false;
continue;
}

def::utils::DeformationState df1, df2;

try {
df1
.load(compare_binary);
} catch(std::exception& err) {
TEST_COUT
<< err.what() << " - Error while reading the [" << compare_binary << "] file" << std::endl;
throw err;
}

try {
df2
.load(output_state_file);
} catch(std::exception& err) {
TEST_COUT
<< err.what() << " - Error while reading the [" << output_state_file << "] file" << std::endl;
throw err;
}

if (df1.compare(df2, ini_tolerance))
TEST_COUT
<< "Functional tests PASSED" << std::endl;
else
TEST_COUT
<< "Functional tests NOT PASSED" << std::endl;

} catch (...){
good_run
= false;
}
...

Um relatório simples, no estilo do teste de unidade clássico, é impresso.

Conclusão

Os testes unitários e funcionais podem economizar seu tempo e seus negócios!

Faça isso usando uma estrutura forte e não reinvente a roda.

Aproveitar!

… ah, tava esquecendo aqui todo o código

/***************************************************************************************
* *

* Deformetrica *

* *

* Copyright Inria and the University of Utah. All rights reserved. This file is *

* distributed under the terms of the Inria Non-Commercial License Agreement. *

* *

* *

****************************************************************************************/


#include "TestFunctional.h"
#include <iostream>
#include <fstream>
#include <memory>
#include <cstdio>
#include <thread>
#define BOOST_NO_CXX11_SCOPED_ENUMS
#include <boost/filesystem.hpp>
#undef BOOST_NO_CXX11_SCOPED_ENUMS
#include <boost/property_tree/ptree.hpp>
#include <boost/property_tree/ini_parser.hpp>
#include <boost/algorithm/string.hpp>
#include <unistd.h>

#include <stdlib.h>
#include <src/support/utilities/SerializeDeformationState.h>
#include <src/launch/deformetrica.h>

namespace bpt = boost::property_tree;
namespace bfs = boost::filesystem;

namespace testing
{
// TEST_COUT is copyright of https://stackoverflow.com/questions/16491675/how-to-send-custom-message-in-google-c-testing-framework/29155677
namespace internal
{
enum GTestColor {
COLOR_DEFAULT
,
COLOR_RED
,
COLOR_GREEN
,
COLOR_YELLOW

};

extern void ColoredPrintf(GTestColor color, const char* fmt, ...);
}
}
#define PRINTF(...) do { testing::internal::ColoredPrintf(testing::internal::COLOR_GREEN, "[ ] "); testing::internal::ColoredPrintf(testing::internal::COLOR_YELLOW, __VA_ARGS__); } while(0)

// C++ stream interface
class TestCout : public std::stringstream
{
public:
~TestCout()
{
PRINTF
("%s",str().c_str());
}
};

#define TEST_COUT TestCout()


namespace def {
namespace test {

void TestFunctional::SetUp() {
Test::SetUp();
}


#ifdef USE_CUDA
bool use_cuda = true;
#else
bool use_cuda = false;
#endif

#ifdef USE_DOUBLE_PRECISION
bool use_double_precision = true;
#else
bool use_double_precision = false;
#endif

TEST_F
(TestFunctional, LoadAndRun) {

auto file_exist = [](const std::string& f) {
std
::ifstream infile(f);
return infile.good();
};

auto deformetrica_run = [](const std::string& args, const std::string& path, const std::string& stdout_file, const std::string& stderr_file) {
pid_t pid;
auto child = [&]() {
std
::vector<std::string> strs;
boost
::split(strs, args, boost::is_any_of("t "));

int argc = strs.size();
char *argv[argc + 1];

int i = 0;
for (auto &str: strs)
argv
[i++] = (char *) str.c_str();