-
Notifications
You must be signed in to change notification settings - Fork 8
Tutorial: Mocking hardware with GMock
Hardware mocking allows us to test our code right up to the point where it requires hardware functionality. It is generally a 3-step process as described below. This short tutorial will demonstrate how to mock a UART peripheral. The code is highly simplified; in reality, all code should be documented and the interface would include several other functions.
First, we create an interface class, also called an abstract class in C++. An interface class has public methods, and provides and implementation for none of them (except the destructor since it is mandatory). All the methods it defines are virtual, meaning that child classes will have to provide an implementation for them. An interface is a contract, and what we mean by this is that any code which needs to use a UART will be able to use the interface without knowing how the methods are implemented. That is, all the code which will use the hardware only has knowledge of the interface. This is key! For example, any code can call the transmit function below and know what it does without needing to know how it works.
There is only a header file associated with this class.
UartInterface.h
class UartInterface{
public:
virtual ~UartInterface() {}
virtual HAL_StatusTypeDef transmitPoll(
const UART_HandleTypeDef* uartHandlePtr,
uint8_t* arrTransmit,
size_t numBytes,
uint32_t timeout
) const = 0;
virtual HAL_StatusTypeDef receivePoll(
const UART_HandleTypeDef* uartHandlePtr,
uint8_t* arrReceive,
size_t numBytes,
uint32_t timeout
) const = 0;
};
Next, we create a class which inherits from the interface. This new class, UartInterfaceImpl, provides a concrete implementation for the interface. We use UartInterfaceImpl in the actual runtime code. For example, if we write the following code, we should notice the real hardware UART module transmitting "hello world!".
#include "usart.h"
UartInterfaceImpl uart1;
char msg[] = "hello world!;
uart1.transmit(&huart1, static_cast<uint8_t*>(msg), sizeof(msg), 10);
There is a header file and a cpp file associated with this implementation class.
UartInterfaceImpl.h
#include "UartInterface.h"
class UartInterfaceImpl : public UartInterface{
public:
UartInterfaceImpl ();
~UartInterfaceImpl ();
HAL_StatusTypeDef transmitPoll(
const UART_HandleTypeDef* uartHandlePtr,
uint8_t* arrTransmit,
size_t numBytes,
uint32_t timeout
) const override final;
HAL_StatusTypeDef receivePoll(
const UART_HandleTypeDef* uartHandlePtr,
uint8_t* arrReceive,
size_t numBytes,
uint32_t timeout
) const override final;
};
UartInterfaceImpl.cpp
UartInterfaceImpl::UartInterfaceImpl(){
}
UartInterfaceImpl::~UartInterfaceImpl(){
}
HAL_StatusTypeDef UartInterfaceImpl::transmitPoll(
const UART_HandleTypeDef* uartHandlePtr,
uint8_t* arrTransmit,
size_t numBytes,
uint32_t timeout
) const
{
HAL_StatusTypeDef status = HAL_UART_Transmit(
const_cast<UART_HandleTypeDef*>(uartHandlePtr),
arrTransmit,
numBytes,
timeout
);
return status;
}
HAL_StatusTypeDef UartInterfaceImpl::receivePoll(
const UART_HandleTypeDef* uartHandlePtr,
uint8_t* arrReceive,
size_t numBytes,
uint32_t timeout
) const
{
HAL_StatusTypeDef status = HAL_UART_Receive(
const_cast<UART_HandleTypeDef*>(uartHandlePtr),
arrReceive,
numBytes,
timeout
);
return status;
}
The mock class uses GMock to create stub implementations for the methods in the interface. These implementations are completely generic; GMock has no idea how the function is used in your application, but it does know its argument and return types. But don't let this get you down: you can instruct the mock class to behave however you'd like through the mock framework. You can tell it what to return, you can tell it which of its methods it should expect to be called in a certain test, and much more.
The mock class is implemented in a single header file.
MockUartInterface.h
#include "UartInterface.h"
class MockUartInterface : public UartInterface{
public:
MOCK_CONST_METHOD4(
transmitPoll,
HAL_StatusTypeDef(
const UART_HandleTypeDef*,
uint8_t*,
size_t,
uint32_t
)
);
MOCK_CONST_METHOD4(
receivePoll,
HAL_StatusTypeDef(
const UART_HandleTypeDef*,
uint8_t*,
size_t,
uint32_t
)
);
};
Suppose we have a UartDriver class which is basically a facade for the UartInterface. It is a facade because it only provides transmit() and receive() methods while the UartInterface may provide transmitPoll(), transmitIT(), transmitDMA(), etc. Users of the UartDriver would not need to understand how it talks with the UartInterface, but they know it takes care of their hardware calls for them.
UartDriver.h
#include "UartInterface.h"
class UartDriver{
public:
UartDriver(UartInterface* uartif);
bool transmit(uint8_t* arr, size_t arrSize);
bool receive(uint8_t* buff, size_t buffSize);
private:
const UartInterface* m_uartif;
};
In a unit test, we would initialize this UartDriver as follows:
#include "UartDriver.h"
#include "MockUartInterface.h"
MockUartInterface mUart;
UartDriver driver(&mUart);
...
driver.transmit(...);
In production code, we would initialize this UartDriver as follows:
#include "UartDriver.h"
#include "UartInterfaceImpl .h"
UartInterfaceImpl uartImpl;
UartDriver driver(&uartImpl);
...
driver.transmit(...);
As we can see, after initializing the driver to use either the mock or concrete implementation, all other interactions with the driver are completely generic. In fact, any interaction with either the concrete or the mock UartInterface will happen directly through calls to the base class—the interface. Since these methods are all virtual, their invocations will be dispatched to MockUartInterface in the case of the unit test, and dispatched to UartInterfaceImpl in the case of the production code.