1
\$\begingroup\$

The situation

I recently started a cross platform QT project (arm, linux-x86, windows) that aims to interact with CAN-Bus hardware. I want to learn and get used to unit testing from scratch as good as possible while working on that project.

As I have a very limited experience in writing unit tests as well as writing well-designed code to test, it is challenging for me to design my code well, especially because I constantly interact with low level hardware. This requires mocking or emulating as well as unit testing.

In my main can bus class, which I do not want to talk about here (yet), I need to interface a low level C library libsocketcan. Thus I wrote a little wrapper class that works well.

Now I think, that little class would be perfect to learn good testable design.

The testing and mocking framework I use is Googletest, however I think that's not that important from a general point of view when discussing testable design.

My goals

  • I want to be able to unit test my SocketCan-class itself in an elegant way. So I hope for helpful reviews that might lead to a good re-design for the class. Probably I have to mock the class somehow to make unit tests working reasonable for it.

  • When I use that SocketCan-class as a dependency in my main class which acts as a high-level abstraction layer for CAN, I also want the testability of that class not to be reduced by the usage of the SocketCan class.

  • I want to unit test the whole class, even on a platform, where SocketCan is not available (Windows). How to design it properly for that goal?

My thoughts

I am reading a lot regarding unit testing and mocking. For example, there is an article about not to mock, what you do not own. So do I have to wrap my wrapper class again to get it mockable easily?

Should I even unit test that SocketCan class at all or does it in your eyes not provide enough functionality to make tests for it?

Bonus topic: I am a bit doubtful whether my implementation of the cross platform ability is a reasonable way to go. I have that single generic header file that I use for all platforms, and two different implementation *.cpps, that are selected to compile by the build system, depending of the target platform.

My Code

mycanbus_socketcan.h

#ifndef MYCANBUS_SOCKETCAN_H
#define MYCANBUS_SOCKETCAN_H

class CANLIBSHARED_EXPORT SocketCan {
    Q_GADGET
public:
    enum SocketCanState {
        ErrorActive,
        ErrorWarning,
        ErrorPassive,
        BusOff,
        Stopped,
        Sleeping,
        RequestFailed
    };
    Q_ENUM(SocketCanState)

    static bool prepareInterface(const QString interface, const int baudrate);

private:
    static QByteArray getInterfaceNameFromQString(const QString interfaceName);
    static SocketCanState getState(const QString interface);
    static bool setBitrate(const QString interface, const int baudrate);
    static bool interfaceUp(const QString interface);
    static bool interfaceDown(const QString interface);
};
#endif // MYCANBUS_SOCKETCAN_H

mycanbus_socketcan_windows.cpp

#include "mycanbus_socketcan.h"
#include <QCoreApplication>

Q_LOGGING_CATEGORY(lcSocketCan, "my.can.socketcan")

bool SocketCan::PrepareInterface(const QString interface, const int baudrate)
{
    qCInfo(lcSocketCan) << QCoreApplication::translate("", "No socketcan implementation for your operating system."
                                                        " Ignoring interface %1, baudrate %2")
                            .arg(interface)
                            .arg(baudrate);
    return false;
}

mycanbus_socketcan_linux.cpp

#include "libsocketcan.h"
#include "mycanbus_socketcan.h"
#include <QCoreApplication>
#include <QMetaEnum>

Q_LOGGING_CATEGORY(lcSocketCan, "my.can.socketcan")

bool SocketCan::prepareInterface(const QString interface, const int baudrate)
{
    bool result;
    SocketCanState state;
    // Shutting down, reconfiguring, bringing up, state check
    result = interfaceDown(interface);

    state = getState(interface);

    if (state == SocketCanState::RequestFailed){
        qCWarning(lcSocketCan) << QCoreApplication::translate("","Could not get the current state of interface %1, aborting.").arg(interface);
        return false;
    }

    result = setBitrate(interface, baudrate);
    if (result == false){
        qCWarning(lcSocketCan) << QCoreApplication::translate("","Could not set baudrate %1 for interface %2").arg(baudrate).arg(interface);
        return false;
    }

    result = interfaceUp(interface);
    if (result == false){
        qCWarning(lcSocketCan) << QCoreApplication::translate("","Could not bring interface %1 up.").arg(interface);
        return false;
    }

    state = getState(interface);
    if (state == SocketCanState::RequestFailed){
        qCWarning(lcSocketCan) << QCoreApplication::translate("","Could not get the current state of interface %1, aborting.").arg(interface);
        return false;
    }else
    {
        return true;
    }
}

SocketCan::SocketCanState SocketCan::getState(const QString interface)
{
    //Checking for the interface state
    int libSocketCanState;
    SocketCanState state = SocketCanState::RequestFailed;
    int callSuccessfully = can_get_state(getInterfaceNameFromQString(interface), &libSocketCanState);
    if (callSuccessfully != 0) {
        qCWarning(lcSocketCan) << QCoreApplication::translate("", "Socketcan state request failed for interface %1").arg(interface);
        state = RequestFailed;
    } else {
        switch (libSocketCanState) {
        case CAN_STATE_ERROR_ACTIVE:
            state = ErrorActive;
            break;
        case CAN_STATE_ERROR_WARNING:
            state = ErrorWarning;
            break;
        case CAN_STATE_ERROR_PASSIVE:
            state = ErrorPassive;
            break;
        case CAN_STATE_BUS_OFF:
            state = BusOff;
            break;
        case CAN_STATE_STOPPED:
            state = Stopped;
            break;
        case CAN_STATE_SLEEPING:
            state = Sleeping;
            break;
        }
    }
    QMetaEnum stateEnum = QMetaEnum::fromType<SocketCan::SocketCanState>();
    qCDebug(lcSocketCan) << QCoreApplication::translate("", "Socketcan state for interface %1 is: %2").arg(interface).arg(QString(stateEnum.name()) + "::" + stateEnum.valueToKey(state));
    return state;
}

