I have a project (service) where my code is in a library which will be dynamically opened by a process from another project (framework) that I can't modify. I want to find a way to trigger the tests even though I can't modify the main().
The framework (not my code) implements a fmk_process which finds a service_lib.so (containing my code) from a configuration. Many such service libraries can exist for many different applications/backends. As part of fmk_process's initialization, it will call an extern "C" ServiceInit(...); from service_lib.so which purpose is to register callbacks using templates provided by the framework. It will complete other initialization tasks after that.
Then, the normal interaction with fmk_process is to send a payload on a port it listens to, the internal logic will find the callback and invoke it with the payload. Also, the payload is custom and has all its constructors private or = delete in the framework...
how am I supposed to test my service ?
simplified example code to understand:
framework.h (visible to service projects)
class Service;
class CustomPayload {
public:
CustomPayload() = delete; // other constructors/factories are not available either
};
class PayloadInvoker {
public:
virtual void invoke(Service& service, CustomPayload& payload) = 0;
virtual std::shared_ptr<Service> createService() = 0;
};
class IRegistry {
public:
virtual ~IRegistry() = default;
virtual void register(std::string& serviceName, std::shared_ptr<PayloadInvoker> pInvoker) = 0;
virtual void invoke(std::string& serviceName, CustomPayload& payload) = 0;
};
class Registry {
public:
Registry();
~Registry() {}
template <typename T> register(std::string& serviceName, void (T::*method)(CustomPayload& payload));
void invoke(std::string& serviceName, CustomPayload& payload);
private:
template <typename> class PayloadInvokerTemplate;
std:shared_ptr<IRegistry> _pImpl;
};
template <typename T> Registry::PayloadInvokerTemplate : public PayloadInvoker {
public:
PayloadInvokerTemplate(void (T::*method)(CustomPayload& payload)) : _method(method) {}
std::shared_ptr<Service> createService() { return std::make_shared<T>(); }
protected:
void invoke(Service& service, CustomPayload& payload) {
(static_cast<T&>(service).*_method)(payload);
}
void (T::*_method)(CustomPayload& payload);
};
// the template which is actually used by the service_lib.so code
template <typename T> Registry::register(std::string& serviceName, void (T::*method)(CustomPayload& payload)) {
this->_pImpl->register(serviceName, std::make_shared<PayloadInvokerTemplate<T>>(method));
}
framework.cpp (not visible to service projects)
#include "framework.h" // there are other includes obviously
class RegistryImpl : public IRegistry {
public:
void register(std::string& serviceName, std::shared_ptr<PayloadInvoker> pInvoker) override {
this->_map[serviceName] = pInvoker;
}
void invoke(std::string& serviceName, CustomPayload& payload) override {
if (this->_map.contains(serviceName)) {
PayloadInvoker* pInvoker = this->_map[serviceName].get();
std::shared_ptr<Service> pService = pInvoker->createService();
pInvoker->invoke(*pService, payload);
}
}
private:
std::unordered_map<std::string, std::shared_ptr<PayloadInvoker> _map;
};
Registry::Registry() { _pImpl = std::shared_ptr<IRegistry>(new RegistryImpl()); }
void Registry::invoke(std::string& serviceName, CustomPayload& payload) {
this->_pImpl->invoke(serviceName, payload);
}
other hidden framework code for fmk_process
int main() {
// dlopen on service_lib.so
// dlsym on "ServiceInit"
// call ServiceInit() with an instance of Registry
// other init
// listen on port
// call registryInstance.invoke(...)
}
now my code service.cpp, resulting in service_lib.so
class MyService1 : public Service {
public:
void MyCallback1(std::string& serviceName, CustomPayload& payload) { /* do something */ }
void MyCallback2(std::string& serviceName, CustomPayload& payload) { /* do something else */ }
};
class MyService2 : public Service {
public:
void MyCallback3(std::string& serviceName, CustomPayload& payload) { /* do something */ }
void MyCallback4(std::string& serviceName, CustomPayload& payload) { /* do something else */ }
};
extern "C" ServiceInit(Registry registry) {
registry.register<MyService1>("service1", &MyService1::MyCallback1);
registry.register<MyService1>("service2", &MyService1::MyCallback2);
registry.register<MyService2>("service3", &MyService2::MyCallback3);
registry.register<MyService2>("service4", &MyService2::MyCallback4);
}
// somewhere, somehow
TEST(MyServices) {
EXPECT_TRUE(/* all works */);
}
// particularly this part
::testing::InitGoogleTest();
RUN_ALL_TESTS()
So how can I trigger those tests ?
In my CI, I could start fmk_process and make it find service_lib.so, but how am I supposed to call RUN_ALL_TESTS() ?
If I have to send a payload on the port using an external process like curl, I don't consider it a unit test, and I don't want to write a testing library relying on exchanging requests/responses.
What I'm not showing here is that I'm actually extending the framework with my own templates using the framework templates. So I need to test that callbacks registered through my templates can be reached properly by fmk_process, and particularly, I need to test new versions of the framework and make sure that the templates in my lib are still compatible. If I were to reproduce and mock the framework behavior, I would not be able to test that they are still compatible when the framework evolves (which happens quite often).