diff --git a/src/lib/test/mockinjector.hpp b/src/lib/test/mockinjector.hpp index cc39ba0bd..0b308ec19 100644 --- a/src/lib/test/mockinjector.hpp +++ b/src/lib/test/mockinjector.hpp @@ -20,6 +20,40 @@ */ +/** @file mockinjector.hpp + ** Support for unit-testing with a mock implementation of some singleton service. + ** Using a Singleton to access a global service (instead of using a dependency injection + ** framework) has the downside of making unit-tests hard to write. When the code to be + ** tested accesses the global service, usually, several further services must be available. + ** Usually this (and the lack of flexibility) counts as the main arguments against using + ** Singletons. Yet still our current analysis (as of 2011) for Lumiera is that this + ** drawback doesn't justify all the overhead of writing our own dependency-injection + ** container (given the lack of pre-existing suitable C++ implementations). + ** + ** lib::test::MockInjector provides a mechanism to overcome this problem. Given + ** a Singleton service, it allows to install a subclass of the service interface + ** temporarily as a mock implementation. As an additional convenience shortcut, + ** the Use4Test template can be used as a scoped variable to automate this process + ** of installing and removing the mock service within the call scope of a single test + ** method or test object. + ** + ** \par prerequisites + ** The Singleton needs to be managed and accessed through Lumiera's lib::Singleton + ** factory. Moreover, an explicit template specialisation to use MockInjector instead + ** of the plain lib::SingletonFactory needs to be given globally, for the service interface + ** to use. For this template specialisation, a forward declaration of the service is + ** sufficient. See singleton-preconfigure.hpp for examples. + ** + ** The Mock implementation of the service needs to be default constructible and + ** assignment compatible to the service interface. Moreover, the mock implementation class + ** must expose a typedef \c ServiceInterface pointing to this interface. The instance of + ** the mock service implementation will be created in heap memory and managed by a static + ** smart-ptr variable; it will be destroyed when leaving the scope marked by Use4Test. + ** + ** @see SingletonFactory + ** @see SingletonTestMock_test + ** @see MediaAccessMock_test usage example + */ #ifndef LUMIERA_TEST_MOCKINJECTOR_H @@ -31,10 +65,15 @@ #include #include +#include #include namespace lib { + template + class Singleton; // forward decl; see singleton-preconfigure + + namespace test{ using boost::scoped_ptr; @@ -49,7 +88,7 @@ namespace test{ template class MockInjector : public SingletonFactory { - scoped_ptr mock_; + static scoped_ptr mock_; public: /** Overwriting the normal Singleton creation Interface @@ -74,32 +113,59 @@ namespace test{ MockInjector () {}; - /** @note MockInjector singleton factory objects can be copied, - * but the copy will start out with clean internal state, - * i.e. exhibiting normal SingletonFactory behaviour - * without mock object. - */ - MockInjector (const MockInjector& other) - : SingletonFactory(other), mock_(0) { } - - MockInjector& operator= (const MockInjector& other) - { - return SingletonFactory::operator= (other); - } + // MockInjector objects may be copied freely (no object fields) }; + /** embedded static var to hold onto a configured mock implementation */ + template + scoped_ptr MockInjector::mock_; - /** - * Details of creating and accessing mock instances, - * especially for services implemented as Singleton. - */ + + + + + /** Details of creating and accessing mock instances, * + * especially for services implemented as Singleton. */ namespace mock { using boost::enable_if; using lumiera::Yes_t; using lumiera::No_t; + /** + * Metafunction: does the Type in question + * give us a clue about what service interface to use? + */ + template + class defines_ServiceInterface + { + META_DETECT_NESTED (ServiceInterface); + + public: + enum{ value = HasNested_ServiceInterface::value + }; + }; + + /** + * Trait template: getting the types of the + * Service interface and SingletonFactory + */ + template + struct SingletonAccess + { + typedef No_t ServiceInterface; + typedef No_t Factory; + }; + + template + struct SingletonAccess >::type> + { + typedef typename IMP::ServiceInterface ServiceInterface; + typedef lib::Singleton Factory; + }; + + /** * A Metafunction to find out, if a type in question * is used as Singleton and especially has been configured @@ -108,16 +174,14 @@ namespace test{ template class is_Singleton_with_MockInjector { - META_DETECT_NESTED (ServiceInterface); - META_DETECT_FUNCTION(void, injectSubclass, (TY*) const); - - typedef typename MOCK::ServiceInterface ServiceInterface; + typedef typename SingletonAccess::ServiceInterface Interface; + typedef typename SingletonAccess::Factory SingletonFactory; + META_DETECT_MEMBER (injectSubclass); public: - typedef lib::SingletonFactory SingletonFactory; - enum{ value = HasNested_ServiceInterface::value - && HasFunSig_injectSubclass::value + enum{ value = defines_ServiceInterface::value + && HasMember_injectSubclass::value }; }; @@ -127,19 +191,24 @@ namespace test{ * a mock service implementation? */ template - struct AccessPoint; - + struct AccessPoint + { + BOOST_STATIC_ASSERT (is_Singleton_with_MockInjector::value ); + }; // Hint: (1) the mock needs a typedef pointing to the ServiceInterface + // (2) prepare the Singleton-Factory for injecting a Mock implementation... + // look into singleton-preconfigure.hpp for examples! + template struct AccessPoint >::type> { - typedef typename is_Singleton_with_MockInjector::SingletonFactory ServiceAccessPoint; + typedef typename SingletonAccess::Factory ServiceAccessPoint; - void + static void activateMock() { ServiceAccessPoint().injectSubclass (new MOCK()); } - void + static void deactivateMock() { ServiceAccessPoint().injectSubclass (0); @@ -155,19 +224,21 @@ namespace test{ * Placing a suitably specialised instance of this template into a local scope * will inject the corresponding mock installation and remove it when the * control flow leaves this scope. + * @param MOCK the concrete mock implementation to inject. It needs to + * subclass the service interface and expose a typedef \c ServiceInterace */ - template + template struct Use4Test : boost::noncopyable { Use4Test() { - mock::AccessPoint::activateMock(); + mock::AccessPoint::activateMock(); } ~Use4Test() { - mock::AccessPoint::deactivateMock(); + mock::AccessPoint::deactivateMock(); } }; diff --git a/tests/components/backend/mediaaccessmock.hpp b/tests/components/backend/mediaaccessmock.hpp index dce886aa9..ad22aaabb 100644 --- a/tests/components/backend/mediaaccessmock.hpp +++ b/tests/components/backend/mediaaccessmock.hpp @@ -41,6 +41,9 @@ namespace test { { MediaDesc& queryFile (string const& name) const; ChanDesc queryChannel (MediaDesc&, uint chanNo) const; + + public: + typedef MediaAccessFacade ServiceInterface; }; diff --git a/tests/components/backend/mediaaccessmocktest.cpp b/tests/components/backend/mediaaccessmocktest.cpp index 8bd3ae026..b3dfbcd84 100644 --- a/tests/components/backend/mediaaccessmocktest.cpp +++ b/tests/components/backend/mediaaccessmocktest.cpp @@ -30,6 +30,7 @@ #include +using lib::test::Use4Test; using lib::Literal; using std::string; using std::cout; @@ -52,12 +53,10 @@ namespace test { virtual void run(Arg) { - MAF::instance.injectSubclass (new MediaAccessMock); + Use4Test within_this_scope; queryScenario ("test-1"); queryScenario ("test-2"); - - MAF::instance.injectSubclass (0); }