Testes avançados em C++: de testes unitários a end-to-end
No desenvolvimento moderno em C++, a qualidade do código é determinada não apenas pelo seu desempenho, mas também pela sua confiabilidade. Os testes avançados vão muito além de verificações simples — é uma abordagem arquitetural que inclui testes unitários, de integração e end-to-end (e2e). Neste artigo, vamos analisar como construir corretamente a pirâmide de testes para projetos C++, usando Google Test, Google Mock e frameworks próprios.
1. Testes unitários: isolamento e mocks
Os testes unitários verificam módulos individuais (funções, classes) de forma isolada. Em nível avançado, a tarefa principal é aprender a criar mocks corretamente para as dependências. Vejamos um exemplo usando Google Mock.
Suponha que temos uma classe DatabaseConnector que depende de uma API externa. Queremos testar a lógica de processamento de dados sem acessar o banco de dados real.
#include <gmock/gmock.h>#include <gtest/gtest.h>
class DatabaseInterface {public: virtual ~DatabaseInterface() = default; virtual std::string fetchData(int id) = 0;};
class MockDatabase : public DatabaseInterface {public: MOCK_METHOD(std::string, fetchData, (int id), (override));};
class DataProcessor { DatabaseInterface& db;public: explicit DataProcessor(DatabaseInterface& db) : db(db) {} std::string process(int id) { auto raw = db.fetchData(id); if (raw.empty()) return "EMPTY"; return "PROCESSED: " + raw; }};
TEST(DataProcessorTest, ProcessWithValidData) { MockDatabase mockDb; EXPECT_CALL(mockDb, fetchData(42)) .WillOnce(testing::Return("Hello")); DataProcessor processor(mockDb); EXPECT_EQ(processor.process(42), "PROCESSED: Hello");}O uso de MOCK_METHOD permite controlar com precisão o comportamento das dependências. É importante lembrar dos strict mocks (através de NiceMock ou StrictMock) para capturar chamadas inesperadas.
2. Testes de integração: testando interações
Os testes de integração verificam como os módulos funcionam juntos. Em C++, isso geralmente envolve testar a interação com o sistema de arquivos, protocolos de rede ou bancos de dados. Uma abordagem avançada é usar test fixtures e parameterized tests.
Exemplo: testando a classe FileManager, que lê e escreve em arquivos. Usamos diretórios temporários.
class FileManagerTest : public ::testing::Test {protected: std::filesystem::path tempDir; void SetUp() override { tempDir = std::filesystem::temp_directory_path() / "test_files"; std::filesystem::create_directories(tempDir); } void TearDown() override { std::filesystem::remove_all(tempDir); }};
TEST_F(FileManagerTest, WriteAndReadFile) { FileManager fm(tempDir.string()); fm.write("test.txt", "data"); auto content = fm.read("test.txt"); EXPECT_EQ(content, "data");}Para testes parametrizados, usamos TEST_P com INSTANTIATE_TEST_SUITE_P. Isso permite executar o mesmo teste com diferentes dados de entrada.
class MathTest : public ::testing::TestWithParam<std::tuple<int, int, int>> {};TEST_P(MathTest, Add) { auto [a, b, expected] = GetParam(); EXPECT_EQ(add(a, b), expected);}INSTANTIATE_TEST_SUITE_P(AddTests, MathTest, ::testing::Values( std::make_tuple(1, 1, 2), std::make_tuple(-1, 1, 0), std::make_tuple(0, 0, 0) ));3. Testes End-to-End: verificando o ciclo completo
Os testes e2e simulam o comportamento do usuário final. Para aplicações C++, isso pode significar executar o binário completo, verificar sua saída ou interagir através de APIs. Nível avançado