I am writing some unit tests for a C++ project.
As part of this work, I want to write some unit tests which mock the behaviour of a socket
object. More precicely, I am trying to write an test for a function which depends on recv
. In other words, the function I wish to write a test for calls recv
.
The typical way to write a test with a dependency such as this would be to find a runtime dispatch solution. There are at least two straightforward approaches.
- Write an interface class with two derived implementation classes. One of these implementations is the "real" implementation. It wraps
recv
. The other is a "fake" or mocked implementation. Virtual function calls and dynamic dispatch at runtime provide the solution here. - Use a
std::variant
(or similar). In this case, a simpleif
statement can be used to switch between real and mock function calls at runtime.
My thoughts are that there may be a better solution which does not require runtime dispatch. It might be possible to use constexpr
, or some other compile time solution, to switch between calling two implementations of a function.
This might have to be combined with dependency injection. In this case, the dependency may be a type T
injected using a template parameter.
To make things more concrete, below is a sketch for an implementation of some existing code which has not yet had unit tests added to it.
ssize_t do_logic(
const int sock_fd,
const std::unique_ptr<uint8_t[]> &p_buffer,
const size_t buffer_size
) {
// imagine there is some code block here
{
// code block A
}
ssize_t recv_size = recv(sock_fd, p_buffer.get(), buffer_size, 0);
// imaging there is some code block here
{
// code block B
}
return recv_size;
}
int main() {
int sock_fd = socket(AF_INET, SOCK_STREAM, 0);
// this is all just MWE noise
sockaddr_in server_address;
std::memset(&server_address, 0, sizeof(server_address));
server_address.sin_family = AF_INET;
server_address.sin_addr.s_addr = htonl(INADDR_ANY);
server_address.sin_port = htons(1234);
bind(sock_fd, reinterpret_cast<sockaddr*>(&server_address), sizeof(server_address));
listen(sock_fd, 10);
const size_t buffer_size = 1024;
const auto p_buffer = std::make_unique<uint8_t[]>(buffer_size);
do_logic(sock_fd, p_buffer, buffer_size);
return 0;
}
Things to note:
- The function
do_logic
has a dependency onrecv
. (A function from sockets library.) do_logic
also has some additional code which I am trying to avoid repeating. (DRY) These are represented by the code blocksblock A
andblock B
.
Here is an example main
function for a unit test.
int main() {
std::string sock_data = "Hello World. This is some example data for unit testing."
const size_t buffer_size = 1024;
const auto p_buffer = std::make_unique<uint8_t[]>(buffer_size);
do_logic(sock_data, p_buffer, buffer_size);
return 0;
}
So far all is well. We can write an implementation of do_logic
which uses function overloading. (Compile time.) However, this is where things go wrong.
ssize_t do_logic(
std::string sock_data,
const std::unique_ptr<uint8_t[]> &p_buffer,
const size_t buffer_size
) {
// imagine there is some code block here
{
// code block A
}
ssize_t recv_size = recv(sock_data, p_buffer.get(), buffer_size);
// imaging there is some code block here
{
// code block B
}
return recv_size;
}
The problem here is a violation of DRY. While this will work, it is not ideal to have to maintain a synchronization of two code blocks across two different files in a code base.
In this case, block A
and block B
now existing in two different files. Those files are probably in very different places in the source tree, since one of these files is for unit testing, while the other is for production code.
As an asside, we can of course write an implementation of recv(std::string, ... etc ...)
. (Not shown here, but it would probably call std::copy
, or something.)
Is there a solution to this problem which does not require resorting to runtime dispatch?
My initial thoughts were that perhaps a template parameter could be used to write an implementation of do_logic
. This is a kind of dependency injection, where the type is injected into the function.
template<typename T>
ssize_t do_logic(
T sock,
const std::unique_ptr<uint8_t[]> &p_buffer,
const size_t buffer_size
) {
// imagine there is some code block here
{
// code block A
}
constexpr if ( /* ? */ ) {
// this `recv`: the `recv` from the sockets library
ssize_t recv_size = recv(sock, p_buffer.get(), buffer_size, 0);
}
else {
// this `recv`: an implementation of a function `recv(std::string, ...etc...)`
// which we write. It could be a null implementation, or it could call
// `std::copy`.
//
// ssize_t recv(std::string, const auto& p_buffer, const auto buffer_size) { return 0; }
//
ssize_t recv_size = recv(sock, p_buffer.get(), buffer_size);
}
// imaging there is some code block here
{
// code block B
}
return recv_size;
}
For the line constexpr if ( /* ? */ )
, what I want to do is something like
constexpr if ( T isa typename int ) { }
else if ( T isa typename std::string ) { }
else {
static_assert<false>("T must be either type int or type std::string");
}
However, I have no idea how to implement that. I am vaguely aware that C++ 20 (?) introduced concepts. However, from my initial reading into concepts, my interpretation was that concepts are a more abstract idea which groups related types together. I'm not sure if it is the right tool for the job?
To be honest, there may be a simpler approach than the idea I have proposed here. It is quite likely that I am taking this in the wrong direction. Feedback on this would be much appreciated. Thank you in advance.
I am writing some unit tests for a C++ project.
As part of this work, I want to write some unit tests which mock the behaviour of a socket
object. More precicely, I am trying to write an test for a function which depends on recv
. In other words, the function I wish to write a test for calls recv
.
The typical way to write a test with a dependency such as this would be to find a runtime dispatch solution. There are at least two straightforward approaches.
- Write an interface class with two derived implementation classes. One of these implementations is the "real" implementation. It wraps
recv
. The other is a "fake" or mocked implementation. Virtual function calls and dynamic dispatch at runtime provide the solution here. - Use a
std::variant
(or similar). In this case, a simpleif
statement can be used to switch between real and mock function calls at runtime.
My thoughts are that there may be a better solution which does not require runtime dispatch. It might be possible to use constexpr
, or some other compile time solution, to switch between calling two implementations of a function.
This might have to be combined with dependency injection. In this case, the dependency may be a type T
injected using a template parameter.
To make things more concrete, below is a sketch for an implementation of some existing code which has not yet had unit tests added to it.
ssize_t do_logic(
const int sock_fd,
const std::unique_ptr<uint8_t[]> &p_buffer,
const size_t buffer_size
) {
// imagine there is some code block here
{
// code block A
}
ssize_t recv_size = recv(sock_fd, p_buffer.get(), buffer_size, 0);
// imaging there is some code block here
{
// code block B
}
return recv_size;
}
int main() {
int sock_fd = socket(AF_INET, SOCK_STREAM, 0);
// this is all just MWE noise
sockaddr_in server_address;
std::memset(&server_address, 0, sizeof(server_address));
server_address.sin_family = AF_INET;
server_address.sin_addr.s_addr = htonl(INADDR_ANY);
server_address.sin_port = htons(1234);
bind(sock_fd, reinterpret_cast<sockaddr*>(&server_address), sizeof(server_address));
listen(sock_fd, 10);
const size_t buffer_size = 1024;
const auto p_buffer = std::make_unique<uint8_t[]>(buffer_size);
do_logic(sock_fd, p_buffer, buffer_size);
return 0;
}
Things to note:
- The function
do_logic
has a dependency onrecv
. (A function from sockets library.) do_logic
also has some additional code which I am trying to avoid repeating. (DRY) These are represented by the code blocksblock A
andblock B
.
Here is an example main
function for a unit test.
int main() {
std::string sock_data = "Hello World. This is some example data for unit testing."
const size_t buffer_size = 1024;
const auto p_buffer = std::make_unique<uint8_t[]>(buffer_size);
do_logic(sock_data, p_buffer, buffer_size);
return 0;
}
So far all is well. We can write an implementation of do_logic
which uses function overloading. (Compile time.) However, this is where things go wrong.
ssize_t do_logic(
std::string sock_data,
const std::unique_ptr<uint8_t[]> &p_buffer,
const size_t buffer_size
) {
// imagine there is some code block here
{
// code block A
}
ssize_t recv_size = recv(sock_data, p_buffer.get(), buffer_size);
// imaging there is some code block here
{
// code block B
}
return recv_size;
}
The problem here is a violation of DRY. While this will work, it is not ideal to have to maintain a synchronization of two code blocks across two different files in a code base.
In this case, block A
and block B
now existing in two different files. Those files are probably in very different places in the source tree, since one of these files is for unit testing, while the other is for production code.
As an asside, we can of course write an implementation of recv(std::string, ... etc ...)
. (Not shown here, but it would probably call std::copy
, or something.)
Is there a solution to this problem which does not require resorting to runtime dispatch?
My initial thoughts were that perhaps a template parameter could be used to write an implementation of do_logic
. This is a kind of dependency injection, where the type is injected into the function.
template<typename T>
ssize_t do_logic(
T sock,
const std::unique_ptr<uint8_t[]> &p_buffer,
const size_t buffer_size
) {
// imagine there is some code block here
{
// code block A
}
constexpr if ( /* ? */ ) {
// this `recv`: the `recv` from the sockets library
ssize_t recv_size = recv(sock, p_buffer.get(), buffer_size, 0);
}
else {
// this `recv`: an implementation of a function `recv(std::string, ...etc...)`
// which we write. It could be a null implementation, or it could call
// `std::copy`.
//
// ssize_t recv(std::string, const auto& p_buffer, const auto buffer_size) { return 0; }
//
ssize_t recv_size = recv(sock, p_buffer.get(), buffer_size);
}
// imaging there is some code block here
{
// code block B
}
return recv_size;
}
For the line constexpr if ( /* ? */ )
, what I want to do is something like
constexpr if ( T isa typename int ) { }
else if ( T isa typename std::string ) { }
else {
static_assert<false>("T must be either type int or type std::string");
}
However, I have no idea how to implement that. I am vaguely aware that C++ 20 (?) introduced concepts. However, from my initial reading into concepts, my interpretation was that concepts are a more abstract idea which groups related types together. I'm not sure if it is the right tool for the job?
To be honest, there may be a simpler approach than the idea I have proposed here. It is quite likely that I am taking this in the wrong direction. Feedback on this would be much appreciated. Thank you in advance.
Share Improve this question edited Mar 2 at 13:51 康桓瑋 43.6k5 gold badges63 silver badges127 bronze badges asked Mar 2 at 13:32 user2138149user2138149 17.7k30 gold badges150 silver badges296 bronze badges 3 |1 Answer
Reset to default 1The problem here is a violation of DRY.
Create sub-functions then:
void blockA(/*..*/)
{
// imagine there is some code block here
{
// code block A
}
}
void blockB(/*..*/)
{
// imagine there is some code block here
{
// code block B
}
}
then
ssize_t do_logic(
const int sock_fd,
const std::unique_ptr<uint8_t[]> &p_buffer,
const size_t buffer_size
) {
blockA(/*...*/);
ssize_t recv_size = recv(sock_fd, p_buffer.get(), buffer_size, 0);
blockB(/*...*/);
return recv_size;
}
ssize_t do_logic(
std::string sock_data,
const std::unique_ptr<uint8_t[]> &p_buffer,
const size_t buffer_size
) {
blockA(/*...*/);
ssize_t recv_size = recv(sock_data, p_buffer.get(), buffer_size);
blockB(/*...*/);
return recv_size;
}
The typical way to write a test with a dependency such as this would be to find a runtime dispatch solution. There are at least two straightforward approaches.
Another erase-type useful in that regard is std::function
For compile type, template
is a way to go:
// Mockable method
template <typename RECV_FUNC, typename SOCK>
ssize_t do_logic(
RECV_FUNC f, // std::function<ssize_t (SOCK, uint8_t*, size_t)> to be runtime
SOCK sock_data,
const std::unique_ptr<uint8_t[]> &p_buffer,
const size_t buffer_size
) {
blockA(/*...*/);
ssize_t recv_size = f(sock_data, p_buffer.get(), buffer_size);
blockB(/*...*/);
return recv_size;
}
// Regular entry
ssize_t do_logic(
const int sock_fd,
const std::unique_ptr<uint8_t[]> &p_buffer,
const size_t buffer_size
) {
return do_logic([](auto... args){ return recv(args...); },
sock_fd, p_buffer, buffer_size);
}
// Simple mocked entry (currently noop)
ssize_t do_logic(
std::string sock_data,
const std::unique_ptr<uint8_t[]> &p_buffer,
const size_t buffer_size
) {
return do_logic([](auto&... args){ /*..*/return 0; },
sock_data, p_buffer, buffer_size);
}
发布者:admin,转转请注明出处:http://www.yc00.com/questions/1745121570a4612444.html
if constexpr (std::is_same_v<T, std::string>) { ... }
. That said, if you already have an overloadrecv(std::string, ...)
, then you don't need anyif constexpr
. Just callrecv(sock, ...)
and the overload resolution would pick the right function. – Igor Tandetnik Commented Mar 2 at 15:00std::ostream
as a poster child example. If the function takesvoid do_something(std::ostream& out) { /*...*/ }
you can pass in yourstd::ofstream
for the real code, andstd::ostringstram
as your mock argument in your unit tests. This is how you get polymorphic reuse and adhere to open/close principle fordo_something
: it can work with futureostream
objects that didn't exist when the function was written. It is decoupled from the concreteostream
. – Eljay Commented Mar 2 at 15:17unique_ptr
by const reference seems strange, you might passstd::span<uint8_t>
instead. – Jarod42 Commented Mar 3 at 8:57