SDPT Lab 4

De la WikiLabs
Jump to navigationJump to search

Lab 4: Software Verification - TDD, Mocking, and Code Coverage

Introduction

In traditional embedded systems, testing involves compiling code, flashing it to a board, and physically observing the hardware. This is slow, expensive, and scales poorly. Today, we implement "Shift-Left Testing." We will verify our C++ logic on our Host PC using automated frameworks before it ever touches a microcontroller.

Goals

  • Integrate Google Test and Google Mock via CMake.
  • Write Unit Tests using the AAA (Arrange, Act, Assert) pattern.
  • Use Dependency Injection to decouple business logic from hardware registers.
  • Simulate hardware interactions using Google Mock (GMock).
  • Generate interactive HTML Code Coverage reports using gcovr.

Part 1: Theoretical Reference

Use this section as a reference manual to complete the challenges in Part 2.

1.1 Google Test & Mock Cheat Sheet

Basic Assertions:

  • EXPECT_EQ(actual, expected); (Non-fatal equality check)
  • EXPECT_FLOAT_EQ(actual, expected); (Safe float comparison)
  • EXPECT_TRUE(condition);

Mocking an Interface (GMock):

class MockSensor : public ISensor {
public:
    // MOCK_METHOD(ReturnType, MethodName, (Arguments), (override));
    MOCK_METHOD(float, read_temperature, (), (override));
};

Setting Mock Expectations:

MockSensor fake_sensor;
// Program the mock to return 45.0 exactly twice.
EXPECT_CALL(fake_sensor, read_temperature())
    .Times(2)
    .WillRepeatedly(testing::Return(45.0f));

1.2 CMake Debug & Coverage Setup

Coverage mathematically requires a completely unoptimized binary mapping exactly to your source code lines.

if(CMAKE_BUILD_TYPE STREQUAL "Debug")
    target_compile_options(my_target PRIVATE -O0 -g --coverage)
    target_link_options(my_target PRIVATE --coverage)
endif()

1.3 Running gcovr

Execute this from inside your build/ directory after running the tests:

gcovr -r ../ . --html --html-details -o coverage.html -e ".*_deps.*"

1.4 Official Documentation & Tutorials

When you get stuck on syntax or advanced features during the lab, refer to these official resources:

Part 2: The Challenges

Setup: The Raw Materials

You are building the safety controller for an industrial oven. It must read temperatures from an I2C sensor and trigger a physical alarm if it overheats.

Create a new workspace (sdpt-lab4). Create include/ and src/ directories. Populate them with these three files:

include/ISensor.h (The Hardware Interface)

#pragma once

// Pure virtual interface. We DO NOT implement hardware logic here.
class ISensor {
public:
    virtual float read_temperature() = 0;
    virtual ~ISensor() = default;
};

include/OvenController.h (The Logic Module)

#pragma once
#include "ISensor.h"

class OvenController {
private:
    ISensor* temp_sensor;
    bool alarm_active;
    float max_safe_temp;

public:
    // Constructor uses Dependency Injection!
    OvenController(ISensor* sensor, float safe_limit = 250.0f);
    
    void check_safety();
    bool is_alarm_active() const;
    
    // Pure math function
    static float raw_adc_to_celsius(int adc_value);
};

src/OvenController.cpp (The Buggy Implementation)

#include "../include/OvenController.h"

OvenController::OvenController(ISensor* sensor, float safe_limit) 
    : temp_sensor(sensor), alarm_active(false), max_safe_temp(safe_limit) {}

void OvenController::check_safety() {
    float current_temp = temp_sensor->read_temperature();
    
    // BUG 1: It only triggers if STRICTLY greater. What if it equals the limit?
    if (current_temp > max_safe_temp) {
        alarm_active = true;
    } else {
        alarm_active = false;
    }
}

bool OvenController::is_alarm_active() const {
    return alarm_active;
}

