Create a Unit Test for QSerialPort class and create a Mock Device like to test connection to virtual port, read and write from it
-
Hello everyone, i'm new at testing and i'd like to share with you my project tha contains a SerialPort class that establish connection with serialPort and read and write certain data.
I have to create Unit test for this class to test different feature implementation like (connect to port, error handler, read fake data, write fake data...)SerialPort.h:
// SerialPort.h #ifndef SERIALPORT_H #define SERIALPORT_H #include <QObject> #include <QSerialPort> #include <QTimer> #include <QDateTime> #include <QMap> #include <QSet> #include <QString> #include <QList> #include <QPair> #include "Datamodels/realtimedatamodel.h"// Include the DataModel header #include "Datamodels/alarmesdatamodel.h" #include "Datamodels/controllerdatamodel.h" #include "Datamodels/batterystatedatamodel.h" #include <Datamodels/datamodels.h> #include "controllers/databasemanager.h" /** * @brief The SerialPort class handles serial port communication with scooter data. */ class SerialPort : public QObject { Q_OBJECT enum class KeyCategory { RealTime, Alarms, Controller, Battery, None }; public: /** * @brief Gets the instance of the SerialPort (Singleton pattern). * @return The instance of the SerialPort. */ static SerialPort& getInstance() { static SerialPort instance; // Create a single instance on the first call return instance; } ~SerialPort(); /** * @brief Sends data to QML. * @param key - The key for the data. * @param value - The value associated with the key. */ void dataToQml(const QString key, const QString value); /** * @brief Sends data to the WebSocket. * @param key - The key for the data. * @param value - The value associated with the key. */ void dataToWebSocket(const QString key, const QString value); public slots: /** * @brief Initializes the serial port. */ void initializeSerialPort(); /** * @brief Sends a message via the serial port. */ void sendMessage(); /** * @brief Reads data from the serial port. */ void readData(); /** * @brief Attempts to reconnect to the serial port. */ void attemptReconnect(); /** * @brief Handles errors in serial port communication. * @param error - The serial port error. */ void handleError(QSerialPort::SerialPortError error); /** * @brief Saves the distance information to the database. * @param distance - The distance information to save. */ //void saveDistanceToDatabase(double distance); void initializeAddressKeyMap(); void insertKeyValuePairs(const QList<QPair<QString, QString>>& keyValuePairs); void determineCategory(const QString &key, KeyCategory &category) const; void processData(const QString &key, const QString &stringValue, KeyCategory category); //void processData(const QString &key, const QString &stringValue, KeyCategory category); signals: /** * @brief Signal emitted when data is received from the serial port. * @param data - The received data. */ void dataReceived(const QString &data); /** * @brief Signal emitted when data is sent. * @param datasend - The data that was sent. */ void dataSend(const QString &datasend); /** * @brief Signal emitted when attempting to connect to the serial port. * @param message - The connection message. */ void connectToPort(const QString &message); private: /** * @brief Private constructor for the SerialPort (Singleton pattern). * @param parent - The parent QObject. */ SerialPort(QObject *parent = nullptr); /** * @brief Disable the copy constructor. */ SerialPort(const SerialPort&) = delete; /** * @brief Disable the assignment operator. */ SerialPort& operator=(const SerialPort&) = delete; // KeyCategory determineCategory(const QString &key) const; // DataModels m_dataModel; /**< An instance of the DataModel class. */ QSerialPort m_serialPort; /**< The QSerialPort instance for communication. */ QTimer m_reconnectTimerSerial; /**< Timer for attempting to reconnect to the serial port. */ // Map to store the address-key pairs QMap<QString, QString> addressKeyMap; QString namePort; KeyCategory m_category; DatabaseManager &dbManager = DatabaseManager::getInstance(); QByteArray data; QString dataHex; QString address; QString value; // Extract the third characters of the data without the delemeter QString key; QString stringValue; bool ok; float doubleValue ; const QSet<QString> allowedKeysRealTime = {"speed", "current", "voltage"}; const QSet<QString> allowedKeysAlarmes = {"temperatureAlarm", "voltageAlarm", "currentAlarm", "performanceAlarm"}; const QSet<QString> allowedKeysController = {"mode", "prnd", "lockMark", "brakeMark", "sideMark", "mcuOverTemperature", "motorOverTemperature", "overCurrentState", "overVoltageState", "underVoltageState"}; const QSet<QString> allowedKeysBattery = {"soc", "batteryTemperature", "bmsVoltage", "bmsCurrent", "bmsMaxDischargeCurrent", "bmsFaultLevel", "bmsChargeWarningLevel", "bmsDischargeWarningLevel", "bmsMaxCellVoltage", "bmsMinCellVoltage", "bmsIndexOfMaxCellVoltage", "bmsIndexOfMinCellVoltage", "chargingState", "dischargingState", "feedbackCurrent", "stateOfChargeMos", "stateOfDischargeMos", "stateOfPreDischargeMos", "stateOfBlockingChargeMos", "stateOfBlockingDischargeMos", "stateOfHeaterMos", "stateOfKeySignal", "stateOfChargeAcessSignal", "maxCellTemperature", "minCellTemperature", "mosCellTemperature", "remainCapacityOfTheBattery", "cycleCounter"}; bool m_errorHandled = false; /**< Flag to indicate if an error has been handled. */ // Create instances of your data models AlarmesDataModel alarmesdata ; BatteryStateDataModel batteryStateData ; RealTimeDataModel realTimeModel ; ControllerDataModel controllerModel ; // For testing purposes friend class Test_Serial_Port; }; #endif // SERIALPORT_H
SerialPort.cpp:
#include "serialport.h" //#include "MyWebSocketThread.h" #include <QtSql/QSqlDatabase> #include <QtSql/QSqlQuery> #include <QtSql/QSqlError> #include <QDebug> #include <QUrl> #include <QTimer> //#include "qdatetime.h" #include <QSerialPortInfo> #include <QtSerialPort> #include <QProcess> #include <QTextStream> #include <QSysInfo> #include <QTimer> #include <QThread> #include <QIODevice> SerialPort::SerialPort(QObject *parent) : QObject{parent} { dbManager.openDatabase("/scooter-dash/Database/Pixii.db"); realTimeModel.initialize(); controllerModel.createTable(); batteryStateData.createTable(); alarmesdata.createTable(); dbManager.registerDataModel(&alarmesdata); dbManager.registerDataModel(&realTimeModel); dbManager.registerDataModel(&controllerModel); dbManager.registerDataModel(&batteryStateData); // Connect to the error signal connect(&m_serialPort, &QSerialPort::errorOccurred, this, &SerialPort::handleError); //dataToQml("distance", QString::number(m_realtimedata.distanceGlobal)); m_reconnectTimerSerial.setInterval(2000); // Reconnect every 5 seconds (adjust as needed) m_reconnectTimerSerial.start(); connect(&m_reconnectTimerSerial, &QTimer::timeout, this, &SerialPort::attemptReconnect); initializeAddressKeyMap(); initializeSerialPort(); sendMessage(); } SerialPort::~SerialPort() { if (m_serialPort.isOpen()) m_serialPort.close(); } void SerialPort::handleError(QSerialPort::SerialPortError error) { if (error == QSerialPort::NoError || m_errorHandled) return; qDebug() << "Error occurred: " << m_serialPort.errorString(); emit connectToPort("Error"); m_errorHandled = true; // Mark error as handled m_reconnectTimerSerial.start(); } void SerialPort::attemptReconnect() { qDebug() << "Attempting to reconnect..."; m_serialPort.close(); // Close the serial port if it's still open initializeSerialPort(); // Reinitialize the serial port sendMessage(); // Reset error handled flag if reconnection is successful if (m_serialPort.isOpen()) { m_errorHandled = false; } } void SerialPort::sendMessage(){ // Send a message to give permission to send QByteArray message = "START"; m_serialPort.write(message); } void SerialPort::initializeSerialPort() { m_serialPort.setPortName("COM13"); m_serialPort.setBaudRate(QSerialPort::Baud115200); m_serialPort.setDataBits(QSerialPort::Data8); m_serialPort.setParity(QSerialPort::NoParity); m_serialPort.setStopBits(QSerialPort::OneStop); m_serialPort.setFlowControl(QSerialPort::NoFlowControl); // Connect to the readyRead signal connect(&m_serialPort, &QSerialPort::readyRead, this, &SerialPort::readData); // Open the serial port if (m_serialPort.open(QIODevice::ReadWrite)) { qDebug() << "Serial port opened successfully"; emit connectToPort("NoError"); m_reconnectTimerSerial.stop(); m_errorHandled = false; // Reset error handled flag } else { qDebug() << "Failed to open serial port" << m_serialPort.error(); } } void SerialPort::initializeAddressKeyMap() { insertKeyValuePairs({ {"07", "lockMark"}, {"08", "brakeMark"}, {"0a", "sideMark"}, {"0b", "prnd"}, {"0c", "mode"}, {"0d", "rpm"}, {"0e", "voltage"}, {"0f", "current"}, {"10", "mcuTemperature"}, {"11", "motorTemperature"}, {"15", "throttleSignal"}, {"16", "overCurrentState"}, {"17", "overVoltageState"}, {"18", "underCurrentState"}, {"19", "mcuOverTemperature"}, {"1a", "motorOverTemperature"}, {"1b", "speed"}, {"33", "soc"}, {"99", "batteryTemperature"}, {"1d", "bmsVoltage"}, {"1e", "bmsCurrent"}, {"1f", "bmsMaxDischargeCurrent"}, {"21", "bmsFaultLevel"}, {"22", "bmsChargeWarningLevel"}, {"23", "bmsDischargeWarningLevel"}, {"24", "bmsMaxCellVoltage"}, {"25", "bmsMinCellVoltage"}, {"26", "bmsIndexOfMaxCellVoltage"}, {"27", "bmsIndexOfMinCellVoltage"}, {"28", "chargingState"}, {"29", "dischargingState"}, {"2a", "feedbackCurrent"}, {"2b", "stateOfChargeMos"}, {"2c", "stateOfDischargeMos"}, {"2d", "stateOfPreDischargeMos"}, {"2e", "stateOfBlockingChargeMos"}, {"2f", "stateOfBlockingDischargeMos"}, {"30", "stateOfHeaterMos"}, {"31", "stateOfKeySignal"}, {"32", "stateOfChargeAcessSignal"}, {"34", "maxCellTemperature"}, {"35", "minCellTemperature"}, {"36", "mosCellTemperature"}, {"37", "remainCapacityOfTheBattery"}, {"38", "cycleCounter"}, {"70", "codeLightState"}, {"71", "farLightState"}, {"72", "rightBlinkerState"}, {"73", "leftBlinkerState"}, {"75", "dayLightState"} }); } void SerialPort::insertKeyValuePairs(const QList<QPair<QString, QString>>& keyValuePairs) { for (const auto &pair : keyValuePairs) { addressKeyMap.insert(pair.first, pair.second); } } void SerialPort::determineCategory(const QString &key, KeyCategory &category) const { if (allowedKeysRealTime.contains(key)) { category = KeyCategory::RealTime; } else if (allowedKeysAlarmes.contains(key)) { category = KeyCategory::Alarms; } else if (allowedKeysController.contains(key)) { category = KeyCategory::Controller; } else if (allowedKeysBattery.contains(key)) { category = KeyCategory::Battery; } else { category = KeyCategory::None; } } void SerialPort::processData(const QString &key, const QString &Value, KeyCategory category) { switch (category) { case KeyCategory::RealTime: realTimeModel.addData(key, Value); if (realTimeModel.hasAllRequiredKeys(allowedKeysRealTime)) { realTimeModel.insertData(); } break; case KeyCategory::Alarms: alarmesdata.addData(key, Value); if (alarmesdata.hasAllRequiredKeys(allowedKeysAlarmes)) { alarmesdata.insertData(); } break; case KeyCategory::Controller: controllerModel.addData(key, Value); if (controllerModel.hasAllRequiredKeys(allowedKeysController)) { controllerModel.insertData(); } break; case KeyCategory::Battery: batteryStateData.addData(key, Value); if (batteryStateData.hasAllRequiredKeys(allowedKeysBattery)) { batteryStateData.insertData(); } break; default: break; } } float transformValue(const QString &key, float value) { int decimalValue = static_cast<int>(value); // Check if the decimal value is negative if (decimalValue & 0x8000) { value = -(0x10000 - value); } // Adjust value based on key if (key == "current" || key == "voltage") { value = value*0.1; } return value; } void SerialPort::dataToQml(const QString key, const QString value){ emit dataReceived(QString(key) + ":" + QString(value)); } void SerialPort::dataToWebSocket(const QString key, const QString value){ QString dataToSend = "{\"type\":\"device_status\",\"event\":\"" + key + "\",\"payload\":{\"" + key + "\":"+ value.trimmed() + "}}"; //qDebug ()<< dataToSend; emit dataSend(dataToSend); } void SerialPort::readData() { while (m_serialPort.canReadLine()) { data = m_serialPort.readLine(); qDebug() << "data" << data; dataHex = data.toHex(); qDebug() << "dataHex" << dataHex; address = dataHex.mid(0, 2); // Extract the first two characters as the address value = dataHex.mid(2, dataHex.length()-6).trimmed(); // Extract the third characters of the data without the delemeter key = addressKeyMap.value(address); // qDebug() << "key" << key; // qDebug() << "value" << value; doubleValue = value.toInt(&ok, 16); if (ok) { qDebug() << "Float Value:" << doubleValue; transformValue(key,doubleValue); } else { qDebug() << "Conversion error for:" << value; } //qDebug() << "Address:" << address << "Value:" << value; stringValue = QString::number(doubleValue); determineCategory(key, m_category); processData(key, stringValue, m_category); dataToQml("distance", QString::number(realTimeModel.distanceGlobal)); dataToQml(key, stringValue); dataToWebSocket(key, stringValue); } }
I have created test_serialport and a mockserialport
#ifdef UNIT_SERIAL_TEST #include <QTest> #include "controllers/serialport.h" #include <QSignalSpy> #include <QTimer> #include "mockserialport.h" class Test_Serial_Port : public QObject { Q_OBJECT private slots: void initTestCase(); void cleanupTestCase(); //void testInitialization(); void testSendMessage(); //void testHandleError(); void testReadData(); // void testAttemptReconnect(); void testSerialPortOpen(); private: // Create SerialPort instance //SerialPort *serialPort; MockSerialPort *mockPort = new MockSerialPort; }; void Test_Serial_Port::initTestCase() { //serialPort = new SerialPort(); } void Test_Serial_Port::cleanupTestCase() { // delete serialPort; } void Test_Serial_Port::testSerialPortOpen() { // Use mock serial port instead of real one mockPort->open(QIODevice::ReadWrite); SerialPort m_serialPort(mockPort); // Simulate opening the serial port m_serialPort.initializeSerialPort(); QVERIFY(mockPort->isOpen()); // Check if the port is open } void Test_Serial_Port::testSendMessage() { //SerialPort serialPort; // Mock serial port mockPort->writeData("START", 5); SerialPort m_serialPort(mockPort); // Send message m_serialPort.sendMessage(); // Verify that the message was written to the mock serial port QCOMPARE(mockPort->writtenData, QByteArray("START")); } void Test_Serial_Port::testReadData() { SerialPort m_serialPort(mockPort); QSignalSpy spy(&m_serialPort, &SerialPort::dataReceived); // Simulate incoming data mockPort->setReadData("MockData\n"); // Wait for the dataReceived signal QVERIFY(spy.wait(100)); // Verify that dataReceived was emitted with correct parameters QCOMPARE(spy.count(), 1); QList<QVariant> arguments = spy.takeFirst(); QCOMPARE(arguments.at(0).toString(), QString("someKey")); QCOMPARE(arguments.at(1).toString(), QString("MockData")); } QTEST_MAIN(Test_Serial_Port) #include "test_serialport.moc" #endif
#include "mockserialport.h" MockSerialPort::MockSerialPort(QObject *parent) : QIODevice(parent) { } bool MockSerialPort::open(OpenMode mode) { setOpenMode(mode); return true; // Simulate successful port opening } void MockSerialPort::close() { setOpenMode(NotOpen); } bool MockSerialPort::isOpen() const { return openMode() != NotOpen; } qint64 MockSerialPort::writeData(const char *data, qint64 len) { writtenData.append(QByteArray(data, len)); // Store written data for verification emit readyRead(); // Simulate data being ready to read return len; } void MockSerialPort::setReadData(const QByteArray &data) { m_readDataBuffer.append(data); emit readyRead(); // Simulate data available for reading } qint64 MockSerialPort::readData(char *data, qint64 maxlen) { qint64 len = qMin(maxlen, qint64(readDataBuffer.size())); memcpy(data, readDataBuffer.constData(), len); readDataBuffer.remove(0, len); // Remove data that was read return len; }
The problem here is that it still want to connect with a real port not virtual port.
I want a solution to create a unit test using mock to create like a virtual port and try use cases. -
Hi,
If you want to to mock QSerialPort you have to make it replaceable in your class.
Use a QIODevice pointer as member and either add a setter or a constructor parameter so you can use either QSerialPort or your own mock device.
This means that you need to refactor your logic a bit to setup the port.
On a side note, your class has two different things that look suspicious:- It seems to be doing way too many things seeing its internal and the class name
- singleton implementation more often than not are architectural issues.