Unit Testing
Testing automation code on physical hardware is often slow, expensive, and potentially hazardous. Adapnex separates control algorithms from physical I/O drivers through mapped variables, allowing task logic and control loops to be thoroughly tested in software without physical hardware or hardware driver mocks.
Google Test Framework
Adapnex uses Google Test, the industry-standard C++ testing
framework. Google Test provides rich assertion macros such as EXPECT_EQ, ASSERT_TRUE, and EXPECT_NEAR along with
structured test fixtures.
Google Test ships bundled directly with the Adapnex SDK. You do not need to download or install external testing libraries separately. Consult the Google Test documentation for a complete reference on available assertions and test macros.
Combined with the built-in Simulation platform, you can test Adapnex applications at three distinct
levels:
-
Direct Task Testing: Testing isolated task algorithms directly in unit tests.
-
Deterministic Time Simulation: Simulating task groups and time-dependent function blocks with a virtual clock.
-
Closed-Loop Plant Modeling: Simulating machine dynamics using
LambdaTaskor dedicatedTaskmodels to verify feedback control loops.
Direct Task Testing
Because an Adapnex Task exposes its inputs and outputs as plain member variables, you can instantiate a
task directly on the stack inside a standard Google Test (TEST), assign inputs, invoke its
Update() method manually, and assert the resulting outputs.
#include "adapnex.h"
#include <gtest/gtest.h>
class ThresholdAlarmTask final : public Task {
public:
float temperature_in = 0.0f;
bool alarm_out = false;
void Update() override {
alarm_out = temperature_in > 80.0f;
}
};
TEST(ThresholdAlarmTask, ActivatesAlarmWhenOverLimit) {
ThresholdAlarmTask task;
// Normal condition
task.temperature_in = 72.5f;
task.Update();
EXPECT_FALSE(task.alarm_out);
// Over-temperature condition
task.temperature_in = 85.0f;
task.Update();
EXPECT_TRUE(task.alarm_out);
}
Direct task testing is fast and well-suited for verifying pure mathematical calculations, combinational logic, and discrete state transitions.
Deterministic Time Simulation
Control applications frequently depend on timers (TON, TOF, TP), pulse generators, and rate
limits. Testing these with wall-clock time is slow and introduces non-deterministic test flakes due to CPU scheduling.
The Simulation test fixture replaces the real-time operating system scheduler with a virtual
clock. When you derive a test suite from Simulation using TEST_F(Simulation, …), calling
Simulate(duration) advances virtual time deterministically:
-
Instant Fast-Forwarding: Hours or days of application execution run in a few milliseconds of CPU time.
-
Zero Jitter: Simulated cycles execute at exact mathematical time steps without scheduling drift or race conditions.
-
Deterministic Results: Tests produce identical results across local developer workstations and CI runners.
#include "adapnex.h"
#include <gtest/gtest.h>
class PulseBeaconTask final : public Task {
public:
bool enable_in = false;
bool pulse_out = false;
void Update() override {
// Generates a 2-second pulse on a rising edge of enable_in
pulse_timer(enable_in, 2s);
pulse_out = pulse_timer.Q;
}
private:
TP pulse_timer;
};
TEST_F(Simulation, GeneratesAccuratePulseDuration) {
const auto group = Application::CreateCyclicTaskGroup({.period = 10ms});
const auto beacon = group->CreateTask<PulseBeaconTask>();
// Initial state
Simulate(100ms);
EXPECT_FALSE(beacon->pulse_out);
// Trigger pulse
beacon->enable_in = true;
Simulate(10ms);
EXPECT_TRUE(beacon->pulse_out);
// Still active after 1 second
Simulate(1s);
EXPECT_TRUE(beacon->pulse_out);
// After 2 seconds total, pulse expires
Simulate(1s);
EXPECT_FALSE(beacon->pulse_out);
}
Closed-Loop Plant Simulation
Testing closed-loop controllers (such as hysteresis or PID controllers) requires simulating the physical process (the "plant") responding to controller commands.
You can implement a simplified first-order plant model using a LambdaTask registered in the same
task group. In each simulated cycle, the plant task inspects the controller’s outputs, updates the simulated physics,
and updates the controller’s input variables:
#include "adapnex.h"
#include <gtest/gtest.h>
class OvenHeaterTask final : public Task {
public:
float temperature_in = 20.0f;
bool heater_out = false;
void Update() override {
// Hysteresis control: turn heater on below 60C, turn off above 65C
hysteresis(temperature_in, 60.0f, 65.0f);
heater_out = hysteresis.Q;
}
private:
Hysteresis hysteresis;
};
TEST_F(Simulation, ControlsOvenTemperatureWithinBand) {
const auto group = Application::CreateCyclicTaskGroup({.period = 100ms});
const auto oven = group->CreateTask<OvenHeaterTask>();
// First-order thermal plant simulation
group->CreateTask<LambdaTask>([&]() {
if (oven->heater_out) {
// Heating phase: temperature rises
oven->temperature_in += 0.05f;
} else {
// Ambient cooling phase: temperature drops
oven->temperature_in -= 0.02f;
}
});
// Simulate 10 minutes of heating from cold ambient
Simulate(10min);
// Verify temperature reached the operating band
EXPECT_GE(oven->temperature_in, 60.0f);
EXPECT_LE(oven->temperature_in, 66.0f);
// Simulate an additional 12 hours of steady-state operation
Simulate(12h);
// Verify temperature remains regulated within the hysteresis limits
EXPECT_GE(oven->temperature_in, 59.5f);
EXPECT_LE(oven->temperature_in, 65.5f);
}
The 12-hour thermal simulation executes in milliseconds, verifying that the hysteresis controller maintains regulation indefinitely without drift or deadlock.
While LambdaTask is well-suited for simple transfer functions or disturbance injection, more
complex plant models (such as multi-axis kinematics, fluid networks, or state machines with internal storage) are
commonly implemented by creating a dedicated class derived from Task. Dedicated task models encapsulate
their own parameters, maintain private state, and can be reused across different test suites.
Declaring Tests in CMake
Register test executables in your CMakeLists.txt using the adapnex_tests() macro provided by
the Adapnex toolchain:
cmake_minimum_required(VERSION 3.29)
project(my_controller)
enable_testing()
# Application executable
adapnex_executable(my_controller main.cpp)
# Test suite executable linking Google Test and the Simulation platform
adapnex_tests(my_controller_tests main_test.cpp)