Closed panmareksadowski closed 4 years ago
Third idea: 3) Maybe is it possible to mock connection_provider instead of ozo::request method?
Hi!
The best thing you can do is to make an abstraction for the library API. The goal of the library is providing functionality with a universal API but not DB abstraction for user software since typically API use cases of a standalone project are just a subset of all the use cases covered by the library API. Assume your software uses callback-style asynchronous code, so you do not need to support all the other types of completion token, and in most cases, you do not need to support associated executor and associated allocator. And so on. So the abstraction you need is simpler than the library API. And this is the place where you can make stubs or mocks for interaction with a DB without supporting any additional functionality. You shall not try to mock the library API - it is just a waste of time.
If you want to use dynamic polymorphism for the abstraction layer - try to use std::variant
as a type erasure for polymorphic arguments of your abstraction. At least you know all the types in your project.
For the static polymorphism, you can find some examples of mocking Boost.Asio entities in the library tests with help of internal customisation points (e.g. see: asio.h and test_asio.h).
Does it make sense?
Hi!
Thank you for your response.
Of course, I don't need to mock library API and I use some abstraction for the library API. How you said my application will not need that general interface as a library API offer and can eliminate some static polymorphism. However, is one part that is difficult to specialize, it is result of the request. It can be done by creating another interface method for every database request or at least every request that returns different columns types. It is how I more or less did it already, but it is not perfect as it needs to write a special interface for something that could be done in general.
Returning something like std::variant to avoid static polymorphism is how it is done in libpqxx. I also thought about to use something like std::variant to return the result in my interface to get rid of static polymorphism, but it looks that to do it I would need to deal with raw untyped data(by ozo::result) and convert to desired type myself.
Thanks for links to how is it done in Boost.Asio. I will try to analyze it and take an example of them.
There are two type-erasures in OZO: ozo::binary_query
and ozo::result
. So you can use them with your internal abstractions to support dynamic polymorphism.
but it looks that to do it I would need to deal with raw untyped data(by ozo::result) and convert to desired type myself.
You can always use ozo::recv_result
function to convert ozo::result
to the particular result type. It is not covered by the documentation yet but you can look at the unit tests.
Hope that helps. If you have any additional questions - you are welcome. With best regards, Sergei
Thanks for your interest in my issue. It helps a lot.
I end up with something like that:
class DbManager {
public:
explicit DbManager(boost::asio::io_service &io_service) { //initilize connectionProvider
}
class Result {
public:
Result(ozo::result &&res) : res(std::move(res))
template <typename Out> Out get() {
Out out;
auto oid_map = ozo::empty_oid_map();
ozo::recv_result(res, oid_map, std::back_insert_iterator(out));
return out;
}
private:
ozo::result res;
};
virtual Result request(ozo::binary_query &&query, const spawn::yield_context &yield) {
ozo::result result;
auto connection = ozo::request(connectionProvider, query, std::ref(result), yield);
return Result(std::move(result));
}
private:
MyConcreteConnectionProviderType connectionProvider;
};
It is fine. I used types-erasure as you advised me which eliminated templates from request method. It allows me to make request method virtual and then mock it in tests.
I miss one puzzle to make it all work in my tests. It is serialization typed data to ozo::result
which is in fact pointer to pg_result
. I found in tests binary serialization methods: tests, but it does not help a lot in my case. These functions serialize directly to a binary stream data and I didn't find a way how to map it to pg_result
.
So finally what I need is the inverse function to ozo::recv_result
. Then I can serialize data that I expect to receive in tests and return them in the mocked request method.
Thanks again for your interest in my issue.
Hi!
It looks like you may need to use std::any inside the Result:
class Result {
public:
template <typename T>
Result(T &&res) : res(std::forward<T>(res)){}
template <typename Out> Out get() {
if (res.type() == typeid(Out)) {
return std::any_cast<Out>(any);
}
if (res.type() != typeid(ozo::result)) {
throw std::logic_error("bad result type"); //or better something more meaningful
}
Out out;
auto oid_map = ozo::empty_oid_map();
ozo::recv_result(std::any_cast<ozo::result&>(res), oid_map, std::back_insert_iterator(out));
return out;
}
private:
std::any res;
};
So, in this case, you can provide exactly the type you need from your mock. I do not like this solution much, but for the dynamic polymorphism, this is the clear and straightforward solution IMO.
Thanks for your response.
I have been tried something similar as you have shown. Ozo::result can not be in std::any, because of deleted copy constructor, but it is not a problem. It is quite easy to resolve. However, what I don't like in this solution is that mocking part needs to be in production code. I'm still looking for a better solution. I will let you know if I come up with something better.
Thanks again for your help.
Yes, that's the problem, I didn't pay attention that the copyable is necessary. I do think you will be forced to create your own version of the std::any
which is not good. You can make an std::shared_ptr<ozo::result>
but it may be not an efficient solution. The other thing - we can make something like:
auto shared_result = result.share();
and such shared result will be copyable and may be used with std::any
. It should be relatively easy to implement.
Hi Marek!
You can try to use #240 with std::any
.
Hi,
This solution works, but it mixing production code with testing code. I still look for some better way to not obscure the production code by code needed for testing.
What I am missing is to serialize expected data to ozo::result
(which is a wrapper to pg_result
). I can serialize one field using send
function(link), but I still don't know how to expand it to serialize the entire result(table). Maybe do you have some hints?
What I am missing is to serialize expected data to ozo::result(which is a wrapper to pg_result). I can serialize one field using send function(link), but I still don't know how to expand it to serialize the entire result(table). Maybe do you have some hints?
IMO this is the wrong way. You are mixing your code with some hacks under the tests so you are testing not only the production code but hacks with libpq and OZO internals. We are using static polymorphism (ozo::basic_result<T>
), for this case are mocking pg_result
with a dedicated mock object. So as you can see we have customization points are used in tests and created for tests. But this is ok because in the other way you have no tests at all or starting to make some hacks around the external library. So IMO std::any
approach for the dynamic polymorphism is much better comparing to hacking around pg_result
.
Well another variant
namespace test {
class result_mock;
}
class DbManager {
public:
explicit DbManager(boost::asio::io_service &io_service) { //initilize connectionProvider
}
class Result {
public:
Result(ozo::result &&res) : res(std::move(res))
template <typename Out> Out get() {
return std::visit([&](auto& result) {
Out out;
auto oid_map = ozo::empty_oid_map();
recv_result(res, oid_map, std::back_insert_iterator(out));
return out;
});
}
private:
std::variant<ozo::result, test::result_mock*> res;
};
virtual Result request(ozo::binary_query &&query, const spawn::yield_context &yield) {
ozo::result result;
auto connection = ozo::request(connectionProvider, query, std::ref(result), yield);
return Result(std::move(result));
}
private:
MyConcreteConnectionProviderType connectionProvider;
};
So you can dispatch mock object through the std::visit
. This is still better than hacking around binary raw buffers and pg_result
.
Finally, I decided to use link seams and "shadow" request function in tests by a linker(look here). It allows us to not interpose testing code into production code.
There were 2 other approaches:
Feel free to comment if you have any suggestions or comments for this approach.
It is very desirable to have a possibility of stubbing or mocking database requests for tests and as the request is a template it is not an easy task.
Is it any recommended way to do this by yandex?
After some research, I found 2 ways to do this: 1) Write a specialized interface for every specialization of method request what I will need to use.
2)Write a wrapper that takes std::Function object as request implementation.
Then it can be passed wrapped by lambda ozo::request for production code
or stub implementation for tests.
Do you have any better solutions than these above?