Testes C++ (Skill de Agente)
Fluxo de trabalho de testes orientado a agentes para C++ moderno (C++17/20) usando GoogleTest/GoogleMock com CMake/CTest.
Quando Usar
- Escrever novos testes C++ ou corrigir testes existentes
- Projetar cobertura de testes unitários/integração para componentes C++
- Adicionar cobertura de testes, proteção de CI ou proteção contra regressão
- Configurar fluxos de trabalho CMake/CTest para execução consistente
- Investigar falhas de teste ou comportamento instável
- Habilitar sanitizadores para diagnóstico de memória/condição de corrida
Quando NÃO Usar
- Implementar novos recursos de produto sem alterações de teste
- Refatorações em larga escala não relacionadas à cobertura ou falhas de teste
- Ajuste de desempenho sem regressões de teste para validar
- Projetos não C++ ou tarefas não relacionadas a testes
Conceitos Principais
- Ciclo TDD: vermelho → verde → refatorar (testes primeiro, correção mínima, depois limpeza).
- Isolamento: preferir injeção de dependência e fakes a estado global.
- Layout de teste:
tests/unit,tests/integration,tests/testdata. - Mocks vs fakes: mock para interações, fake para comportamento stateful.
- Descoberta CTest: usar
gtest_discover_tests()para descoberta estável de testes. - Sinal de CI: executar subconjunto primeiro, depois suíte completa com
--output-on-failure.
Fluxo de Trabalho TDD
Siga o ciclo VERMELHO → VERDE → REFATORAR:
- VERMELHO: escreva um teste com falha que capture o novo comportamento
- VERDE: implemente a menor alteração para passar
- REFATORAR: limpe enquanto os testes permanecem verdes
// tests/add_test.cpp
#include <gtest/gtest.h>
int Add(int a, int b); // Fornecido pelo código de produção.
TEST(AddTest, AddsTwoNumbers) { // VERMELHO
EXPECT_EQ(Add(2, 3), 5);
}
// src/add.cpp
int Add(int a, int b) { // VERDE
return a + b;
}
// REFATORAR: simplificar/renomear assim que os testes passarem
Exemplos de Código
Teste Unitário Básico (gtest)
// tests/calculator_test.cpp
#include <gtest/gtest.h>
int Add(int a, int b); // Fornecido pelo código de produção.
TEST(CalculatorTest, AddsTwoNumbers) {
EXPECT_EQ(Add(2, 3), 5);
}
Fixture (gtest)
// tests/user_store_test.cpp
// Stub de pseudocódigo: substitua UserStore/User pelos tipos do projeto.
#include <gtest/gtest.h>
#include <memory>
#include <optional>
#include <string>
struct User { std::string name; };
class UserStore {
public:
explicit UserStore(std::string /*path*/) {}
void Seed(std::initializer_list<User> /*users*/) {}
std::optional<User> Find(const std::string &/*name*/) { return User{"alice"}; }
};
class UserStoreTest : public ::testing::Test {
protected:
void SetUp() override {
store = std::make_unique<UserStore>(":memory:");
store->Seed({{"alice"}, {"bob"}});
}
std::unique_ptr<UserStore> store;
};
TEST_F(UserStoreTest, FindsExistingUser) {
auto user = store->Find("alice");
ASSERT_TRUE(user.has_value());
EXPECT_EQ(user->name, "alice");
}
Mock (gmock)
// tests/notifier_test.cpp
#include <gmock/gmock.h>
#include <gtest/gtest.h>
#include <string>
class Notifier {
public:
virtual ~Notifier() = default;
virtual void Send(const std::string &message) = 0;
};
class MockNotifier : public Notifier {
public:
MOCK_METHOD(void, Send, (const std::string &message), (override));
};
class Service {
public:
explicit Service(Notifier ¬ifier) : notifier_(notifier) {}
void Publish(const std::string &message) { notifier_.Send(message); }
private:
Notifier ¬ifier_;
};
TEST(ServiceTest, SendsNotifications) {
MockNotifier notifier;
Service service(notifier);
EXPECT_CALL(notifier, Send("hello")).Times(1);
service.Publish("hello");
}
Início Rápido CMake/CTest
# CMakeLists.txt (trecho)
cmake_minimum_required(VERSION 3.20)
project(example LANGUAGES CXX)
set(CMAKE_CXX_STANDARD 20)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
include(FetchContent)
# Prefira versões bloqueadas ao projeto. Se usar uma tag, use uma versão fixa conforme a política do projeto.
set(GTEST_VERSION v1.17.0) # Ajuste à política do projeto.
FetchContent_Declare(
googletest
# Estrutura de teste Google (repositório oficial)
URL https://github.com/google/googletest/archive/refs/tags/${GTEST_VERSION}.zip
)
FetchContent_MakeAvailable(googletest)
add_executable(example_tests
tests/calculator_test.cpp
src/calculator.cpp
)
target_link_libraries(example_tests GTest::gtest GTest::gmock GTest::gtest_main)
enable_testing()
include(GoogleTest)
gtest_discover_tests(example_tests)
cmake -S . -B build -DCMAKE_BUILD_TYPE=Debug
cmake --build build -j
ctest --test-dir build --output-on-failure
Executando Testes
ctest --test-dir build --output-on-failure
ctest --test-dir build -R ClampTest
ctest --test-dir build -R "UserStoreTest.*" --output-on-failure
./build/example_tests --gtest_filter=ClampTest.*
./build/example_tests --gtest_filter=UserStoreTest.FindsExistingUser
Depurando Falhas
- Execute novamente o teste com falha único usando filtro gtest.
- Adicione registro com escopo ao redor da asserção com falha.
- Execute novamente com sanitizadores habilitados.
- Expanda para a suíte completa assim que a causa raiz for corrigida.
Cobertura
Prefira configurações no nível do alvo em vez de flags globais.
option(ENABLE_COVERAGE "Habilitar flags de cobertura" OFF)
if(ENABLE_COVERAGE)
if(CMAKE_CXX_COMPILER_ID MATCHES "GNU")
target_compile_options(example_tests PRIVATE --coverage)
target_link_options(example_tests PRIVATE --coverage)
elseif(CMAKE_CXX_COMPILER_ID MATCHES "Clang")
target_compile_options(example_tests PRIVATE -fprofile-instr-generate -fcoverage-mapping)
target_link_options(example_tests PRIVATE -fprofile-instr-generate)
endif()
endif()
GCC + gcov + lcov:
cmake -S . -B build-cov -DENABLE_COVERAGE=ON
cmake --build build-cov -j
ctest --test-dir build-cov
lcov --capture --directory build-cov --output-file coverage.info
lcov --remove coverage.info '/usr/*' --output-file coverage.info
genhtml coverage.info --output-directory coverage
Clang + llvm-cov:
cmake -S . -B build-llvm -DENABLE_COVERAGE=ON -DCMAKE_CXX_COMPILER=clang++
cmake --build build-llvm -j
LLVM_PROFILE_FILE="build-llvm/default.profraw" ctest --test-dir build-llvm
llvm-profdata merge -sparse build-llvm/default.profraw -o build-llvm/default.profdata
llvm-cov report build-llvm/example_tests -instr-profile=build-llvm/default.profdata
Sanitizadores
option(ENABLE_ASAN "Habilitar AddressSanitizer" OFF)
option(ENABLE_UBSAN "Habilitar UndefinedBehaviorSanitizer" OFF)
option(ENABLE_TSAN "Habilitar ThreadSanitizer" OFF)
if(ENABLE_ASAN)
add_compile_options(-fsanitize=address -fno-omit-frame-pointer)
add_link_options(-fsanitize=address)
endif()
if(ENABLE_UBSAN)
add_compile_options(-fsanitize=undefined -fno-omit-frame-pointer)
add_link_options(-fsanitize=undefined)
endif()
if(ENABLE_TSAN)
add_compile_options(-fsanitize=thread)
add_link_options(-fsanitize=thread)
endif()
Diretrizes para Testes Instáveis
- Nunca use
sleeppara sincronização; use variáveis de condição ou latches. - Torne os diretórios temporários únicos por teste e sempre os limpe.
- Evite tempo real, rede ou dependências do sistema de arquivos em testes unitários.
- Use sementes determinísticas para entradas aleatórias.
Melhores Práticas
FAÇA
- Mantenha testes determinísticos e isolados
- Prefira injeção de dependência a globais
- Use
ASSERT_*para pré-condições,EXPECT_*para múltiplas verificações - Separe testes unitários vs integração em rótulos ou diretórios do CTest
- Execute sanitizadores no CI para detecção de memória e condição de corrida
NÃO FAÇA
- Não dependa de tempo real ou rede em testes unitários
- Não use sleeps como sincronização quando uma variável de condição puder ser usada
- Não faça mock excessivo de objetos de valor simples
- Não use correspondência de string frágil para logs não críticos
Armadilhas Comuns
- Usar caminhos temporários fixos → Gere diretórios temporários únicos por teste e limpe-os.
- Depender do tempo de relógio de parede → Injete um relógio ou use fontes de tempo falsas.
- Testes de concorrência instáveis → Use variáveis de condição/latches e waits limitados.
- Estado global oculto → Redefina o estado global em fixtures ou remova globais.
- Mock excessivo → Prefira fakes para comportamento stateful e faça mock apenas de interações.
- Falta de execução de sanitizadores → Adicione builds ASan/UBSan/TSan no CI.
- Cobertura em builds apenas de depuração → Garanta que os alvos de cobertura usem flags consistentes.
Apêndice Opcional: Fuzzing / Teste de Propriedades
Use somente se o projeto já suportar LLVM/libFuzzer ou uma biblioteca de teste de propriedades.
- libFuzzer: melhor para funções puras com E/S mínima.
- RapidCheck: testes baseados em propriedades para validar invariantes.
Harness mínimo do libFuzzer (pseudocódigo: substitua ParseConfig):
#include <cstddef>
#include <cstdint>
#include <string>
extern "C" int LLVMFuzzerTestOneInput(const uint8_t *data, size_t size) {
std::string input(reinterpret_cast<const char *>(data), size);
// ParseConfig(input); // função do projeto
return 0;
}
Alternativas ao GoogleTest
- Catch2: apenas cabeçalho, correspondentes expressivos
- doctest: leve, sobrecarga de compilação mínima