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 LoadAndRun
teste será gerenciado automaticamente pelo framework google.
TEST_F(TestFunctional, LoadAndRun) {
/* CODE HERE */
}
2. estratégiafork()
Como muitos softwares, Deformetrica
ler arquivos xml e executar processos diferentes depende da entrada.
Esses diferentes processos representam o subgrupo de funcionalidades que devem ser verificadas.
A ideia é rodar deformetrica
em um child
subprocesso 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.ini
argc,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 while
loop, 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();