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 theSocketCan
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 *.cpp
s, 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