Skip to content
This repository was archived by the owner on Dec 29, 2022. It is now read-only.

Commit d6c923f

Browse files
authored
Added Testing Style and Tips document. (#9)
1 parent 09fc883 commit d6c923f

File tree

5 files changed

+283
-7
lines changed

5 files changed

+283
-7
lines changed

doxygen/style_suggestions.dox

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -36,13 +36,13 @@ There are two ways of accomplishing this; composition or inheritance.
3636
Composition is often preferable over inheritance as it gives more flexibility but both should be considered and are possible.
3737

3838
Composition example (from [Robot Localization](@ref robotlocalization.cpp)):
39-
@snippet robotlocalization.cpp TopicStates Composition Example
39+
@snippetlineno robotlocalization.cpp TopicStates Composition Example
4040

4141
Inheritance examples
4242
From [Fancy Vending Machine](@ref fancyvendingmachine.cpp):
43-
@snippet fancyvendingmachine.cpp TopicStates Inheritance Example
43+
@snippetlineno fancyvendingmachine.cpp TopicStates Inheritance Example
4444
From [Beat Machine](@ref beatmachine.cpp):
45-
@snippet beatmachine.cpp TopicStates Inheritance Example
45+
@snippetlineno beatmachine.cpp TopicStates Inheritance Example
4646

4747
Inheritance can save you some typing and be OK when literally all you're changing is the type 'name' and when you know the data structure will never change and when the 'is a' relationship holds strongly. Inheritance is the lazy approach but that you'll likely have to ditch (at great expense possibly) at some point in the future.
4848

@@ -51,19 +51,19 @@ Inheritance can save you some typing and be OK when literally all you're changin
5151
Sometimes all a topic needs to express is the fact that _it updated_, that it _happened_ or any other purely unary signals:
5252

5353
From [Trivial Vending Machine](@ref trivialvendingmachine.cpp):
54-
@snippet trivialvendingmachine.cpp Trivial TopicState
54+
@snippetlineno trivialvendingmachine.cpp Trivial TopicState
5555
From [Fancy Vending Machine](@ref fancyvendingmachine.cpp):
56-
@snippet fancyvendingmachine.cpp Trivial TopicState
56+
@snippetlineno fancyvendingmachine.cpp Trivial TopicState
5757

5858
\subsection ssg-atomic-change Variables/bits/data that change together atomically belong together.
5959

6060
Examples:
6161

6262
A product and its new price:
63-
@snippet fancyvendingmachine.cpp Mutually Atomic Variables
63+
@snippetlineno fancyvendingmachine.cpp Mutually Atomic Variables
6464

6565
A robot's pose and a timestamp:
66-
@snippet robotlocalization.cpp Mutually Atomic Variables
66+
@snippetlineno robotlocalization.cpp Mutually Atomic Variables
6767

6868
\subsection ssg-mutually-exclusive Mutually exclusive states also belong together.
6969

doxygen/testing_style.dox

Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,101 @@
1+
/**
2+
@page testing-style Testing - Unit & Integration Tests
3+
4+
\tableofcontents
5+
6+
\section ts-unit Unit Tests
7+
8+
One of the design goals of the Framework was to facilitate full testing coverage of any detectors. DetectorGraph is designed so that each Detector is tested as a Unit - by passing specific input `TopicStates` and comparing output `TopicStates` with expected values.
9+
@warning Before continuing it may be good to familiarize yourself with the first few [examples](@ref custom_examples) as this document builds on top of those examples.
10+
11+
\subsection ts-unit-siso Basics
12+
13+
The most basic case is for a SISO Detector like the one in the [HelloWorld Example](@ref helloworld.cpp). Detector unit tests should be written following the Arrange-Act-Assert (AAA) unit testing pattern.
14+
15+
_Arrange_ for the test consists of a graph instance with only the detector under test in it. In addition to that it's also cursory to grab a pointer `outTopic` to the output Topics we will check in the Assert section of the test. This help setting expectations for the reader and cleans up the later sections of the test.
16+
@snippetlineno helloworld.cpp UnitTest-Above-1
17+
Note that different Unit Testing frameworks will use different function signatures for tests so adapt it as necessary.
18+
19+
_Act_ consists in _Pushing_ the input data topic into the input queue and _Evaluating_ the graph.
20+
@snippetlineno helloworld.cpp UnitTest-Above-2
21+
22+
_Assert_ consists in [inspecting](@ref topic_inspection_apis) the output topic(s):
23+
@snippetlineno helloworld.cpp UnitTest-Above-3
24+
Different Unit Testing frameworks will use different & more expressive assert methods but the idea is the same.
25+
26+
\subsection ts-unit-siso-aaa-aa Act-Assert sequences
27+
28+
In some cases it's nice to submit a detector to a small sequence of Act/Asserts. Care should be taken to maintain the test focused on a single behavior and not let it balloon out of proportion (testing more than one single behavior) - so this only applies when the behavior being tested is sequential in nature.
29+
30+
One such case is to test the counting behavior of the [CounterWithReset Example](@ref counterwithreset.cpp).
31+
The test starts as before:
32+
@snippetlineno counterwithreset.cpp UnitTest-Count-1
33+
34+
And follows with an extra _Act_ and _Assert_ pair:
35+
@snippetlineno counterwithreset.cpp UnitTest-Count-2
36+
Note that the middle _Assert_ is not necessary per se - it is helpful when it gives the reader confidence that _things are going as expected_ but they will be *very* unhelpful if they distract the reader from what's coming next; one or a couple of assert lines are fine, 10 are not. Use your best judgment here.
37+
38+
\subsection ts-unit-miso Multiple Input Single Output
39+
40+
Most times when testing a detector with multiple inputs and multiple outputs each input has to be _Pushed_ and _Evaluated_ on its own.
41+
Again, for the [CounterWithReset Example](@ref counterwithreset.cpp):
42+
@snippetlineno counterwithreset.cpp UnitTest-ResetCount-1
43+
Here the _Arrange_ step includes pushing some data and evaluating the graph.
44+
45+
_Act_ and _Assert_ are then done just as before:
46+
@snippetlineno counterwithreset.cpp UnitTest-ResetCount-2
47+
48+
\subsection ts-unit-futpub Future Publishes
49+
50+
In order for detectors to close feedback loops/cycles in the graph they must be [FuturePublisher](@ref DetectorGraph::FuturePublisher)s.
51+
The simplest example of this is shown in the `ResetDetector` within the [CounterWithReset Example](@ref counterwithreset.cpp).
52+
The interesting detail here is that Topics published with [PublishOnFutureEvaluation](@ref DetectorGraph::FuturePublisher::PublishOnFutureEvaluation) are, as the name suggests, published only in the next evaluation and that needs to be taken into account in the unit test.
53+
54+
@warning See the section below for a note on [Lag](@ref DetectorGraph::Lag) for reasons why that's preferred than what's shown here.
55+
56+
Initial _Arrange_ and _Act_ are as normal:
57+
@snippetlineno counterwithreset.cpp UnitTest-ResetDetected-1
58+
59+
But the test must check:
60+
- that the output is not immediately published
61+
- that the intended `TopicState` is published on the next evaluation.
62+
63+
And that can be done with:
64+
@snippetlineno counterwithreset.cpp UnitTest-ResetDetected-2
65+
66+
The same idea applies when testing the negative complementary of such behaviors:
67+
@snippetlineno counterwithreset.cpp UnitTest-NoResetDetected-1
68+
69+
\subsection ts-unit-lag Lag
70+
71+
[Lags](@ref DetectorGraph::Lag) are the more general, preferred & readable way to close loops in a graph - and one place they shine the shiniest is in unit tests; since Lags are Detectors themselves and the lagged Topic is just another Topic. That means unit tests do not need to keep track of future publishes as that's all handled outside that one detector. Unit tests then are trivial with respect to the loop:
72+
73+
@snippetlineno fancyvendingmachine.cpp UnitTest-LowerUserBalanceOnSale
74+
75+
@note There are two reasons why Lags didn't simply replace `FuturePublish`; legacy code uses FuturePublish extensively and Lags may a bit wasteful in deeply embedded (limited code space) targets.
76+
77+
78+
\section ts-system Integration Tests
79+
80+
In order to test the integration between detectors one can instantiate a subset of detectors (up to the entire set) of an application and test it in the same way as unit tests - all you have to do is think of the whole thing as one single big detector. This test should then be stable regardless of how many detectors compose the solution in between the inputs and outputs.
81+
82+
In integration tests it's important to _flush_ the graph input queue continuously when appropriate for the reasons described in @ref ts-unit-futpub . This can be seen in the example for [CounterWithReset](@ref counterwithreset.cpp) below on line 311:
83+
84+
@snippetlineno counterwithreset.cpp UnitTest-CounterResetIntegration
85+
86+
*/
87+
88+
/**
89+
@page seo-testing Testing
90+
See [Testing Style](@ref testing-style).
91+
*/
92+
93+
/**
94+
@page seo-unit-testing Unit Testing
95+
@copydoc seo-testing
96+
*/
97+
98+
/**
99+
@page seo-integ-testing Integration Testing
100+
@copydoc seo-testing
101+
*/

examples/counterwithreset.cpp

Lines changed: 111 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -163,6 +163,49 @@ class EventCountDetector : public DetectorGraph::Detector
163163
};
164164
//![EventCountDetector]
165165

