Testing PLC code is difficult when every function block creates its own world internally.
This often happens in older IEC 61131-3 projects. A hydraulic power unit function block instantiates its valve blocks directly. A winch function block instantiates its brake, clutch, sensors, and valves directly. The structure looks convenient at first because fewer variables need to be linked at the program level.
The cost appears later.
If the winch state machine depends on internally created brakes, clutches, sensors, and valves, then testing the winch means testing the whole physical system around it. The test requires hardware or a large simulation of everything inside the block. The logic cannot be tested in isolation because the dependencies are hidden inside the implementation.
That is the problem dependency injection solves.
The idea is simple:
A function block should not create every object it depends on. Some dependencies should be passed into it from the outside.
In TwinCAT, this becomes useful when combined with interfaces.
Start With the Dependency
Assume a controller needs a pressure value. The controller should not care whether the value comes from a real analog input, a fieldbus device, a simulation model, or a test object.
It only needs a sensor contract.
INTERFACE ISensorThe interface exposes one property:
PROPERTY Value : LREALThe real sensor implements the interface.
FUNCTION_BLOCK PressureSensor IMPLEMENTS ISensor
VAR
valueActual : LREAL;
END_VARThe cyclic method can read hardware, apply scaling, or process diagnostics. The caller does not need to know those details.
METHOD update
valueActual := 123.4;The property getter returns the measured value.
Value := valueActual;Now a variable of type ISensor can refer to any function block that implements this
interface.
VAR
pressureSensor : PressureSensor;
sensor : ISensor;
pressure : LREAL;
END_VAR
sensor := pressureSensor;
IF sensor <> 0 THEN
pressure := sensor.Value;
END_IFThis is already useful. The controller can depend on ISensor instead of
PressureSensor. But the real benefit appears when testing starts.
Replace the Real Sensor
For a test, create a fake sensor that implements the same interface.
FUNCTION_BLOCK SensorFake IMPLEMENTS ISensor
VAR
valueActual : LREAL;
END_VARThe fake exposes the same Value property.
Value := valueActual;It also provides a method to set test values.
METHOD setValue
VAR_INPUT
value : LREAL;
END_VAR
valueActual := value;The production code reads ISensor.Value. It does not know whether the instance is a real
sensor or a fake sensor.
That is the useful abstraction. The controller depends on behavior, not on the concrete implementation.
The same pattern works for actuators. A valve can be represented by an interface.
INTERFACE IValveFor example:
METHOD open
METHOD close
PROPERTY IsOpen : BOOLA real valve writes outputs. A fake valve stores the requested state in memory. The controller can use both through the same interface.
Inject the Dependency
The next step is to pass the required objects into the controller.
The controller stores interface references internally.
FUNCTION_BLOCK PressureController
VAR
sensor : ISensor;
valve : IValve;
initialized : BOOL;
END_VARUse an explicit init() method to connect the dependencies.
METHOD init : BOOL
VAR_INPUT
sensorInput : ISensor;
valveInput : IValve;
END_VAR
IF sensorInput = 0 OR valveInput = 0 THEN
initialized := FALSE;
init := FALSE;
RETURN;
END_IF
sensor := sensorInput;
valve := valveInput;
initialized := TRUE;
init := TRUE;The cyclic logic uses only the interfaces.
METHOD update
VAR CONSTANT
PRESSURE_LIMIT : LREAL := 100.0;
END_VAR
IF NOT initialized THEN
RETURN;
END_IF
IF sensor <> 0 AND_THEN valve <> 0 THEN
IF sensor.Value > PRESSURE_LIMIT THEN
valve.open();
ELSE
valve.close();
END_IF
END_IFIn the real application, pass the real objects.
VAR
pressureSensor : PressureSensor;
pressureValve : PressureValve;
controller : PressureController;
initialized : BOOL;
END_VAR
IF NOT initialized THEN
initialized := controller.init(
sensorInput := pressureSensor,
valveInput := pressureValve);
END_IF
pressureSensor.update();
controller.update();In a test application, pass fake objects.
VAR
sensorFake : SensorFake;
valveFake : ValveFake;
controller : PressureController;
initialized : BOOL;
END_VAR
IF NOT initialized THEN
initialized := controller.init(
sensorInput := sensorFake,
valveInput := valveFake);
END_IF
sensorFake.setValue(120.0);
controller.update();The controller code does not change. Only the objects passed into it change.
That is dependency injection in a PLC context. Not magic. Not a framework. Just a controlled way to provide dependencies from the outside instead of hiding them inside the block.
What This Changes
Dependency injection makes testing practical because the tested block can be separated from real hardware.
You can feed sensor values directly into a fake sensor. You can observe whether a fake valve was opened or closed. You can test transitions in a state machine without connecting a real hydraulic unit.
This changes the design pressure on the code. Function blocks must become smaller. Their interfaces must be clear. Dependencies must be visible. Large blocks that instantiate half the machine internally become harder to justify. Tragic, I know. The monster block had such character.
There are also practical limits.
All instances are still statically allocated. Dependency injection in TwinCAT does not mean dynamic object creation. It means that the application creates the instances and then passes interface references to the blocks that need them.
Interface references must be checked before use.
IF sensor <> 0 THEN
pressure := sensor.Value;
END_IFInitialization order also matters. The top-level application must create and initialize the
objects in a defined order. Do not hide complex wiring in FB_init() unless there is a very
specific reason. FB_init() is called automatically and also runs during online changes.
For most application logic, an explicit init() method is easier to control and easier to
test.
The interface does not remove the need for a protocol. If IValve.open() is called, the
expected behavior must still be defined. Is it a command? A request? Is feedback required?
How are errors reported? The compiler checks that the method exists. It does not understand
your hydraulic concept. Rude, but consistent.
Conclusion
Dependency injection is valuable in PLC programming because it makes dependencies visible.
A controller that depends directly on a concrete sensor and a concrete valve is difficult
to test without those objects. A controller that depends on ISensor and IValve can use
real devices in the machine and fake devices in a test.
That makes testing before commissioning more realistic. It also improves the structure of the PLC application because function blocks become smaller and their contracts become clearer.
The practical rule is simple:
Create objects outside the block. Pass required behavior in through interfaces. Test the block with fake implementations.
