Domínio de Testes Unitários em C++ com Google Test e Google Mock

A Filosofia por Trás das Asserções: ASSERT vs. EXPECT

A diferença fundamental entre as macros ASSERT_* e EXPECT_* no Google Test (GTest) vai além de simplesmente "interromper" ou "continuar" a execução. A escolha correta depende da intenção do teste e da gestão de falhas em cascata.

Considere um teste de inicialização de sistema:

TEST(SystemBootstrapTest, CompleteInitializationFlow) {
    auto* db_conn = establish_db_connection();
    ASSERT_TRUE(db_conn != nullptr) << "Falha na conexão com o banco de dados, abortando testes subsequentes";

    auto app_settings = load_settings("config.yaml");
    EXPECT_FALSE(app_settings.is_empty()) << "O arquivo de configuração está vazio";

    auto session_mgr = create_session_manager(db_conn);
    EXPECT_GT(session_mgr->active_count(), 0) << "Deve haver sessões ativas";
}

A lógica de camadas aqui é crucial:

  • ASSERT_TRUE(db_conn != nullptr) estabelece uma pré-condição crítica. Sem uma conexão ativa, qualquer operação subsequente falhará ou causará um erro de segmentação. Interromper imediatamente economiza tempo e evita ruído nos logs.
  • As macros EXPECT_* funcionam como verificações paralelas. Se a configuração estiver vazia, ainda é valioso saber se o gerenciador de sessões foi instanciado corretamente, coletando o máximo de informações de diagnóstico em uma única execução.

Além disso, o GTest utiliza especialização de templates para garantir segurança de tipos. A macro EXPECT_EQ(a, b) adapta sua estratégia de comparação:

  • Para tipos primitivos, realiza comparação direta.
  • Para strings no estilo C (const char*), compara o conteúdo e não os endereços de memória.
  • Para contêineres da STL (como std::vector), realiza comparação recursiva e detalha as diferenças em caso de falha.
std::vector<int> expected_metrics = {10, 20, 30};
std::vector<int> actual_metrics   = collect_metrics();

EXPECT_EQ(actual_metrics, expected_metrics);

Se a comparação falhar, o GTest integrará a sobrecarga do operador operator<< (se disponível) para exibir exatamente onde a divergência ocorreu, acelerando significativamente a depuração.

Organização de Testes: TEST vs. TEST_F

A escolha entre TEST e TEST_F dita como o estado é gerenciado durante a execução da suíte de testes.

Testes Independentes com TEST

Para funções puras, algoritmos ou utilitários sem efeitos colaterais, TEST é a escolha ideal. Cada teste é isolado e não compartilha estado.

TEST(StringUtilsTest, ConcatenateWithSeparator) {
    EXPECT_EQ(join_strings({"x", "y", "z"}, "|"), "x|y|z");
    EXPECT_EQ(join_strings({}, ","), "");
}

TEST(MathOperationsTest, FactorialBaseCase) {
    EXPECT_EQ(calculate_factorial(0), 1);
}

Compartilhamento de Estado com TEST_F

Quando os testes exigem um contexto complexo ou objetos com estado, TEST_F (Test Fixture) evita a duplicação de código de configuração.

class RingBufferTest : public ::testing::Test {
protected:
    void SetUp() override {
        ring_buf_ = std::make_unique<RingBuffer>(2048);
        ring_buf_->push("data", 4);
    }

    void TearDown() override {
        ring_buf_.reset();
    }

    std::unique_ptr<RingBuffer> ring_buf_;
};

TEST_F(RingBufferTest, PopRetrievesPushedData) {
    char out[10] = {};
    size_t bytes = ring_buf_->pop(out, sizeof(out));
    EXPECT_EQ(bytes, 4);
    EXPECT_STREQ(out, "data");
}

TEST_F(RingBufferTest, SizeReflectsPushOperations) {
    EXPECT_EQ(ring_buf_->size(), 4);
    ring_buf_->push("_more", 5);
    EXPECT_EQ(ring_buf_->size(), 9);
}

O ciclo de vida do TEST_F garante que uma nova instância da classe fixture seja criada para cada teste, executando SetUp() antes e TearDown() depois. Isso previne vazamentos de estado entre casos de teste.

graph TD
    A[Iniciar Runner de Testes] --> B{Analisar TEST/TEST_F}
    B --> C[Identificar TEST: MathOperationsTest.*]
    B --> D[Identificar TEST_F: RingBufferTest.*]

    C --> E[Executar Corpo do Teste Diretamente]
    D --> F[Instanciar RingBufferTest]
    F --> G[Invocar SetUp]
    G --> H[Executar Corpo do TEST_F]
    H --> I[Invocar TearDown]
    I --> J[Destruir Instância]

    E & J --> K[Agregar Resultados e Gerar Relatório]

Asserções Avançadas: Precisão e Exceções

Comparação de Ponto Flutuante

Devido às limitações do padrão IEEE 754, a comparação direta de floats com == é propensa a erros. O GTest fornece macros especializadas que utilizam tolerância baseada em ULP (Units in the Last Place).

TEST(PhysicsMathTest, VelocityCalculationPrecision) {
    float computed_velocity = 9.8f * 3.0f;
    EXPECT_FLOAT_EQ(computed_velocity, 29.4f);

    double precise_pi = 3.14159265358979;
    EXPECT_NEAR(precise_pi, M_PI, 1e-12);
}

Validação de Exceções

Verificar se um código falha graciosamante é tão importante quanto verificar seu sucesso. As macros de exceção garantem que os caminhos de erro sejam acionados corretamente.

