Simulated Objects
From the system under test (SUT) they are needed as regular communication partners fulfilling some services. For the test environment they give information about the communication control flows in which the system under test is involved and they offer the possibility to send some trigger to the SUT.
#include "ISomeService.h" // regular interface to be implemented #include "TestToolBox/SimulatedObjectBase.h" class MySimulatedObject : public ISomeService , public TestToolBox::SimulatedObjectBase { public: // Constructor MySimulatedObject (std::string const & in_objectName); // Interface ISomeService void CalculateOne (int in_val, int& out_result); void CalculateTwo (int in_val, int& out_result); };To connect to the base functionality for simulated objects as given by the test framework you have simply to derive your class from SimulatedObjectBase. Within constructor you have to pass the instance name and class type of your object to the base class:
MySimulatedObject::MySimulatedObject( std::string const & in_objectName) : SimulatedObjectBase (in_objectName, TTB_CLASS_NAME) { ... }
The macro TTB_CLASS_NAME will automatically deduce the name of your specific class. The names of enclosing namespaces are removed. In the Example above the result will be "MySimulatedObject".
MySimulatedObject::MySimulatedObject(void) : SimulatedObjectBase ("noSpecificName", TTB_CLASS_NAME) { // Here or in any other method you can change the object name SimulatedObjectBase::SetObjectName (SomeSpecificFunctionToBuildName()); }Simply pass a dummy name to the base class within initializer list. If needed build the final instance name by calling base method SetObjectName within constructor body or in any other method.
Probably you will have to choose specific instance names only for the case when there are multiple instances of the same class type and you want to address a specific instance.
void MySimulatedObject::CalculateOne (int in_val, int& out_result) { out_result = 17; // if result is needed within your test TTB_METHOD_CALL(); }Within macro TTB_METHOD_CALL() the test framework automatically deduces the name of your function. When during testing your simulated object gets called the following output will be recorded as test event:
CalculateOne
For automatic verification of the occured control flow you now can write within your test case:
SendSomeTriggerToTheSystemUnderTest(); // Now expecting: TTB_EXP("CalculateOne");
void MySimulatedObject::CalculateTwo (int in_val, int& out_result) { out_result = in_val * 2; TTB_METHOD_CALL1("in_value=" << in_value << " out_result=" << out_result); }Macro TTB_METHOD_CALL1() allows to specify any output in stream syntax. When during testing your simulated object gets called the following output will be recorded as test event:
CalculateTwo in_value=13 out_result=26
For automatic verification of the occured control flow you now can simply write within your test case:
SendSomeTriggersToTheSystemUnderTest(); // In this test case expecting several calls: TTB_EXP("CalculateTwo in_value=13 out_result=26"); TTB_EXP("CalculateTwo in_value=10 out_result=20"); TTB_EXP("CalculateTwo in_value=-7 out_result=-14");
TTB::TheGlobalOptions()->Set(Option::oOBJECT_NAME);An appropriate location where to add this instruction could be the initialization routine where you setup your environment.
When during testing your simulated object gets called the following output will be recorded as test event:
CalculateOne (MyObjectName) CalculateTwo in_value=5 out_result=10 (MyObjectName)It is possible to switch on tracking of object names only for a specific class type or even only for a specific method:
// activate object name for class type TTB::TheGlobalOptions()->Set(Option::oOBJECT_NAME,"MySimulatedObject"); // activate object name for specific method TTB::TheGlobalOptions()->Set(Option::oOBJECT_NAME,"MySimulatedObject::CalculateTwo");
Transferred to our simulated objects this means: in one test case we want to track a specific subset of method calls, in another test case we want to track a different subset of method calls. To solve this requirement the test framework offers the possibility to set the not interesting methods to "silent" mode:
TTB::TheGlobalOptions()->Set(Option::oSILENT, MySimulatedObject::CalculateTwo);An appropriate location where to add this instruction could be the initialization code where you prepare the environment for your test case.
When during testing both methods (e.g. CalculateOne(), CalculateTwo()) of your simulated object get called then the following output will be recorded as test event:
CalculateOne (MyObjectName)The call to CalculateTwo is not visible here but it will be performed regularly and from the point of view of the system under test all works fine. With setting silent mode for a method only the tracking as test event and the optional generation of a sync event is switched off.
// activate silent mode for all methods of given class type TTB::TheGlobalOptions()->Set(Option::oSILENT,"MySimulatedObject"); // deactivate silent mode for a selected method, method will be tracked within test TTB::TheGlobalOptions()->Set(Option::oSILENT, "MySimulatedObject::CalculateTwo", OptionType::NOT_ACTIVE);
Your test sequence sends some trigger to the system under test (e.g. calls some interface function) and then has to wait until the expected delayed call to your fake object occurs.
For an easy solution to this problem the test framework offers the possibility of automatically rising a synchronization event when a method of a simulated object gets called.
To activate the automatic generation of sync events you could address a single method or a whole class (i.e. all methods of the class):
// activate sync for a specific class method TTB::TheGlobalOptions()->Set(Option::oSYNC,"MySimulatedObject::CalculateTwo"); // activate sync for all methods of a class TTB::TheGlobalOptions()->Set(Option::oSYNC,"MySimulatedObject");When the synchronization is activated, a test sequence waiting for a delayed call looks like this:
TTB_INIT_SYNC(1); DoSomeTriggerCallToTheSystemUnderTest() TTB_WAIT_SYNC(); // wait here until the sync event occurs // now the call has happened and the expected // call data can be verified TTB_EXP("CalculateTwo in_value=13 out_result=26 (MyObjectName)")For a detailed description of test control and automatic verification for asynchronous control flows see Multithreading, Synchronisation paralleler Abläufe and Zentralisierte Erfassung der "Testereignisse" (TestEvents).
From the aspect of testing it is important to stress the SUT also with error reactions. Therefore when implementing a simulated object it always should support a failing of its methods.
When deriving your specific object class from SimulatedObjectBase you will get optional error return values (nearly) for free. In most cases you only have to
The following two examples illustrate how simple it is to simulate error behaviour:
int MySimulatedObject::DoSomething () { return TTB_METHOD_CALL_RET1(int); } int MySimulatedObject::DoSomethingElse (std::string in_info) { // adding some info about input params return TTB_METHOD_CALL_RET2(int, "in_info=" << in_info); }To support your return type "int" you have to implement an appropriate overload of function MakeReturnValue somewhere in your link unit:
namespace TestToolBox { std::string MakeReturnValue ( int& out_retVal, bool in_simulateFailure, std::string const & in_methodName, // not used here SimulatedObjectBase const * in_pObject) // not used here { if (in_simulateFailure) out_retVal = -1; return ""; // optionally give info for test output }; }When during testing the simulated methods get called they will return value 0 and write the following output to test events:
DoSomething (MyObjectName) DoSomething in_info=someInfo (MyObjectName)You can activate an error return value specific for each method by writing within test script:
TTB::TheGlobalOptions()->Set(Option::oERROR,"MySimulatedObject::DoSomething"); TTB::TheGlobalOptions()->Set(Option::oERROR,"MySimulatedObject::DoSomethingElse");When the methods are called again they will return value -1 and write the following output to test events:
DoSomething return error (MyObjectName) DoSomethingElse in_info=someInfo return error (MyObjectName)
struct SomeErrorResult { SomeErrorResult () : m_errorCode (0) {} SomeErrorResult ( int in_errorCode, std::string const & in_errorInfo) : m_errorCode (in_errorCode) , m_errorInfo (in_errorInfo) {} int m_errorCode; std::string m_errorInfo; };and a method returning this type, then you can implement:
SomeErrorResult MySimulatedObject::DoSomethingImportant () { return TTB_METHOD_CALL_RET1(SomeErrorResult); }To support your return type "SomeErrorResult" you have to implement an appropriate overload of function MakeReturnValue somewhere in your link unit:
namespace TestToolBox { std::string MakeReturnValue( SomeErrorResult& out_retVal, bool in_simulateFailure, std::string const & in_methodName, SimulatedObjectBase const * in_pObject) { if (in_simulateFailure) { out_retVal.m_errorCode = -1; out_retVal.m_errorInfo = "Simulated error in " // add some context info about method and object + in_pObject->GetClassName() + "::" + in_methodName; } else // if not yet done within default construction { out_retVal.m_errorCode = 0; out_retVal.m_errorInfo = ""; } // to override the default error info "return error" std::string info; if (in_simulateFailure) { std::ostringstream oss; oss << "return error code " << out_retVal.m_errorCode; info = oss.str(); } return info; }; }When during testing the simulated method gets called it will return SomeErrorResult() and write the following output to test events:
DoSomethingImportant (MyObjectName)You can activate an error return value specific for your method by writing within test script:
TTB::TheGlobalOptions()->Set(Option::oERROR,"MySimulatedObject::DoSomethingImportant");When the method is called again it will return an error struct containing m_errorCode = -1, m_errorInfo = "Simulated error in MySimulatedObject::DoSomethingImportant " and write the following output to test events:
DoSomethingImportant return error code -1 (MyObjectName)
Header file:
#include "ISomeService.h" // regular interface to be implemented #include "TestToolBox/SimulatedObjectBase.h" class MySimulatedObject : public ISomeService , public TestToolBox::RegisteredSimulatedObject { public: // Constructor MySimulatedObject (std::string const & in_objectName); ... };Within constructor you have to pass the instance name and class type of your object to the base class. This will automatically register your object:
MySimulatedObject::MySimulatedObject( std::string const & in_objectName) : RegisteredSimulatedObject (in_objectName, TTB_CLASS_NAME) { ... }
When within testing you need access to your simulated object, you can simply write:
TTB::TheRegisteredSimulatedObjects()->Get("ObjectA")->DoSomethingInBase(); // DoSomethingInBase is one of the methods available within base classes // SimulatedObjectBase or RegisteredSimulatedObject.After you have changed your object's name, you have also to adjust the access code:
// Access object named "A" to change its name TTB::TheRegisteredSimulatedObjects()->Get("A")->SetObjectName("DummyB"); // From now on you have to use the new name TTB::TheRegisteredSimulatedObjects()->Get("DummyB")->DoSomething();Last but not least: The destructor of base class RegisteredSimulatedObject automatically unregisters the object.
SimObject("DummyB")->DoSomethingInBase();
But with a simple downcast function
MySimulatedObject* FindMySimulatedObject(std::string const & in_objectName) { // Downcast to your specific class return static_cast<MySimulatedObject*>(SimObject(in_objectName)); }you again can simply write
FindMySimulatedObject("DummyB")->DoSomethingInDerived();
Depending on the specific needs for your test environment and also depending on your personal aesthetic sensibilities you may want to have influence on the basic informations written to test events.
namespace TTB = TestToolBox; namespace FI = TestToolBox::Formating; // Define format for output of method calls // The sequence of calls represents the sequence within output string. // Not added info blocks will not be used for output. FI::FormatInstruction instruction; instruction.AddInfoBlock(FI::InfoBlock(Formating::CLASS_NAME,"","::")); instruction.AddInfoBlock(FI::InfoBlock(Formating::METHOD_NAME,"")); instruction.AddInfoBlock(FI::InfoBlock(Formating::OBJECT_NAME," (", ")")); instruction.AddInfoBlock(FI::InfoBlock(Formating::USER_TEXT)); TTB::TheFormatter()->SetFormatInstructionForMethodCall(instruction);
When during testing your simulated object gets called the output will now have the following format:
MySimulatedObject::CalculateOne (MyObjectName) MySimulatedObject::CalculateTwo (MyObjectName) in_value=5 out_result=26Without specific configuration the same method calls would have the following format:
CalculateOne (MyObjectName) CalculateTwo in_value=5 out_result=26 (MyObjectName)
Example: Within one test case a called function of a simulated object may succeed, within the next test case the same function shall fail to test error handling of the system under test.
By calling method _IsOptionSet() the default implementation of _MethodCall() checks a couple of predefined options. General rule: if the option is not found within the set of local options then the search continues within global set of options. If the (boolean) option cannot be found within both sets it is assumed that the option is not set, i.e. its value is "false".
Within the method implementations in your derived class you may check any option via the same mechanism.
Example: "multiplication shall fail" or "SimulateFullContainer" are good names for options in the context of testing.
There are the following predefined options which are used within the implementation of base class SimulatedObjectBase and which you should use also within your code where appropriate:
void MySimulatedObject::CalculateTwo (int in_val, int& out_result) { out_result = in_val * 2; // Check whether error condition is set for the scope of this method if (_IsError("CalculateTwo")) { out_result = -1; } TTB_METHOD_CALL1("in_value=" << in_value << " out_result=" << out_result); // Remarks: // _IsError("CalculateTwo") is short form of // _IsOptionSet(Option::oERROR, "CalculateTwo") // // Alternatively check for an error flag concerning all methods: // _IsError() is short form of // _IsOptionSet(Option::oERROR, Option::ALL) }
When thinking about method calls to a simulated object the scope where to check for some option usually has to do with the name of the called method and the class name. Within base implementation SimulatedObjectBase::_MethodCall the options are checked by using the name of the called function as the scope.
To decide whether an option is set, SimulatedObjectBase::_IsOptionSet (someOption, "MyMethodName") searches for an option entry according to the following sequence:
Knowing this search algorithm you can choose an appropriate scope to address the reaction you need.
// Switch off method tracking for all objects and methods TTB::TheGlobalOptions()->Set(Option::oSILENT); // Selectively switch on all methods of a single class TTB::TheGlobalOptions()->Set(Option::oSILENT,"MyDerivedClass1", OptionType::NOT_ACTIVE); // Selectively switch on a single method of a single class TTB::TheGlobalOptions()->Set(Option::oSILENT,"MyDerivedClass2::SomeMethod", OptionType::NOT_ACTIVE);
// Selectively switch on two methods of a single class TTB::TheGlobalOptions()->Set(Option::oSYNC,"MyDerivedClass::SomeMethod1"); TTB::TheGlobalOptions()->Set(Option::oSYNC,"MyDerivedClass::SomeMethod2");
// assume you have an array of simulated objects std::vectormySimObjects = CreateMyObjects(); // The 3rd element shall return an error when getting called mySimObjects[2]->_SetOption(Option::oERROR,"SomeMethod");
class Option { ... void Set ( std::string const & in_option, std::string const & in_scope = Option::ALL, OptionType::Enum in_optionType = OptionType::PERMANENTLY_ACTIVE, int in_numRemainingCalls = 0) const; ... };The OptionType has one of the following meanings:
MyTestExecutable.exe -SomeCounter 17
int counter = TTB::TheEnvironment()->GetCommandLineOptionVal<int>("-SomeCounter"); // change existing option value ++counter; // store it again to make it available to all interested locations TTB::TheEnvironment()->SetCommandLineOptionVal("-SomeCounter", counter);
// define new option of type double TTB::TheEnvironment()->SetCommandLineOptionVal("-SomeDoubleValue", 3.14);
TTB_INIT_SYNC(5); SendSometriggerToTheSystemUnderTest(); TTB_WAIT_SYNC() TTB_EXP("CalculateSomething in_val=3 outVal=6 (MySimObject1)"); TTB_EXP("CalculateSomething in_val=4 outVal=8 (MySimObject1)"); TTB_EXP("DoSomethingElse (MySimObject1)"); TTB_EXP("CalculateSomething in_val=5 outVal=10 (MySimObject1)"); TTB_EXP("CalculateSomething in_val=6 outVal=12 (MySimObject1)"); TTB_EXP("DoSomethingElse (MySimObject2)");To be able to hold the execution on the 3rd call of "CalculateSomething" the standard implementation of the method is sufficient:
void MySimulatedObject::CalculateSomething (int in_val, int& out_val) { out_val = in_val * 2; TTB_METHOD_CALL1("in_value=" << in_val << " out_val=" << out_val); }To activate blocking you have to write within test script:
mySimObject1->_SetOption(Option::oBLOCKING, "CalculateSomething", AFTER_N_CALLS_ACTIVE_ONCE, 3); TTB_INIT_SYNC(4); SendSometriggerToTheSystemUnderTest(); TTB_WAIT_SYNC() TTB_EXP("CalculateSomething in_val=3 outVal=6 (MySimObject1)"); TTB_EXP("CalculateSomething in_val=4 outVal=8 (MySimObject1)"); TTB_EXP("DoSomethingElse (MySimObject1)"); TTB_EXP("CalculateSomething-Start in_val=5 outVal=10 (MySimObject1)"); // Now do something while the execution is halted ... // Continue execution TTB_INIT_SYNC(3); mySimObject1->_ContinueExecution(); TTB_WAIT_SYNC() TTB_EXP("CalculateSomething-Stop TTB_EXP("CalculateSomething in_val=6 outVal=12 (MySimObject1)"); TTB_EXP("DoSomethingElse (MySimObject2)");All checking, halting and continuing of execution is done within base class implementation.
But when this is not the case you could delegate the trigger call within your test environment to a separate thread. Then the main thread of your test script would be free for calling continue().
void MySimulatedObject::CalculateSomething (int in_val, int& out_val) { out_val = in_val * 2; TTB_METHOD_CALL2 ( // write input params when function is called "in_value=" << in_val, // embedded lambda function, // gets called just before function returns std::ostringstream oss; if (_IsError("CalculateSomething ")) { out_val = -1; } oss << "out_value=" << out_value; return oss.str(); ); }Instead of using macro TTB_METHOD_CALL2 you could also use the underlying function _MethodCall:
void MySimulatedObject::CalculateSomething (int in_val, int& out_val) { out_val = in_val * 2; _MethodCall(__FUNCTION__, // write input params when function is called TTB_STRING_S("in_value=" << in_val), // embedded lambda function, // gets called just before function returns ([&](void) -> std::string { std::ostringstream oss; if (_IsError("CalculateSomething ")) { out_val = -1; } oss << "out_value=" << out_value; return oss.str(); })); }The base implementation considers params 2 and 3 of function _MethodCall() for generating output. Param 2 is a simple string which is written to test events when the function is entered. Param 3 is a lambda function which also returns a string for output. When the function call is not blocked both outputs are presented in a single line. In the case of blocking the lambda function gets evaluated just before the function is left and the returned output is written to test events as a separate line.
Now you could write the following test script:
mySimObject1->_SetOption(Option::oBLOCKING, "CalculateSomething", AFTER_N_CALLS_ACTIVE_ONCE, 3); TTB_INIT_SYNC(4); SendSometriggerToTheSystemUnderTest(); TTB_WAIT_SYNC() TTB_EXP("CalculateSomething in_val=3 outVal=6 (MySimObject1)"); TTB_EXP("CalculateSomething in_val=4 outVal=8 (MySimObject1)"); TTB_EXP("DoSomethingElse (MySimObject1)"); TTB_EXP("CalculateSomething-Start in_val=5 (MySimObject1)"); // While the execution is halted decide that the blocked call shall return // an error value and the next call shall automatically succeed again. mySimObject->_SetOption(Option::oERROR, "CalculateSomething", ACTIVE_ONCE); // Continue execution TTB_INIT_SYNC(3); mySimObject1->_ContinueExecution(); TTB_WAIT_SYNC() TTB_EXP("CalculateSomething-Stop outVal=-1 TTB_EXP("CalculateSomething in_val=6 outVal=12 (MySimObject1)"); TTB_EXP("DoSomethingElse (MySimObject2)");
Of course there is a number of specific solutions, you can implement:
If there are multiple instances you can try to implement a counter and add the counter value to your hard coded object name.
Furthermore you could change the name of an registered object after its creation by calling its base method SimulatedObjectBase::SetObjectName()
. This will also update the object's registration. It will always
be reachable under its current name.