DataDecorator tests
In Chapter 5, Data, we created various classes deriving from DataDecorator. Let's create companion test classes for each of those and test the following functionalities:
- Object construction
- Setting the value
- Getting the value as JSON
- Updating the value from JSON
In cm-tests/source/data, create the DateTimeDecoratorTests, EnumeratorDecoratorTests, IntDecoratorTests, and StringDecoratorTests classes.
Let’s begin with the simplest suite, IntDecoratorTests. The tests will be broadly similar across the suites, so once we’ve written one suite, we will be able to copy most of it across to the other suites and then supplement as necessary.
int-decorator-tests.h:
#ifndef INTDECORATORTESTS_H #define INTDECORATORTESTS_H
#include <QtTest>
#include <data/int-decorator.h> #include <test-suite.h>
namespace cm { namespace data {
class IntDecoratorTests : public TestSuite { Q_OBJECT
public: IntDecoratorTests();
private slots: void constructor_givenNoParameters_setsDefaultProperties(); void constructor_givenParameters_setsProperties(); void setValue_givenNewValue_updatesValueAndEmitsSignal(); void setValue_givenSameValue_takesNoAction(); void jsonValue_whenDefaultValue_returnsJson(); void jsonValue_whenValueSet_returnsJson(); void update_whenPresentInJson_updatesValue(); void update_whenNotPresentInJson_updatesValueToDefault(); };
}}
#endif
A common approach is to follow a “method as a unit” approach, where each method is the smallest testable unit in a class and then that unit is tested in multiple ways. So we begin by testing the constructor, both with and without parameters. The setValue() method should only do anything when we actually change the value, so we test both setting a different value and the same value. Next, we test that we can convert the decorator to a JSON value, both with a default value (0 in the case of an int) and with a set value. Finally, we perform a couple of tests against the update() method. If we pass in a JSON that contains the property, then we expect the value to be updated as per the JSON value. However, if the property is missing from the JSON, we expect the class to handle it gracefully and reset to a default value instead.
Note that we aren’t explicitly testing the value() method. This is just a simple accessor method with no side effects, and we will be calling it in the other unit tests, so we will be indirectly testing it there. Feel free to create additional tests for it if you wish.
int-decorator-tests.cpp:
#include "int-decorator-tests.h"
#include <QSignalSpy>
#include <data/entity.h>
namespace cm { namespace data { // Instance
static IntDecoratorTests instance;
IntDecoratorTests::IntDecoratorTests() : TestSuite( "IntDecoratorTests" ) { }
}
namespace data { // Tests
void IntDecoratorTests::constructor_givenNoParameters_setsDefaultProperties() { IntDecorator decorator; QCOMPARE(decorator.parentEntity(), nullptr); QCOMPARE(decorator.key(), QString("SomeItemKey")); QCOMPARE(decorator.label(), QString("")); QCOMPARE(decorator.value(), 0); }
void IntDecoratorTests::constructor_givenParameters_setsProperties() { Entity parentEntity; IntDecorator decorator(&parentEntity, "Test Key", "Test Label",
99); QCOMPARE(decorator.parentEntity(), &parentEntity); QCOMPARE(decorator.key(), QString("Test Key")); QCOMPARE(decorator.label(), QString("Test Label")); QCOMPARE(decorator.value(), 99); }
void IntDecoratorTests::setValue_givenNewValue_updatesValueAndEmitsSignal() { IntDecorator decorator; QSignalSpy valueChangedSpy(&decorator,
&IntDecorator::valueChanged); QCOMPARE(decorator.value(), 0); decorator.setValue(99); QCOMPARE(decorator.value(), 99); QCOMPARE(valueChangedSpy.count(), 1); }
void IntDecoratorTests::setValue_givenSameValue_takesNoAction() { Entity parentEntity; IntDecorator decorator(&parentEntity, "Test Key", "Test Label",
99); QSignalSpy valueChangedSpy(&decorator,
&IntDecorator::valueChanged); QCOMPARE(decorator.value(), 99); decorator.setValue(99); QCOMPARE(decorator.value(), 99); QCOMPARE(valueChangedSpy.count(), 0); }
void IntDecoratorTests::jsonValue_whenDefaultValue_returnsJson() { IntDecorator decorator; QCOMPARE(decorator.jsonValue(), QJsonValue(0)); } void IntDecoratorTests::jsonValue_whenValueSet_returnsJson() { IntDecorator decorator; decorator.setValue(99); QCOMPARE(decorator.jsonValue(), QJsonValue(99)); }
void IntDecoratorTests::update_whenPresentInJson_updatesValue() { Entity parentEntity; IntDecorator decorator(&parentEntity, "Test Key", "Test Label", 99); QSignalSpy valueChangedSpy(&decorator,
&IntDecorator::valueChanged); QCOMPARE(decorator.value(), 99); QJsonObject jsonObject; jsonObject.insert("Key 1", "Value 1"); jsonObject.insert("Test Key", 123); jsonObject.insert("Key 3", 3); decorator.update(jsonObject); QCOMPARE(decorator.value(), 123); QCOMPARE(valueChangedSpy.count(), 1); }
void IntDecoratorTests::update_whenNotPresentInJson_updatesValueToDefault() { Entity parentEntity; IntDecorator decorator(&parentEntity, "Test Key", "Test Label",
99); QSignalSpy valueChangedSpy(&decorator,
&IntDecorator::valueChanged); QCOMPARE(decorator.value(), 99); QJsonObject jsonObject; jsonObject.insert("Key 1", "Value 1"); jsonObject.insert("Key 2", 123); jsonObject.insert("Key 3", 3); decorator.update(jsonObject); QCOMPARE(decorator.value(), 0); QCOMPARE(valueChangedSpy.count(), 1); }
}}
Unit tests tend to follow an Arrange > Act > Assert pattern. Preconditions for the test are fulfilled first: variables are initialized, classes are configured, and so on. Then, an action is performed, generally calling the function being tested. Finally, the results of the action are checked. Sometimes one or more of these steps will not be necessary or may be merged with another, but that is the general pattern.
We begin testing the constructor by initializing a new IntDecorator without passing in any parameters and then test that the various properties of the object have been initialized to expected default values using QCOMPARE to match actual against expected values. We then repeat the test, but this time, we pass in values for each of the parameters and verify that they have been updated in the instance.
When testing the setValue() method, we need to check whether or not the valueChanged() signal is emitted. We can do this by connecting a lambda to the signal that sets a flag when called, as follows:
bool isCalled = false; QObject::connect(&decorator, &IntDecorator::valueChanged, [&isCalled](){ isCalled = true; });
/*...Perform action...*/ QVERIFY(isCalled);
However, a much simpler solution we’ve used here is to use Qt’s QSignalSpy class that keeps track of calls to a specified signal. We can then check how many times a signal has been called using the count() method.
The first setValue() test ensures that when we provide a new value that is different to the existing one, the value is updated and the valueChanged() signal is emitted once. The second test ensures that when we set the same value, no action is taken and the signal is not emitted. Note that we use an additional QCOMPARE call in both cases to assert that the value is what we expect it to be before the action is taken. Consider the following pseudo test:
- Set up your class.
- Perform an action.
- Test that the value is 99.
If everything works as expected, step 1 sets the value to 0, step 2 takes the correct action and updates the value to 99, and step 3 passes because the value is 99. However, step 1 could be faulty and wrongly sets the value to 99, step 2 is not even implemented and takes no action, and yet step 3 (and the test) passes because the value is 99. With a QCOMPARE precondition after step 1, this is avoided.
The jsonValue() tests are simple equality checks, both with a default value and a set value.
Finally, with the update() tests, we construct a couple of JSON objects. In one object, we add an item that has the same key as our decorator object (“Test Key”), which we expect to be matched and the associated value (123) passed through to setValue(). In the second object, the key is not present. In both cases, we also add other extraneous items to ensure that the class can correctly ignore them. The post action checks are the same as for the setValue() tests.
The StringDecoratorTests class is essentially the same as IntDecoratorTests, just with a different value data type and default values of empty string "" rather than 0.
DateTimeDecorator also follows the same pattern, but with additional tests for the string formatting helper methods toIso8601String() and so on.
EnumeratorDecoratorTests performs the same tests but requires a little more setup because of the need for an enumerator and associated mapper. In the body of the tests, whenever we test value(), we also need to test valueDescription() to ensure that the two remain aligned. For example, whenever the value is eTestEnum::Value2, the valueDescription() must be Value 2. Note that we always use the enumerated values in conjunction with the value() checks and static_cast them to an int. Consider the following example:
QCOMPARE(decorator.value(), static_cast<int>(eTestEnum::Value2));
It may be tempting to make this much shorter by just using the raw int value:
QCOMPARE(decorator.value(), 2);
The problem with this approach, other than the number 2 having much less meaning to readers of the code than the enumerated Value2, is that the values of eTestEnum can change and render the test invalid. Consider this example:
enum eTestEnum { Unknown = 0, MyAmazingNewTestValue, Value1, Value2, Value3 };
Due to the insertion of MyAmazingNewTestValue, the numeric equivalent of Value2 is actually now 3. Any tests that used the number 2 to represent Value2 are now wrong, whereas those that use the more long-winded static_cast<int>(eTestEnum::Value2) are still correct.
Rebuild and run the new test suites, and they should all happily pass and give us renewed confidence in the code we wrote earlier. With the data decorators tested, let's move on to our data models next.