TEST(YamlParserTest, ThrowsOnInvalidSyntax) {
    EXPECT_THROW(parse_yaml("key: [unterminated"), YamlSyntaxException);
    EXPECT_ANY_THROW(parse_yaml(""));
    EXPECT_NO_THROW(parse_yaml("key: value"));
}

Google Mock: Controlando Comportamentos e Dependências

Enquanto o GTest valida o estado e os resultados, o Google Mock (GMock) verifica as interações e o comportamento entre objetos.

Isolamento de Dependências

Considere um serviço de notificação que depende de gateways externos. Testar isso com implementações reais introduz lentidão, custos e não determinismo.

class EmailGateway {
public:
    virtual ~EmailGateway() = default;
    virtual bool dispatch(const std::string& recipient, const std::string& body) = 0;
};

class MockEmailGateway : public EmailGateway {
public:
    MOCK_METHOD(bool, dispatch, (const std::string&, const std::string&), (override));
};

A macro MOCK_METHOD (padrão a partir do C++17) unifica a sintaxe, aceitando o tipo de retorno, nome do método, parâmetros e qualificadores.

Modos de Rigor: Nice, Naggy e Strict

Template Comportamento Caso de Uso
NiceMock<T> Ignora chamadas não esperadas silenciosamente Testes de integração parcial, mocks auxiliares
NaggyMock<T> Emite avisos para chamadas não esperadas (Padrão) Desenvolvimento ativo e depuração
StrictMock<T> Falha no teste se houver chamadas não esperadas Testes de regressão, validação estrita de contratos
TEST(NotificationTest, RetryOnTransientFailure) {
    StrictMock<MockEmailGateway> mock_gateway;
    NiceMock<MockMetricsCollector> mock_metrics;

    ON_CALL(mock_gateway, dispatch(_, _)).WillByDefault(Return(false));

    EXPECT_CALL(mock_gateway, dispatch(StrEq("admin@sys.com"), _))
        .Times(1)
        .WillOnce(Return(true));

    NotificationDispatcher svc(&mock_gateway, &mock_metrics);
    EXPECT_TRUE(svc.send_alert("admin@sys.com", "Server Down"));
}

ON_CALL vs. EXPECT_CALL

A distinção entre estas duas macros é fundamental para a legibilidade do teste:

  • ON_CALL: Define um comportamento padrão (stub). Não exige que o método seja chamado.
  • EXPECT_CALL: Estabelece uma expectativa estrita. O teste falhará se a chamada não ocorrer conforme especificado.
ON_CALL(mock_db, fetch_record(_, _))
    .WillByDefault(Return(DatabaseRecord{}));

EXPECT_CALL(mock_db, fetch_record(StrEq("users"), Eq(1)))
    .Times(1)
    .WillOnce(Return(active_users_page));

Matchers e Actions: Refinando Interações

Os matchers permitem validar argumentos complexos de forma declarativa.

EXPECT_CALL(mock_cache, store(Eq("user_profile"), Gt(0), Pointee(StrEq("verified"))))
    .Times(AtLeast(1));

As actions definem o que o mock deve fazer quando invocado:

  • Return(value): Retorna uma cópia do valor.
  • ReturnRef(ref): Retorna uma referência, evitando cópias de objetos grandes.
  • DoAll(a1, a2): Executa múltiplas ações em sequência.
  • Invoke(func): Delega a chamada para uma função ou método real.
EXPECT_CALL(mock_db, execute_query(_, _, _))
    .WillOnce(DoAll(
        Invoke(&audit_logger, &AuditLogger::log_query),
        Return(true)
    ));

EXPECT_CALL(mock_api, fetch_status())
    .WillOnce(Return(503))
    .WillOnce(Return(503))
    .WillRepeatedly(Return(200));

Práticas de Engenharia e Ciclo de Vida

Injeção de Dependência

A testabilidade exige que as dependências sejam injetadas, preferencialmente através de interfaces abstratas no construtor.

class InvoiceGenerator {
public:
    explicit InvoiceGenerator(TaxCalculator* calc) : tax_calc_(calc) {}
    double generate(double base_amount);
private:
    TaxCalculator* tax_calc_;
};

Gerenciamento de Expectativas

Ao usar fixtures com mocks compartilhados, é crucial limpar as expectativas no TearDown para evitar vazamento de estado entre testes.

void TearDown() override {
    testing::Mock::VerifyAndClearExpectations(gateway_.get());
}

Validação de Sequência

Para fluxos onde a ordem das chamadas é crítica, o bloco InSequence impõe uma ordem estrita de execução.

{
    InSequence seq;
    EXPECT_CALL(mock_auth, authenticate("admin")).WillOnce(Return(true));
    EXPECT_CALL(mock_auth, generate_token("admin")).WillOnce(Return("jwt_token"));
}
sequenceDiagram
    participant Test
    participant SUT
    participant Mock

    Test->>SUT: Instanciar objeto (Injetar Mock)
    Test->>Mock: Configurar EXPECT_CALL(...)
    Test->>SUT: Invocar método sob teste
    SUT->>Mock: Acionar método virtual
    Mock-->>SUT: Retornar valor configurado
    Test->>Mock: VerifyAndClearExpectations()
    Note right of Test: Validar conformidade das interações

Este diagrama serve como modelo padrão para o design de cada teste com Mock. Cada etapa responde a uma pergunta fundamental:

  • A dependência foi injetada corretamente?
  • As expectativas foram configuradas?
  • O método sob teste foi acionado?
  • As interações foram validadas?

Este ciclo de quatro etapas garante a integridade e a confiabilidade de cada caso de teste isolado.

Tags: C++ GoogleTest GoogleMock UnitTesting Mocking

Publicado em 7-4 09:45