General Concepts:
Components:
Features:
Configuration:
HowTo's:
—-
API's:
Client Applications:
Testing:
Functionality:
<markdown> # Test Harness The TestHarness class in NebulaStream is a framework to run quey deployment tests. This page shows how to create a test using the TestHarness class. ## Creating a Query Deployment Test Using The TestHarness In this example, we will go through the process of creating a test to deploy a simple query with a filter operator. #### Prepare a test case. The test case is similar to other tests. ``` TEST_F(Example, exampleTest) { ... } ``` #### Instantiate a TestHarness Class TestHarness takes a query (without the sink operator), a restPort, and a rpcPort as its constructor parameter. ``` TEST_F(Example, exampleTest) { // instantiate a TestHarness object std::string queryWithFilterOperator = R"(Query::from("car").filter(Attribute("key") < 30))"; TestHarness testHarness = TestHarness(queryWithFilterOperator, restPort, rpcPort); ... } ``` #### Define and Add Sources TestHarness supports two kinds of sources: MemorySource and CSVSource. To add a memory source, we need to prepare the struct defining a Tuple of the source and the schema of the source. After that, we can call `addMemorySource` and pass three parameters: the name of the logical source, the schema, and the name of the physical source. ``` TEST_F(Example, exampleTest) { ... // define and add memory sources struct Car { uint32_t key; uint32_t value;uint64_t timestamp;}; auto carSchema = Schema::create()->addField("key", DataTypeFactory::createUInt32()) ->addField("value", DataTypeFactory::createUInt32())->addField("timestamp", DataTypeFactory::createUInt64()); testHarness.addMemorySource("car", carSchema, "car_physical_stream_1"); testHarness.addMemorySource("car", carSchema, "car_physical_stream_2"); ... } ``` To add a memory source, we need to first define a `PhysicalSourceConfigPtr`. After that, we call `addCSVSource` and pass this object along with the schema used in the source to be added. ``` TEST_F(Example, exampleTest) { ... // define and add csv sources PhysicalSourceConfigPtr conf = PhysicalSourceConfig::create(sourceConfig); testHarness.addCSVSource(conf, carSchema); ... } ``` Note: - `sizeOf(TupleStruct) == schema.getSizeInBytes()` (The size of the Struct must equal to the size of the schema in bytes) - `bufferSize* % sizeOf(TupleStruct) == 0` (The default value of bufferSize is `4096`. The `bufferSize` should be divisible by the size of the struct. Currently, we do not support a tuple that spans multiple buffers.) #### Pushing Element to The Memory Source (not applicable to CSVSource) To push a tuple to a memory source, we can call the `pushElement()` from the TestHarness object. This method takes an instance of the struct of the tuple and the index of the source as parameters. ``` TEST_F(Example, exampleTest) { ... // push elements to sources testHarness.pushElement<Car>({40, 40, 40}, 0); testHarness.pushElement<Car>({30, 30, 30}, 1); testHarness.pushElement<Car>({71, 71, 71}, 1); testHarness.pushElement<Car>({21, 21, 21}, 0); ... } ``` Note: - All pushed elements/tuples will be stored in a single buffer when it is not full. (`sizeOf(Tuple) * nTuples < 4096`) #### Defining the Expected Output We need to define a struct of the expected output. The struct needs to overload the `==` operator to compare between two instances. We then create a vector of this struct to define what output we expect from the test. ``` TEST_F(Example, exampleTest) { ... // define the expected output struct Output {uint32_t key; uint32_t value;uint64_t timestamp; bool operator==(Output const& rhs) const { return (key == rhs.key && value == rhs.value && timestamp == rhs.timestamp); } }; std::vector<Output> expectedOutput = {{40, 40, 40}, {71, 71, 71}}; ... } ``` Notes: - The output data types have to match the input data type. - Output struct should not lead to paddings. #### Comparing the Actual Output and the Expected Output In this final step, we first obtain the actual output from the TestHarness by calling `getOutput()`. This method will execute the query deployment and return the output of the query. We can then use `EXPECT_EQ` and `EXPECT_THAT` to compare the actual output to the expected output. ``` TEST_F(Example, exampleTest) { ... // compare the actual output to the expected output std::vector<Output> actualOutput = testHarness.getOutput<Output>(expectedOutput.size()); EXPECT_EQ(actualOutput.size(), expectedOutput.size()); EXPECT_THAT(actualOutput, ::testing::UnorderedElementsAreArray(expectedOutput)); } ``` ### Creating Hierarchical Topology In the previous example, we only create a flat topology with `n` workers. The TestHarness framework allows us to create a hierarchical framework by specifying the parent worker. Furthermore, we can also add non-source worker (e.g., the workers between sensors and the sink) using the `addNonSourceWorker` method. ``` TEST_F(Example, exampleTest) { … add hierarchical topology testHarness.addNonSourceWorker(); worker with idx=0 (with the sink node as default parent) testHarness.addNonSourceWorker(testHarness.getWorkerId(0)); //worker with idx=1 testHarness.addMemorySource("car", carSchema, "car1", testHarness.getWorkerId(1)); //worker with idx = 2 … } ``` ### More Example More examples of test cases using the TestHarness framework can be found in `/test/Integration/TestHarnessUtilTest`. </markdow n> |