#Automated Testing#Unit Tests#Non-Regression#Software Quality#CI/CD

Automated Testing: From Unit Tests to Non-Regression

A

Alexandre Jeffroy

Software Engineer

||3 min read

A project I won't forget: an aerospace simulator with several hundred thousand lines of C++, a whole team of developers. A minor change to a navigation module, on a Friday. By Monday, several approach flight scenarios were broken. Several days of bug hunting, and a delayed delivery.

The cause: no automated test to catch the regression. It surfaced during manual acceptance testing, far too late. Since then, on all my projects, automated tests aren't optional. They're the safety net that lets you change code with confidence.

Why automate tests

The first reason is immediate detection: a test run in CI reveals the problem in minutes, not weeks. Next comes confidence in changes, because solid coverage lets you refactor without fear of breaking existing behaviour. A good test also acts as living documentation, since it describes how the code should behave and goes stale far faster than a comment when it's no longer up to date. Finally, it makes regression impossible: once a bug is fixed, I write a test that locks the fix in, and the bug can't come back without the test flagging it.

The testing pyramid

Not all tests are equal. The higher you go in the pyramid, the more expensive tests are to write, the slower to run, and the more fragile: most of the coverage should come from **unit tests**, complemented by **integration tests**, with a limited number of end-to-end **functional tests**.

A unit test, in practice

A good unit test is fast, independent, reproducible and self-validating. Example on a railway braking distance calculation:

cpp
1class BrakingCalculator {
2public:
3    // speed in km/h, deceleration in m/s²
4    double computeBrakingDistance(double speedKmh, double decelMs2) const {
5        if (speedKmh < 0.0 || decelMs2 <= 0.0)
6            throw std::invalid_argument("Invalid parameters");
7        const double speedMs = speedKmh / 3.6;
8        return (speedMs * speedMs) / (2.0 * decelMs2);   // d = v² / (2a)
9    }
10};
11
12TEST(BrakingCalculatorTest, StandardBrakingAt100KmH) {
13    BrakingCalculator calc;
14    // Expected: about 772 metres, 1 metre tolerance
15    EXPECT_NEAR(772.0, calc.computeBrakingDistance(100.0, 0.5), 1.0);
16}
17
18TEST(BrakingCalculatorTest, NegativeSpeedThrows) {
19    BrakingCalculator calc;
20    EXPECT_THROW(calc.computeBrakingDistance(-50.0, 1.0), std::invalid_argument);
21}

To isolate the unit under test, real dependencies (a sensor, an actuator) are replaced with mocks. This is particularly useful for testing a safety behaviour without real hardware:

cpp
1TEST(SafetyMonitorTest, UnhealthySensorTriggersDanger) {
2    MockPositionSensor sensor;
3    sensor.setPosition(500.0);  // safe position
4    sensor.setHealthy(false);   // but faulty sensor
5
6    SafetyMonitor monitor(sensor, /*dangerZone=*/900.0);
7    EXPECT_TRUE(monitor.isInDangerZone());  // fail-safe principle
8}

Integration tests: verifying interactions

Unit tests verify isolated units; integration tests verify that these units work correctly together, for example a controller, its sensors and actuators in a full emergency braking scenario. They're slower, but essential to validate real interactions.

On critical systems: coverage and traceability

Certification standards (DO-178C in aerospace, CENELEC EN 50128 in railway) mandate strict coverage levels depending on criticality, measured with tools like gcov/lcov. Every detected anomaly must result in a test that reproduces it, permanently added to the regression suite. And every requirement must be covered by at least one traced test:

cpp
1// REQ-042: the system shall activate emergency braking
2//          within 200 ms of danger detection
3TEST(BrakingSystemTest, REQ_042_EmergencyBrakingResponse) {
4    auto start = std::chrono::high_resolution_clock::now();
5    system_.detectDanger();
6    auto elapsed = std::chrono::duration_cast<std::chrono::milliseconds>(
7        std::chrono::high_resolution_clock::now() - start).count();
8
9    EXPECT_TRUE(system_.isEmergencyBraking());
10    EXPECT_LE(elapsed, 200);
11}

What matters in the end

Automated tests only have value if they run systematically, on every commit, in CI. Write the test, integrate it, keep it green: it's this discipline that turns a test suite into a genuine non-regression guarantee, and that lets a critical system evolve without fear of breaking what already worked.