Skip to content
  • Categories
  • Recent
  • Tags
  • Popular
  • Users
  • Groups
  • Search
  • Get Qt Extensions
  • Unsolved
Collapse
Brand Logo
  1. Home
  2. Special Interest Groups
  3. C++ Gurus
  4. Create a Unit Test for QSerialPort class and create a Mock Device like to test connection to virtual port, read and write from it

Create a Unit Test for QSerialPort class and create a Mock Device like to test connection to virtual port, read and write from it

Scheduled Pinned Locked Moved Unsolved C++ Gurus
2 Posts 2 Posters 382 Views
  • Oldest to Newest
  • Newest to Oldest
  • Most Votes
Reply
  • Reply as topic
Log in to reply
This topic has been deleted. Only users with topic management privileges can see it.
  • S Offline
    S Offline
    Sakhana99
    wrote on 11 Sept 2024, 16:02 last edited by
    #1

    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.

    1 Reply Last reply
    0
    • S Offline
      S Offline
      SGaist
      Lifetime Qt Champion
      wrote on 11 Sept 2024, 17:21 last edited by
      #2

      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.

      Interested in AI ? www.idiap.ch
      Please read the Qt Code of Conduct - https://forum.qt.io/topic/113070/qt-code-of-conduct

      1 Reply Last reply
      1

      2/2

      11 Sept 2024, 17:21

      • Login

      • Login or register to search.
      2 out of 2
      • First post
        2/2
        Last post
      0
      • Categories
      • Recent
      • Tags
      • Popular
      • Users
      • Groups
      • Search
      • Get Qt Extensions
      • Unsolved