[CONTEÚDO] Testando sua aplicação C++ com GoogleTest
Pré-requisitos para leitura não obrigatorios
Para leitura e entendimento, vou supor que você ja tenha conhecimento de C++. Caso queira utilizar GoogleTest em seu projeto C++ junto com CMake, recomendo dar uma olhada nesse artigo que escrevi anteriormente sobre o mesmo, clicando aqui. Nesse artigo eu dou um setup básico para adicionar o GoogleTest ao seu projeto.
Para algo mais completo você pode olhar o código fonte dos testes que foram utilizados nesse artigo, lá também é utilizado o CMake com o GoogleTest e pode ser que tenha algo mais que você irá precisar.
Mas o que é GoogleTest?
GoogleTest ( ou GTest ) é um framework C++ de testes feitos pela Google, como diz em seu repositório, ele é a fusão de dois frameworks, o GoogleTest e o GoogleMock.
Projetos que utilizam GTest
Em seu repositório, ele mostra alguns projetos que utilizam o GTest como framework de testes, a lista abaixo tem quais são esses projetos.
Como posso utilizar GoogleTest nos meus projetos?
Como exemplo vamos pensar que você acabou de entrar em uma empresa que desenvolve um software de nutrição e como primeira tarefa foi lhe passado uma tarefa para implementar os testes unitários para o calculo de IMC. A função que faz o calculo de IMC recebe como parâmetro o peso e altura e retornar um enum com a classificação. Abaixo temos o exemplo desse código.
imc.hpp
enum PESO_CLASSIFICACAO{
IDEAL = 0,
ACIMA_PESO = 1,
ABAIXO_PESO = 2,
};
imc.cpp
PESO_CLASSIFICACAO imc(float altura, float peso) {
float imc_value = peso/(altura*altura);
if(imc_value < 18.6) return PESO_CLASSIFICACAO::ABAIXO_PESO;
if(imc_value > 24.9) return PESO_CLASSIFICACAO::ACIMA_PESO;
return PESO_CLASSIFICACAO::IDEAL;
}
Então você pensou em tres testes unitários, um para cada tipo de classificação e eles acabaram ficando assim.
case1/imc-test.cpp
TEST(ImcTest, AbaixoPeso) {
EXPECT_EQ(PESO_CLASSIFICACAO::ABAIXO_PESO, imc(1.80, 50));
}
TEST(ImcTest, AcimaPeso) {
EXPECT_EQ(PESO_CLASSIFICACAO::ACIMA_PESO, imc(1.80, 100));
}
TEST(ImcTest, Ideal) {
EXPECT_EQ(PESO_CLASSIFICACAO::IDEAL, imc(1.80, 70));
}
Para contextualizar melhor, aqui temos que ImcTest é o conjunto de teste a qual nosso teste faz parte, nesse caso esses tres testes fazem parte do mesmo conjunto de teste e ao lado dele temos o nome do teste. Além do EXPECT_ temos o ASSERT_, então fica a seu critério qual vai utilizar.
Ao rodar os testes temos algo parecido com o seguinte.
[==========] Running 3 tests from 1 test suite.
[----------] Global test environment set-up.
[----------] 3 tests from ImcTest
[ RUN ] ImcTest.AbaixoPeso
[ OK ] ImcTest.AbaixoPeso (0 ms)
[ RUN ] ImcTest.AcimaPeso
[ OK ] ImcTest.AcimaPeso (0 ms)
[ RUN ] ImcTest.Ideal
[ OK ] ImcTest.Ideal (0 ms)
[----------] 3 tests from ImcTest (0 ms total)
[----------] Global test environment tear-down
[==========] 3 tests from 1 test suite ran. (0 ms total)
[ PASSED ] 3 tests.
Nesse nosso exemplo só vai ter 3 testes, mas em projetos reais vai conter milhares de testes então é possivel utilizar um filtro.
./unit_test --gtest_filter="ImcTest*"
Obs.: O asterisco (*), serve para dizer ao gtest que pode vim qualquer tipo de carácter após ImcTest.
Testes parametrizados
Após implementar os testes você foi apresentar o seu código e disseram que tava bom, mas o padrão da equipe é utilizar testes parametrizados. Você como um bom desenvolvedor C++ que sabe testes parametrizados foi lá e fez.
case2/imc-test.cpp
struct TestParams {
float altura;
float peso;
PESO_CLASSIFICACAO expected;
};
class ImcTestParametrizado : public ::testing::TestWithParam<TestParams> {};
TEST_P(ImcTestParametrizado, CheckClassification) {
const TestParams& params = GetParam();
EXPECT_EQ(params.expected, imc(params.altura, params.peso));
}
INSTANTIATE_TEST_SUITE_P(
ImcTests,
ImcTestParametrizado,
::testing::Values(
TestParams{1.80, 50, PESO_CLASSIFICACAO::ABAIXO_PESO},
TestParams{1.80, 100, PESO_CLASSIFICACAO::ACIMA_PESO},
TestParams{1.80, 70, PESO_CLASSIFICACAO::IDEAL}
)
);
Testes parametrizados é uma boa forma de diminuir o número de código repetido para os testes e ficar apenas variando os valores de testes. Nesse caso temos que nos testes que foram feitos anteriormente, apenas o valor da classificação, peso e altura que eram alterados.
A saída desse teste ficou algo parecido com o seguinte.
[==========] Running 3 tests from 1 test suite.
[----------] Global test environment set-up.
[----------] 3 tests from ImcTests/ImcTestParametrizado
[ RUN ] ImcTests/ImcTestParametrizado.CheckClassification/0
[ OK ] ImcTests/ImcTestParametrizado.CheckClassification/0 (0 ms)
[ RUN ] ImcTests/ImcTestParametrizado.CheckClassification/1
[ OK ] ImcTests/ImcTestParametrizado.CheckClassification/1 (0 ms)
[ RUN ] ImcTests/ImcTestParametrizado.CheckClassification/2
[ OK ] ImcTests/ImcTestParametrizado.CheckClassification/2 (0 ms)
[----------] 3 tests from ImcTests/ImcTestParametrizado (0 ms total)
[----------] Global test environment tear-down
[==========] 3 tests from 1 test suite ran. (0 ms total)
[ PASSED ] 3 tests.
Note que ao final de cada nome de teste ficou um /número, para nomear seus testes você pode utilizar uma função para nomear o teste como tínhamos anteriormente.
No INSTANTIATE_TEST_SUITE_P vamos adicionar uma função lambda para ele criar um dicionario para mapear os valores do enum para seus nomes em versão texto.
imc.cpp
std::map<PESO_CLASSIFICACAO, std::string> MapEnumToString() {
std::map<PESO_CLASSIFICACAO, std::string> enumMap;
enumMap[PESO_CLASSIFICACAO::IDEAL] = "IDEAL";
enumMap[PESO_CLASSIFICACAO::ACIMA_PESO] = "ACIMA_PESO";
enumMap[PESO_CLASSIFICACAO::ABAIXO_PESO] = "ABAIXO_PESO";
return enumMap;
}
case2/imc-test.cpp
INSTANTIATE_TEST_SUITE_P(
ImcTests,
ImcTestParametrizado,
::testing::Values(
...
),
[](const auto& info) {
return std::string("teste_") + MapEnumToString().at(info.param.expected);
}
);
É possível utilizar uma função que retorna uma string como no exemplo abaixo.
std::string GenerateTestName(const testing::TestParamInfo<TestParams>& info) {
return std::string("teste_") + MapEnumToString().at(info.param.expected);
}
No instantiate, é só trocar a função lambda para O GenerateTestName da seguinte forma.
&GenerateTestName
Basta tirar a função lambda para isso e vai está funcionando corretamente.
Testes tipados
No Google testes também conseguimos utilizar os testes tipados.
aqui vamos utilizar um exemplo simples, que é uma função de soma e uma função de subtração, só que o tipo dos parâmetros de entrada e o retorno são um template.
math.hpp
template<typename T>
T add(T a, T b) {
return a + b;
}
template<typename T>
T sub(T a, T b) {
return a - b;
}
Abaixo temos como ficaria o teste tipado para elas.
case3/math-test.cpp
using testing::Types;
template <typename T>
class MathTest : public testing::Test {
};
using MyTypes = Types<int, double>;
TYPED_TEST_CASE(MathTest, MyTypes);
TYPED_TEST(MathTest, Addition) {
EXPECT_EQ(add<TypeParam>(2, 3), 5);
EXPECT_EQ(add<TypeParam>(-2, 3), 1);
EXPECT_EQ(add<TypeParam>(0, 0), 0);
}
TYPED_TEST(MathTest, subtraction) {
EXPECT_EQ(sub<TypeParam>(2, 3), -1);
EXPECT_EQ(sub<TypeParam>(-2, 3), -5);
EXPECT_EQ(sub<TypeParam>(0, 0), 0);
}
Com um case de teste tipado (TYPED_TEST_CASE), a gente pode colocar vários testes, no exemplo acima utilizamos a gente defini os tipos que queremos utilizar e o resultado esperado.
Testes tipados parametrizados
Os testes tipados parametrizados são um pouco complexo de entender, de antemão já vou indicando a pagina onde é explicado como utiliza-los, só clicar aqui. E também vou indicar o repositório onde tem outro exemplo de teste parametrizado tipado, só clicar aqui.
Esse tipo de teste acredito que vai ser muito pouco usado, mas ele está ai para quem quiser utilizar em seus projetos. Abaixo temos uma adaptação do que está escrito na documentação e no exemplo, essa foi forma mais didática que encontrei de como utilizar esse tipo de teste.
Abaixo temos um exemplo que tenta juntar a parametrização dos testes, para isso utilizamos um vector e para tipo de (int, double), temos um param diferente.
case4/math-test.cpp
template <typename T>
class MathTest : public testing::TestWithParam<T> {
protected:
static std::vector<TestParams<T>> params;
};
template<> std::vector<TestParams<int>> MathTest<int>::params = {
{2, 3, 5, -1},
{-2, 3, 1, -5},
{0, 0, 0, 0}
};
template<> std::vector<TestParams<double>> MathTest<double>::params {
{3.0, 3.0, 6.0, 0.0},
{-2.0, 3.0, 1.0, -5.0},
{0.1, 0.1, 0.2, 0.0}
};
using MyTypes = Types<int, double>;
TYPED_TEST_SUITE_P(MathTest);
TYPED_TEST_P(MathTest, Addition) {
for (auto param : MathTest<TypeParam>::params)
EXPECT_EQ(add<TypeParam>(param.a, param.b), param.add);
}
TYPED_TEST_P(MathTest, Subtraction) {
for(auto param : MathTest<TypeParam>::params)
EXPECT_EQ(sub<TypeParam>(param.a, param.b), param.sub);
}
REGISTER_TYPED_TEST_CASE_P(MathTest, Addition, Subtraction);
INSTANTIATE_TYPED_TEST_SUITE_P(MathTests, MathTest, MyTypes);
Conteúdo adicional
Para mais conteúdo recomendo olhar o seguinte a própria documentação do GoogleTest. Lá tem a parte de Mocking que não foi abordado aqui. Também tem uma parte sobre test fixture.
Repositório com o código dos testes: https://github.com/italonicacio/testando-sua-aplicacao-cpp-com-google-test/tree/main/project