166+
//![UnitTest-Count-1]
167+
void Test_Count()
168+
{
169+
DetectorGraph::Graph graph;
170+
EventCountDetector detector(&graph);
171+
auto outTopic = graph.ResolveTopic<EventCount>();
172+
173+
graph.PushData(EventHappened());
174+
graph.EvaluateGraph();
175+
176+
DG_ASSERT(outTopic->HasNewValue());
177+
DG_ASSERT(outTopic->GetNewValue().count == 1);
178+
179+
//![UnitTest-Count-1]
180+
//![UnitTest-Count-2]
181+
graph.PushData(EventHappened());
182+
graph.EvaluateGraph();
183+
184+
DG_ASSERT(outTopic->HasNewValue());
185+
DG_ASSERT(outTopic->GetNewValue().count == 2);
186+
}
187+
//![UnitTest-Count-2]
188+
189+
//![UnitTest-ResetCount-1]
190+
void Test_ResetCount()
191+
{
192+
// Arrange
193+
DetectorGraph::Graph graph;
194+
EventCountDetector detector(&graph);
195+
auto outTopic = graph.ResolveTopic<EventCount>();
196+
graph.PushData(EventHappened());
197+
graph.EvaluateGraph();
198+
199+
//![UnitTest-ResetCount-1]
200+
//![UnitTest-ResetCount-2]
201+
graph.PushData(Reset());
202+
graph.EvaluateGraph();
203+
204+
DG_ASSERT(outTopic->HasNewValue());
205+
DG_ASSERT(outTopic->GetNewValue().count == 0);
206+
}
207+
//![UnitTest-ResetCount-2]
208+
166209
//![Reset Detector]
167210
class ResetDetector : public DetectorGraph::Detector
168211
, public DetectorGraph::SubscriberInterface<EventCount>
@@ -187,6 +230,43 @@ class ResetDetector : public DetectorGraph::Detector
187230
};
188231
//![Reset Detector]
189232