float OvenController::raw_adc_to_celsius(int adc_value) {
    // BUG 2: Integer division truncates the decimal! (e.g., 5/2 = 2, not 2.5)
    return (adc_value / 4095) * 330.0f; 
}

Challenge 1: The Verification Build System (Estimated: 20 mins)

We need a build system that fetches Google Test and compiles our code with coverage instrumentation. Your Task:

  1. Create a CMakeLists.txt in the root directory.
  2. Build a static library named oven_lib containing src/OvenController.cpp.
  3. Use FetchContent to pull Google Test (https://github.com/google/googletest.git, tag v1.13.0).
  4. Create an executable named unit_tests. (You will create tests/test_oven.cpp in the next step).
  5. Link oven_lib, gtest_main, and gmock to your test executable.
  6. Add the CMake logic to inject -O0 -g --coverage ONLY if the build type is "Debug".

Verification: Run cmake -DCMAKE_BUILD_TYPE=Debug .. inside your build folder. It must configure without errors and download GTest.

Challenge 2: Pure Math TDD (Estimated: 25 mins)

Let's catch the integer division bug using the Red-Green-Refactor cycle. Your Task:

  1. Create a tests/ directory and a tests/test_oven.cpp file.
  2. Include <gtest/gtest.h> and your OvenController.h.
  3. Write a TEST(MathTest, ConvertsADCToCelsius).
  4. Use EXPECT_FLOAT_EQ to assert that an ADC value of 2047 (roughly half of 4095) returns approximately 164.957f.
  5. Compile and run ./unit_tests. (Watch it fail! This is the RED phase).
  6. Refactor OvenController.cpp to fix the integer division bug. Recompile. (Watch it pass! This is the GREEN phase).

Challenge 3: Hardware Mocking (Estimated: 40 mins)

We need to test check_safety(), but we are on a PC. We have no I2C hardware. We must mock the ISensor. Your Task:

  1. In your test file, include <gmock/gmock.h>.
  2. Create a MockSensor class that inherits from ISensor. Use MOCK_METHOD to mock read_temperature.
  3. Write a TEST(SafetyTest, TriggersAlarmOnOverheat).
    • Arrange: Instantiate the MockSensor. Instantiate the OvenController, injecting the mock sensor into it with a safe limit of 250.0f.
    • Expectation: Program the mock to return 260.0f when called.
    • Act: Call check_safety().
    • Assert: Expect that is_alarm_active() is true.
  4. Write a second test: TEST(SafetyTest, TriggersAlarmExactlyAtLimit).
    • Program the mock to return exactly 250.0f.
    • Run the test. It will FAIL due to Bug 1. Fix the production code so it triggers on `>=`.

Challenge 4: The Coverage Report (Estimated: 15 mins)

Did we test every branch of our code? Let's prove it mathematically. Your Task:

  1. Ensure you have run ./unit_tests at least once so the .gcda files are generated.
  2. Run the gcovr command provided in the Theoretical Reference section.
  3. Open the resulting coverage.html file in your web browser. Check that you have achieved 100% branch coverage.

Submission and Evaluation (Moodle VPL)

For this lab, your work will be automatically graded by the Moodle Virtual Programming Lab (VPL) engine.

How to submit:

  1. Ensure your code compiles locally and all tests pass (Green).
  2. Do NOT upload your build/ directory or any generated HTML files.
  3. Select the following files from your workspace:
    • CMakeLists.txt
    • include/ISensor.h
    • include/OvenController.h
    • src/OvenController.cpp
    • tests/test_oven.cpp
  4. Package these files into a single .zip archive (maintaining the folder structure).
  5. Go to the Lab 4 assignment on Moodle and upload your .zip file into the VPL submission portal.
  6. Click the Evaluate button.

The VPL server will automatically compile your CMake project, execute your Google Tests, and run gcovr. Your grade will be calculated based on the number of passing tests and the percentage of branch coverage achieved.