bool SocketCan::setBitrate(const QString interface, const int baudrate)
{
    qCDebug(lcSocketCan) << QCoreApplication::translate("", "Trying to set baudrate %1 for interface %2").arg(baudrate).arg(interface);
    if (can_set_bitrate(getInterfaceNameFromQString(interface), baudrate) == 0) {
        qCDebug(lcSocketCan) << QCoreApplication::translate("", "Baudrate set successfully");
        return true;
    } else {
        qCWarning(lcSocketCan) << QCoreApplication::translate("", "Baudrate could not be set");
        return false;
    }
}

bool SocketCan::interfaceUp(const QString interface)
{
    qCDebug(lcSocketCan) << QCoreApplication::translate("", "Trying to bring interface %1 up").arg(interface);
    if (can_do_start(getInterfaceNameFromQString(interface)) == 0) {
        qCDebug(lcSocketCan) << QCoreApplication::translate("", "Interface brought up successfully");
        return true;
    } else {
        qCWarning(lcSocketCan) << QCoreApplication::translate("", "Interface could not be brought up!");
        return false;
    }
}

bool SocketCan::interfaceDown(const QString interface)
{
    qCDebug(lcSocketCan) << QCoreApplication::translate("", "Trying to shut interface %1 down").arg(interface);
    if (can_do_stop(getInterfaceNameFromQString(interface)) == 0) {
        qCDebug(lcSocketCan) << QCoreApplication::translate("", "Interface shut down successfully");
        return true;
    } else {
        qCWarning(lcSocketCan) << QCoreApplication::translate("", "Interface could not be shut down!");
        return false;
    }
}

QByteArray SocketCan::getInterfaceNameFromQString(const QString interfaceName){
    QByteArray ba = interfaceName.toLocal8Bit();
    return ba;
}

libsocketcan.h

3rd party: https://github.com/lalten/libsocketcan/blob/master/include/libsocketcan.h

\$\endgroup\$
  • 1
    \$\begingroup\$ It looks like you're asking for a review of parts of the code (the unit tests) that you haven't shown us. If you want a review of the tests, you really do need to include them in the question! On the other hand, if your tests aren't yet complete and you want advice, you're too early for Code Review - we need completed, working code for review. \$\endgroup\$ – Toby Speight Dec 7 '18 at 15:21
  • \$\begingroup\$ Thanks, I understand your point. However, where would be the right place to ask for such a specific advice? The above code is working, so I thought it could be reviewed as it is, with having in mind to prepare it for a later use in unit tests. I might have been wrong on this. \$\endgroup\$ – darkmattercoder Dec 8 '18 at 10:58
2
\$\begingroup\$

This answer doesn't particularly address your questions, but it does talk about some generic C++ stuff. To make this clearer, I'm going to divide it between 'Feedback' and 'Opinion'.

Feedback

When passing const QString interface, pass it as a reference. Nearly all class instances should be passed as references.

#include "mycanbus_socketcan.h"
#include <QCoreApplication>


bool result;
SocketCanState state;
// Shutting down, reconfiguring, bringing up, state check
result = interfaceDown(interface);

state = getState(interface);

should simply be

bool result = interfaceDown(interface);
SocketCanState state = getState(interface);

It's not (old) C, so initialize and declare things where they're used, not at the beginning of the function.

if (state == SocketCanState::RequestFailed){
    qCWarning(lcSocketCan) << QCoreApplication::translate("","Could not get the current state of interface %1, aborting.").arg(interface);
    return false;
}else
{
    return true;
}

The else here is not needed. Simply return true, because the previous block will have already returned false. This happens elsewhere in your code as well.

Opinion

#ifndef MYCANBUS_SOCKETCAN_H

All modern C++ compilers support #pragma once. I prefer to use it. You can weigh the pros and cons.

In my opinion, system header includes should be done before user includes. C++ is order-sensitive when it comes to includes.

\$\endgroup\$
  • 2
    \$\begingroup\$ There is significant debate here, here and even in the description here on Wikipedia about #pragma once as well as debate about include order. \$\endgroup\$ – bruglesco Dec 8 '18 at 1:52
  • \$\begingroup\$ thanks, I am not sure about #pragma once. Each argument against or towards its usage has a point. As my IDE will generate the ifdef guards from a template while class creation, I think I will stay with them for now. Regarding the include order, there are strong discussions also and my clang format standard beautifier re-arranges the includes based on the webkit guidelines. If I omit the else, clang static analysis as well as the compiler warns me about potential issues, so I included it. \$\endgroup\$ – darkmattercoder Dec 8 '18 at 11:00
  • 1
    \$\begingroup\$ @bruglesco I've made that clearer in my answer, that some of these points are only opinion. \$\endgroup\$ – Reinderien Dec 12 '18 at 15:38
  • \$\begingroup\$ accepted your answer for the time being. Currently refactoring with your suggestions and looking forward to post a more concise question soon. \$\endgroup\$ – darkmattercoder Dec 18 '18 at 14:59

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.