233+
//![UnitTest-ResetDetected-1]
234+
void Test_ResetDetected()
235+
{
236+
// Arrange
237+
DetectorGraph::Graph graph;
238+
ResetDetector detector(&graph);
239+
auto outTopic = graph.ResolveTopic<Reset>();
240+
graph.PushData(EventCount(ResetDetector::kMaxCount));
241+
graph.EvaluateGraph();
242+
243+
//![UnitTest-ResetDetected-1]
244+
//![UnitTest-ResetDetected-2]
245+
DG_ASSERT(!outTopic->HasNewValue());
246+
247+
graph.EvaluateGraph();
248+
249+
DG_ASSERT(outTopic->HasNewValue());
250+
}
251+
//![UnitTest-ResetDetected-2]
252+
253+
//![UnitTest-NoResetDetected-1]
254+
void Test_NoResetDetected()
255+
{
256+
// Arrange
257+
DetectorGraph::Graph graph;
258+
ResetDetector detector(&graph);
259+
auto outTopic = graph.ResolveTopic<Reset>();
260+
graph.PushData(EventCount(ResetDetector::kMaxCount-1));
261+
graph.EvaluateGraph();
262+
263+
DG_ASSERT(!outTopic->HasNewValue());
264+
265+
graph.EvaluateGraph();
266+
DG_ASSERT(!outTopic->HasNewValue());
267+
}
268+
//![UnitTest-NoResetDetected-1]
269+
190270
//![CounterWithResetGraph]
191271
class CounterWithResetGraph : public DetectorGraph::ProcessorContainer
192272
{
@@ -213,6 +293,31 @@ class CounterWithResetGraph : public DetectorGraph::ProcessorContainer
213293
};
214294
//![CounterWithResetGraph]
215295

296+
//![UnitTest-CounterResetIntegration]
297+
void Test_CounterResetIntegration()
298+
{
299+
DetectorGraph::Graph graph;
300+
EventCountDetector counterDetector(&graph);
301+
ResetDetector resetDetector(&graph);
302+
auto outTopic = graph.ResolveTopic<EventCount>();
303+
304+
for (int i = 1; i <= ResetDetector::kMaxCount; i++)
305+
{
306+
graph.PushData(EventHappened());
307+
graph.EvaluateGraph();
308+
DG_ASSERT(outTopic->HasNewValue());
309+
DG_ASSERT(outTopic->GetNewValue().count == i);
310+
311+
while(graph.EvaluateIfHasDataPending()) {} // See also GraphTestUtils::Flush()
312+
}
313+
314+
graph.PushData(EventHappened());
315+
graph.EvaluateGraph();
316+
DG_ASSERT(outTopic->HasNewValue());
317+
DG_ASSERT(outTopic->GetNewValue().count == 1);
318+
}
319+
//![UnitTest-CounterResetIntegration]
320+
216321
//![main]
217322
int main()
218323
{
@@ -221,6 +326,12 @@ int main()
221326
{
222327
counterGraph.ProcessData(EventHappened());
223328
}
329+
330+
Test_Count();
331+
Test_ResetCount();
332+
Test_ResetDetected();
333+
Test_NoResetDetected();
334+
Test_CounterResetIntegration();
224335
}
225336
//![main]
226337

