> ## Content Index
> Fetch the complete content index at: https://www.smallstepsystems.com/llms.txt
> Use this file to discover other available public pages before exploring further.

# A Mock QCanBusDevice for TDD
- URL: https://www.smallstepsystems.com/a-mock-qcanbusdevice-for-tdd/
- Published: 2022-02-21T19:18:43.000Z
- Updated: 2026-01-03T21:51:31.000Z
- Author: Burkhard Stubert
- Tags: Continuous Delivery

I am using TDD to implement a hardware-independent mock `QCanBusDevice`. We can use `MockCanBusDevice` in unit, integration and acceptance tests - without access to a hardware CAN bus. A terminal application can also use it in its own process to simulate the communication with a machine.

## Motivation

The machine component in the hexagonal architecture of a harvester terminal comes with three adapers:

- The *Machine* *adapter* communicates over 1-4 CAN busses with the other ECUs of the harvester.
- The *In-Process Simulator* *adapter* runs on the development computer in the same process as the terminal application, simulates the ECUs and communicates over function calls with the terminal applicaton.
- The *Mock adapter* is used by unit, integration and acceptance tests to simulate or mock the communication with the ECUs.

![Hexagonal Architecture for harvester terminal with four ports and multiple adapters per port.](https://storage.ghost.io/c/c7/ee/c7ee1cf5-b86f-491b-aab7-20ad081598e1/content/images/2025/11/harvester-terminal-hexagonal-architecture-1.png)

Hexagonal Architecture for harvester terminal

Unit tests must be able to force a CAN bus device into all kinds of exceptional scenarios. This is very difficult or nearly impossible for real CAN bus devices. Unit tests must also run very quickly (best in less than 3 seconds) to enable the many tiny steps of TDD. Hence, accessing sockets, files or databases is not an option. Therefore, unit tests do not use the hardware-dependent `QCanBusDevice` but a hardware-independent mock one

The `MockCanBusDevice` is both a *mock* and a *fake* (see p. 114 of James Grenning's book [Test-Driven Development for Embedded C](https://wingman-sw.com/test-driven-development-for-embedded-c-book?ref=smallstepsystems.com)). As a fake, it provides a partial and simplified implementation of a SocketCAN device. As a mock, it allows tests to control its behaviour. For example, tests can configure the mock so that reading or writing a frame fails or succeeds.

`MockCanBusDevice` is used by the in-process simulator and the mock adapter of the Machine component. It makes writing unit, integration and acceptance tests much easier and running these tests much faster. I am using TDD to implement a MockCanBusDevice that resembles the SocketCAN implementation of `[QCanBusDevice](https://doc.qt.io/qt-6/qcanbusdevice.html?ref=smallstepsystems.com)`. You find the example code [in the GitHub repository eu-terminal-apps](https://github.com/bstubert/eu-terminal-apps/tree/mock-can-bus-device/eu-machine/tests/can%5Fbus%5Fdevice?ref=smallstepsystems.com).

## Creating the Mock QCanBusDevice

We create a test skeleton with a single test `testCreateCanBusDevice` for creating a CAN bus device. The test always passes.

We instantiate the class `MockCanBusDevice` in the test function:

```
// File: test_can_bus_device.cpp
#include "mock_can_bus_device.h"

void TestCanBusDevice::testCreateCanBusDevice()
{
    MockCanBusDevice canBus;
}
```

We provide the minimal implementation of the class `MockCanBusDevice` that makes the test compile and hence pass. The class declaration is enough, as the compiler creates the default constructor (Commit: 5be529c).

We derive `MockCanBusDevice` from the abstract class `QCanBusDevice` and add to *CMakeLists.txt* the `Qt5::SerialBus` module, which contains all the CAN classes.

```
// File: mock_can_bus_device.h
#include <QCanBusDevice>

class MockCanBusDevice : public QCanBusDevice
{
};
```

As expected, the compiler complains that the class `MockCanBusDevice` is abstract and that the pure virtual methods `writeFrame`, `interpretErrorFrame`, `open` and `close` have not been implemented. In QtCreator, we place the cursor in the word `QCanBusDevice`, hit *Alt + Enter* and select *Insert Virtual Functions of Base Classes*. As the dialog has all the pure virtual functions selected, we just click on *OK* and let QtCreator generate the declarations and definitions of the pure virtual functions. We add the missing return statements.

```
// File: mock_can_bus_device.cpp

bool MockCanBusDevice::writeFrame(const QCanBusFrame &frame)
{
    return false;
}

QString MockCanBusDevice::interpretErrorFrame(const QCanBusFrame &errorFrame)
{
    return {};
}

bool MockCanBusDevice::open()
{
    return false;
}

void MockCanBusDevice::close()
{
}
```

The test now compiles and passes.

## Connecting to the Mock QCanBusDevice

After creating the `MockCanBusDevice`, we call `[QCanBusDevice::connectDevice](https://doc.qt.io/qt-6.2/qcanbusdevice.html?ref=smallstepsystems.com#connectDevice)` to connect the device with the real CAN bus. True to its name, `MockCanBusDevice` will mock the real CAN bus.

The non-virtual function `connectDevice` calls the overridden function `MockCanBusDevice::open` in the device state `ConnectingState`. If `open` succeeds, it sets the state to `ConnectedState` and returns `true`. Otherwise, `open` returns `false`, connectDevice sets the state to `UnconnectedState` and returns immediately with `false`. We get this information from a look at the implementation of `QCanBusDevice::connectDevice` in *qtserialbus/src/serialbus/qcanbusdevice.cpp* and at the implementation of `SocketCanBackend::open` in *qtserialbus/src/plugins/canbus/socketcan/socketcanbackend.cpp*.

The test `testConnectionSucceeded` checks whether the `device` can connect to the (mock) CAN bus successfully when unconnected.

```
void TestCanBusDevice::testConnectionSucceeded()
{
    MockCanBusDevice device;
    QCOMPARE(device.state(), QCanBusDevice::UnconnectedState);
    QVERIFY(device.connectDevice());
}
```

The test fails, because `open` returns `false`. The simplest fix to make `open` return `true`.

```
bool MockCanBusDevice::open()
{
    return true;
}
```

If the connection succeeds, the `device` must be in the state `QCanBusDevice::ConnectedState`.

```
void TestCanBusDevice::testConnectionSucceeded()
{
    MockCanBusDevice device;
    QVERIFY(device.connectDevice());
    QCOMPARE(device.state(), QCanBusDevice::ConnectedState);
}
```

The test fails, because the `device` is in `QCanBusDevice::ConnectingState`. The fix is a one-liner.

```
bool MockCanBusDevice::open()
{
    setState(QCanBusDevice::ConnectedState);
    return true;
}
```

We remove `testCreateCanBusDevice`, as all test functions will create a `MockCanBusDevice`.

Clients of MockCanBusDevice must handle the case, where the connection succeeds as well as where it fails. In the failure case, client tests must be able to control the return value of the function `open`. The test `testConnectionFailed` introduces this capability:

```
void TestCanBusDevice::testConnectionFailed()
{
    MockCanBusDevice device;
    device.setOpenSucceeded(false);
    QVERIFY(!device.connectDevice());
    QCOMPARE(device.state(), QCanBusDevice::UnconnectedState);
}
```

The function `setOpenSucceeded` stores its argument in the member variable `m_openSucceeded` (default: `true`). If `open` is expected to fail (`m_openSucceeded == false`), it immediately returns with `false`. If open is expected to succeed (`m_openSucceeded == true`), it sets the state to `ConnectedState` and returns `true`.

```
bool MockCanBusDevice::open()
{
    if (!m_openSucceeded)
    {
        return false;
    }
    setState(QCanBusDevice::ConnectedState);
    return true;
}
```

Both tests pass now.

A further look at the implementation reveals that `QCanBusDevice::connectDevice` returns with `false` without calling `open`, if the state is not `UnconnectedState` on entrance. Additionally, the function outputs a warning message and sets the error to `ConnectionError`. The resulting state is the same as on entrance. `ConnectionError` is also set, if the `open` call fails.

We must run `testConnectionFailed` on different combinations of `open`'s return value and the state before calling `connectDevice` and check the result for the correct state and error. This is best done by [data-driven tests](https://doc.qt.io/qt-6/qttestlib-tutorial2-example.html?ref=smallstepsystems.com). The data-driven version of `testConnectionFailed` looks as follows.

```
void TestCanBusDevice::testConnectionFailed_data()
{
    QTest::addColumn<QCanBusDevice::CanBusDeviceState>("stateBefore");
    QTest::addColumn<bool>("openSucceeded");
    QTest::addColumn<QCanBusDevice::CanBusDeviceState>("stateAfter");
    QTest::addColumn<QCanBusDevice::CanBusError>("canError");

    QTest::newRow("Unconnected + false -> Unconnected + NoError")
            << QCanBusDevice::UnconnectedState << false
            << QCanBusDevice::UnconnectedState << QCanBusDevice::ConnectionError;
}

void TestCanBusDevice::testConnectionFailed()
{
    QFETCH(QCanBusDevice::CanBusDeviceState, stateBefore);
    QFETCH(bool, openSucceeded);
    QFETCH(QCanBusDevice::CanBusDeviceState, stateAfter);
    QFETCH(QCanBusDevice::CanBusError, canError);

    MockCanBusDevice device;
    device.setState(stateBefore);
    device.setOpenSucceeded(openSucceeded);
    QVERIFY(!device.connectDevice());
    QCOMPARE(device.state(), stateAfter);
    QCOMPARE(device.error(), canError);
}
```

The code doesn't compile, because `QCanBusDevice::setState` is protected. We reimplement it as a public function in `MockCanBusDevice`. The above test compiles but fails on the last `QCOMPARE`. We fix this by setting the error in the failure case of the `open` function.

```
bool MockCanBusDevice::open()
{
    if (!m_openSucceeded)
    {
        setError(u"Open failed"_qs, QCanBusDevice::ConnectionError);
        return false;
    }
    setState(QCanBusDevice::ConnectedState);
    return true;
}
```

We now put the data-driven test to good use and add more rows to `testConnectionFailed_data`.

```
void TestCanBusDevice::testConnectionFailed_data()
{
    ...
    QTest::newRow("Connected + true -> Connected + ConnectionError")
            << QCanBusDevice::ConnectedState << true
            << QCanBusDevice::ConnectedState << QCanBusDevice::ConnectionError;
    QTest::newRow("Connecting + false -> Connecting + ConnectionError")
            << QCanBusDevice::ConnectingState << false
            << QCanBusDevice::ConnectingState << QCanBusDevice::ConnectionError;
    QTest::newRow("Closing + true -> Closing + ConnectionError")
            << QCanBusDevice::ClosingState << true
            << QCanBusDevice::ClosingState << QCanBusDevice::ConnectionError;
}
```

We don't add the row *"Unconnected + true"*, because this would be the same as `testConnectionSucceeded`. We could add the three rows *"Connected + false"*, *"Connecting + true"* and *"Closing + false"* to cover all possible combinations. I didn't, because these three rows would add hardly any value. The rows would not test `MockCanBusDevice` but `QCanBusDevice`, which is not our objective.

## Disconnecting from the Mock QCanBusDevice

The function [QCanBusDevice::disconnect](https://doc.qt.io/qt-6.2/qcanbusdevice.html?ref=smallstepsystems.com#disconnectDevice) calls the pure virtual function [QCanBusDevice::close](https://doc.qt.io/qt-6.2/qcanbusdevice.html?ref=smallstepsystems.com#close), which is overridden by `MockCanBusDevice::close`. A look at the implementation of `QCanBusDevice::disconnectDevice` reveals the exact behaviour.

If the device is in `ConnectedState`, `disconnectDevice` sets the state to `ClosingState` and calls the virtual function `close`, which sets the state to `UnconnectedState`. We add this good case to the end of `testConnectionSucceeded` and rename the test function into `testConnectAndDisconnectDevice`.

```
void TestCanBusDevice::testConnectAndDisconnectDevice()
{
    MockCanBusDevice device;
    QCOMPARE(device.state(), QCanBusDevice::UnconnectedState);

    QVERIFY(device.connectDevice());
    QCOMPARE(device.state(), QCanBusDevice::ConnectedState);

    device.disconnectDevice();
    QCOMPARE(device.state(), QCanBusDevice::UnconnectedState);
}
```

The test fails, because `close` does nothing so far. Let us change this.

```
void MockCanBusDevice::close()
{
    setState(QCanBusDevice::UnconnectedState);
}
```

We must check what happens when `disconnectDevice` is called in the other three states. If the device is `Connecting`, `disconnectDevice` behaves the same as for `Connected`. If the device is `Unconnected` or `Closing`, `disconnectDevice` outputs the warning *"qt.canbus: Can not disconnect an unconnected device"* and returns without calling `close`. The state on exit is the same as on entrance. No error is reported. This demands another data-driven test.

```
void TestCanBusDevice::testDisconnectDevice_data()
{
    QTest::addColumn<QCanBusDevice::CanBusDeviceState>("stateBefore");
    QTest::addColumn<QCanBusDevice::CanBusDeviceState>("stateAfter");

    QTest::newRow("Connecting -> Unconnected")
            << QCanBusDevice::ConnectingState << QCanBusDevice::UnconnectedState;
    QTest::newRow("Closing -> Closing")
            << QCanBusDevice::ClosingState << QCanBusDevice::ClosingState;
    QTest::newRow("Unconnected -> Unconnected")
            << QCanBusDevice::UnconnectedState << QCanBusDevice::UnconnectedState;
}

void TestCanBusDevice::testDisconnectDevice()
{
    QFETCH(QCanBusDevice::CanBusDeviceState, stateBefore);
    QFETCH(QCanBusDevice::CanBusDeviceState, stateAfter);

    MockCanBusDevice device;
    device.setState(stateBefore);
    device.disconnectDevice();
    QCOMPARE(device.state(), stateAfter);
    QCOMPARE(device.error(), QCanBusDevice::NoError);
}
```

## Writing To the Mock QCanBusDevice

If everything is alright, the function `SocketCanBackend::writeFrame` writes the given frame to the CAN bus without any queuing, emits the signal `framesWritten(1)` and returns `true`. Let us get the implementation of `MockCanBusDevice::writeFrame` rolling with this test.

```
void TestCanBusDevice::testWriteFrame()
{
    MockCanBusDevice device;
    QSignalSpy spy{&device, &QCanBusDevice::framesWritten};
    QVERIFY(device.writeFrame(QCanBusFrame{}));
    QCOMPARE(spy.count(), 1);
}
```

The test fails, because `writeFrame` is hard-wired to return `false` and because it doesn't emit the signal `framesWritten`. The fix is easy.

```
bool MockCanBusDevice::writeFrame(const QCanBusFrame &frame)
{
    emit framesWritten(1);
    return true;
}
```

We know that a frame was written but not which frame. We make `MockCanBusDevice` record all the frames written and later also the frames read. Tests of `MockCanBusDevice`'s clients can check whether a request (a written frame) sees the right response (a read frame). We extend `testWriteFrame` a little bit.

```
void TestCanBusDevice::testWriteFrame()
{
    MockCanBusDevice device;
    QSignalSpy spy{&device, &QCanBusDevice::framesWritten};
    QVERIFY(device.writeFrame(QCanBusFrame{}));
    auto frames = device.recordedFrames();
    QCOMPARE(frames.count(), 1);
    QCOMPARE(spy.count(), frames.count());
    QCOMPARE(frames[0].toString(), QCanBusFrame{}.toString());
}
```

As the test writes one frame, the `device` must record one frame as well (1st `QCOMPARE`). Every time `writeFrame` is called, `device` emits the signal `framesWritten(1)` (2nd `QCOMPARE`). The recorded frame must be the same as the written frame (3rd `QCOMPARE`). As `QCanBusFrame` lacks an equality operator, we compare the string representations of the frames.

The function `MockCanBusDevice::writeFrame` appends the written frame to the vector `m_recordedFrames` of `QCanBusFrame`s. Clients can access the recorded frames through the function `recordedFrames`.

```
bool MockCanBusDevice::writeFrame(const QCanBusFrame &frame)
{
    m_recordedFrames.append(frame);
    emit framesWritten(1);
    return true;
}
```

The SocketCAN implementation of QCanBusDevice handles a couple of error conditions.

- When the device is not connected, the function `writeFrame` returns with `false` immediately without setting any error.
- When `writeFrame` fails to write the frame to the socket representing the real CAN bus, it flags a `WriteError` and returns `false`.
- When `writeFrame` tries to write an invalid frame, it flags a `WriteError` and returns `false`.
- When `writeFrame` tries to write a frame in flexible data-rate (FD) format but FD is not enabled, it flags a `WriteError` and returns `false`.

I'll only look at the first two error conditions in this post. You'll find the third one in the example code. I won't implement the fourth one, as none of the CAN devices, with which I have been working so far, has used FD. Let us add the test for writing a CAN frame when the device is not connected.

```
void TestCanBusDevice::testWriteNoFrameWhenNotConnected()
{
    MockCanBusDevice device;
    QVERIFY(!device.writeFrame(QCanBusFrame{}));
}
```

This test fails, because `writeFrame` always returns `true`. We make this test pass by adding a bouncing condition.

```
bool MockCanBusDevice::writeFrame(const QCanBusFrame &frame)
{
    if (state() != ConnectedState)
        return false;
    m_recordedFrames.append(frame);
    emit framesWritten(1);
    return true;
}
```

`testWriteNoFrameWhenNotConnected` passes but `testWriteFrame` fails. We fix this failure by connecting the device before calling `writeFrame`.

```
void TestCanBusDevice::testWriteFrame()
{
    MockCanBusDevice device;
    QSignalSpy spy{&device, &QCanBusDevice::framesWritten};
    device.connectDevice();
    QVERIFY(device.writeFrame(QCanBusFrame{}));
    ...
```

We complete `testWriteNoFrameWhenNotConnected` by checking that the signal `framesWritten` is never emitted and that no frames were recorded.

The next test handles the error scenario, where writing to the socket representing the real CAN bus fails. Reasons could be that the CAN bus is overloaded or that the CAN connector has a defect. In the failure case, `writeFrame` flags a `WriteError`, returns false, does not record the frame and does not emit the `framesWritten` signal.

We must introduce a flag `m_writeSucceeded` similar to `m_openSucceeded`. The flag allows test functions to control whether the write fails or succeeds.

```
void TestCanBusDevice::testWriteFrameFails()
{
    MockCanBusDevice device;
    device.connectDevice();
    device.setWriteSucceeded(false);
    QVERIFY(!device.writeFrame(QCanBusFrame{}));
    QCOMPARE(device.error(), QCanBusDevice::WriteError);
}
```

We implement `setWriteSuceeded` similar to `setOpenSucceeded` and extend `writeFrame` by a new if-clause.

```
bool MockCanBusDevice::writeFrame(const QCanBusFrame &frame)
{
    if (state() != ConnectedState)
        return false;
    if (!m_writeSucceeded)
    {
        setError(u"Writing frame failed"_qs, QCanBusDevice::WriteError);
        return false;
    }
    m_recordedFrames.append(frame);
    emit framesWritten(1);
    return true;
}
```

Again, we complete the test by checking that the signal `framesWritten` is never emitted and that no frames were recorded.

## Reading From the Mock QCanBusDevice

`QCanBusDevice`s notify their clients like the test functions through the signal `framesReceived` that new CAN frames have arrived. The signal `framesReceived` is emitted by the protected function `QCanBusDevice::enqueueReceivedFrames`. When clients receive the signal framesReceived, they consume the frames with `readFrame`, `framesAvailable` and `readAllFrames`.

A function `receiveFrames`, which simulates or mocks the reception of some CAN frames, adds the received frames to the incoming queue and `enqueueReceivedFrames` does the rest as with frames received from the real CAN bus. Let us start with the following test.

```
void TestCanBusDevice::testReadFramesOneByOne()
{
    MockCanBusDevice device;
    QSignalSpy spy{&device, &QCanBusDevice::framesReceived};
    device.connectDevice();
    device.receiveFrames({frame1, frame2});
    QCOMPARE(spy.count(), 1);
}
```

`frame1` and `frame2` are valid frames defined as constant member variables of `TestCanBusDevice`. The test passes once we provide the following implementation for `receiveFrames`.

```
void MockCanBusDevice::receiveFrames(const QVector<QCanBusFrame> &frames)
{
    enqueueReceivedFrames(frames);
}
```

We strengthen the test by checking the number of available frames before and after reading a single frame with `readFrame`.

```
void TestCanBusDevice::testReadFramesOneByOne()
    ...
    QCOMPARE(device.framesAvailable(), 2);
    auto frame = device.readFrame();
    QCOMPARE(frame.toString(), frame1.toString());

    QCOMPARE(device.framesAvailable(), 1);
    frame = device.readFrame();
    QCOMPARE(frame.toString(), frame2.toString());

    QCOMPARE(device.framesAvailable(), 0);
```

We test `readAllFrames` in pretty much the same way.

```
void TestCanBusDevice::testReadAllFrames()
{
    MockCanBusDevice device;
    QSignalSpy spy{&device, &QCanBusDevice::framesReceived};
    device.connectDevice();
    device.receiveFrames({frame1, frame2});
    QCOMPARE(spy.count(), 1);

    QCOMPARE(device.framesAvailable(), 2);
    auto frames = device.readAllFrames();
    QCOMPARE(frames[0].toString(), frame1.toString());
    QCOMPARE(frames[1].toString(), frame2.toString());
    QCOMPARE(device.framesAvailable(), 0);
}
```

In contrast to the pure virtual function `QCanBusDevice::writeFrame`, `QCanBusDevice::readFrame` and `QCanBusDevice::readAllFrames` are non-virtual functions and already perform much of the error handling. They bail out immediately with an `OperationError` if the device is not in `ConnectedState`. `readFrame` doesn't flag any error but returns an invalid frame, if it is called on an empty queue of incoming frames. We don't have to test these error situations, because the tests of `QCanBusDevice` do it already.

This leaves us with one error situation: reading from the socket representing the real CAN bus fails. We introduce a flag `m_readSucceeded` similar to `m_writeSucceeded` and control the flag through the function `setReadSucceeded`. The following test gets the implementation rolling.

```
void TestCanBusDevice::testReceivingFramesFails()
{
    MockCanBusDevice device;
    device.connectDevice();
    device.setReadSucceeded(false);
    device.receiveFrames({frame1, frame2});
    QCOMPARE(device.error(), QCanBusDevice::ReadError);
    QCOMPARE(device.framesAvailable(), 0);
}
```

We extend `receiveFrames` by the error case.

```
void MockCanBusDevice::receiveFrames(const QVector<QCanBusFrame> &frames)
{
    if (!m_readSucceeded)
    {
        setError(u"Reading frames failed"_qs, QCanBusDevice::ReadError);
        return;
    }
    enqueueReceivedFrames(frames);
}
```

The equivalent of `receiveFrames` in the SocketCAN implementation (`SocketCanBackend::readSocket`) works the same in principle. It flags a `ReadError` and returns without enqueueing the received frames, if it encounters a problem reading from the CAN socket.