examples/fancyvendingmachine.cpp

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -540,6 +540,25 @@ class UserBalanceDetector : public DetectorGraph::Detector
540540

541541
};
542542

543+
//![UnitTest-LowerUserBalanceOnSale]
544+
void Test_LowerUserBalanceOnSale()
545+
{
546+
DetectorGraph::Graph graph;
547+
UserBalanceDetector detector(&graph);
548+
auto outTopic = graph.ResolveTopic<UserBalance>();
549+
graph.PushData(CoinInserted(kCoinType1d));
550+
graph.EvaluateGraph();
551+
DG_ASSERT(outTopic->HasNewValue());
552+
DG_ASSERT(outTopic->GetNewValue().totalCents == 100);
553+
554+
graph.PushData(Lagged<SaleProcessed>(SaleProcessed(kProductIdTypeNone, 75)));
555+
graph.EvaluateGraph();
556+
557+
DG_ASSERT(outTopic->HasNewValue());
558+
DG_ASSERT(outTopic->GetNewValue().totalCents == 25);
559+
}
560+
//![UnitTest-LowerUserBalanceOnSale]
561+
543562
class SaleProcessor : public DetectorGraph::Detector
544563
, public DetectorGraph::SubscriberInterface<UserBalance>
545564
, public DetectorGraph::SubscriberInterface<SelectedProduct>
@@ -854,6 +873,8 @@ int main()
854873

855874
GraphAnalyzer analyzer(fancyVendingMachine.mGraph);
856875
analyzer.GenerateDotFile("fancy_vending_machine.dot");
876+
877+
Test_LowerUserBalanceOnSale();
857878
}
858879

859880
/// @endcond DO_NOT_DOCUMENT

examples/helloworld.cpp

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -194,6 +194,44 @@ class HelloWorldGraph : public DetectorGraph::ProcessorContainer
194194
};
195195
//![ProcessorContainer]
196196

197+
//![UnitTest-Above-1]
198+
void Test_AboveThreshold() // Adapt to your Unit Test Framework
199+
{
200+
// Arrange
201+
DetectorGraph::Graph graph;
202+
OverheatingDetector detector(&graph);
203+
auto outTopic = graph.ResolveTopic<OverheatingState>();
204+
//![UnitTest-Above-1]
205+
//![UnitTest-Above-2]
206+
207+
// Act
208+
graph.PushData(TemperatureSample(OverheatingDetector::kThreshold+1));
209+
graph.EvaluateGraph();
210+
211+
//![UnitTest-Above-2]
212+
//![UnitTest-Above-3]
213+
// Assert
214+
DG_ASSERT(outTopic->HasNewValue()); // Adapt to your Unit Test Framework
215+
DG_ASSERT(outTopic->GetNewValue().isOverheating == true); // Adapt to your Unit Test Framework
216+
}
217+
//![UnitTest-Above-3]
218+
219+
void Test_BelowThreshold()
220+
{
221+
// Arrange
222+
DetectorGraph::Graph graph;
223+
OverheatingDetector detector(&graph);
224+
auto outTopic = graph.ResolveTopic<OverheatingState>();
225+
226+
// Act
227+
graph.PushData(TemperatureSample(OverheatingDetector::kThreshold-1));
228+
graph.EvaluateGraph();
229+
230+
// Assert
231+
DG_ASSERT(outTopic->HasNewValue());
232+
DG_ASSERT(outTopic->GetNewValue().isOverheating == false);
233+
}
234+
197235
//![main]
198236
int main()
199237
{
@@ -222,6 +260,11 @@ int main()
222260
thermostat.ProcessData(TemperatureSample(110));
223261
thermostat.ProcessData(TemperatureSample(120));
224262
//![Using ProcessorContainer]
263+
264+
// Normally your Unit test framework of choice would call this
265+
// automatically. We do explicitly for demonstration purposes.
266+
Test_AboveThreshold();
267+
Test_BelowThreshold();
225268
}
226269
//![main]
227270

0 commit comments

Comments
 (0)