-
Notifications
You must be signed in to change notification settings - Fork 200
Tutorial: Testing
In this chapter of the tutorial we will see how to test code using Fruit, using component replacements.
The full source is available in examples/testing.
Let's say that we have a Greeter
class defined as follows:
// greeter.h
class Greeter {
public:
virtual std::string greet() = 0;
};
fruit::Component<Greeter> getGreeterComponent();
class GreeterImpl : public Greeter {
public:
INJECT(GreeterImpl()) = default;
std::string greet() override {
return "Hello, world!";
}
};
fruit::Component<Greeter> getGreeterComponent() {
return fruit::createComponent()
.bind<Greeter, GreeterImpl>();
}
And we want to cache the result of greet()
in a key-value storage system, so we have:
// cached.h
// Used to annotate (using fruit::Annotated<>) types that have a cached implementation.
struct Cached {};
// cached_greeter.h
fruit::Component<fruit::Annotated<Cached, Greeter>> getCachedGreeterComponent();
// cached_greeter.cpp
class CachedGreeterImpl : public Greeter {
private:
Greeter* greeter;
KeyValueStorage* keyValueStorage;
public:
INJECT(CachedGreeterImpl(Greeter* greeter, KeyValueStorage* keyValueStorage))
: greeter(greeter), keyValueStorage(keyValueStorage) {
}
std::string greet() override {
std::string greeting = keyValueStorage->get("greeting");
if (!greeting.empty()) {
return greeting;
}
greeting = greeter->greet();
keyValueStorage->put("greeting", greeting);
return greeting;
}
};
fruit::Component<fruit::Annotated<Cached, Greeter>> getCachedGreeterComponent() {
return fruit::createComponent()
.bind<fruit::Annotated<Cached, Greeter>, CachedGreeterImpl>()
.install(getKeyValueStorageComponent)
.install(getGreeterComponent);
}
Using the following component for the key-value storage:
// key_value_storage.h
class KeyValueStorage {
public:
// Stores a value associated with the given key.
virtual void put(std::string key, std::string value) = 0;
// Returns the value previously associated with the given key, or an empty string otherwise.
virtual std::string get(std::string key) = 0;
};
fruit::Component<KeyValueStorage> getKeyValueStorageComponent();
// key_value_storage.cpp
class KeyValueStorageImpl : public KeyValueStorage {
public:
INJECT(KeyValueStorageImpl()) = default;
void put(std::string key, std::string value) override {
// Imagine the real implementation here, with network communication.
...
}
std::string get(std::string key) override {
// Imagine the real implementation here, with network communication.
...
}
};
fruit::Component<KeyValueStorage> getKeyValueStorageComponent() {
return fruit::createComponent()
.bind<KeyValueStorage, KeyValueStorageImpl>();
}
So the full system looks like this:
Now we want to add a unit test for cached_greeter.cpp
. In the test, we want to use getCachedGreeterComponent
except for the getKeyValueStorageComponent
that it installs (because it would do network communication and we don't
want to do that in our test).
So we write a fake implementation of the key-value store:
// fake_key_value_storage.h
fruit::Component<KeyValueStorage> getFakeKeyValueStorageComponent();
// fake_key_value_storage.cpp
class FakeKeyValueStorage : public KeyValueStorage {
private:
std::map<std::string, std::string> storage;
public:
INJECT(FakeKeyValueStorage()) = default;
void put(std::string key, std::string value) override {
storage[key] = value;
}
std::string get(std::string key) override {
return storage[key];
}
};
fruit::Component<KeyValueStorage> getFakeKeyValueStorageComponent() {
return fruit::createComponent()
.bind<KeyValueStorage, FakeKeyValueStorage>();
}
Now we use Fruit's replace().with()
to do the replacement, as follows:
fruit::Component<fruit::Annotated<Cached, Greeter>> getMainComponent() {
return fruit::createComponent()
.replace(getKeyValueStorageComponent).with(getFakeKeyValueStorageComponent)
.install(getCachedGreeterComponent);
}
This results in a component that looks like this:
Note that KeyValueStorageComponent
has been replaced by FakeKeyValueStorageComponent
. In this case
KeyValueStorageComponent
was installed only once and directly by getCachedGreeterComponent
, however
replace().with()
also work with components installed at any level of nesting and any number of times.
However, replace().with()
must be before the install()
in getMainComponent
, otherwise Fruit will report the wrong
order as a run-time error.
Now that we have the main component to use for the test, we can write the unit tests:
fruit::Injector<fruit::Annotated<Cached, Greeter>> createInjector() {
return fruit::Injector<fruit::Annotated<Cached, Greeter>>(getMainComponent);
}
TEST(CachedGreeter, NotYetCached) {
fruit::Injector<fruit::Annotated<Cached, Greeter>> injector = createInjector();
Greeter* greeter = injector.get<fruit::Annotated<Cached, Greeter*>>();
ASSERT_EQ(greeter->greet(), "Hello, world!");
}
TEST(CachedGreeter, Cached) {
fruit::Injector<fruit::Annotated<Cached, Greeter>> injector = createInjector();
Greeter* greeter = injector.get<fruit::Annotated<Cached, Greeter*>>();
greeter->greet();
ASSERT_EQ(greeter->greet(), "Hello, world!");
}
Here we're using the googletest library, however this is just an example. Fruit doesn't have any specific support for googletest, any other testing framework would also work.
By writing the tests this way, Fruit will create the injector from scratch every time (i.e., it will execute the
relevant get*Component
functions once for each test). If we have a complex hierarchy with many components,
we can use NormalizedComponent
as an optimization, replacing the above createInjector
function with:
fruit::Component<> getEmptyComponent() {
return fruit::createComponent();
}
fruit::Injector<fruit::Annotated<Cached, Greeter>> createInjector() {
static fruit::NormalizedComponent<fruit::Annotated<Cached, Greeter>> normalizedComponent(getMainComponent);
return fruit::Injector<fruit::Annotated<Cached, Greeter>>(normalizedComponent, getEmptyComponent);
}
This way all get*Component
functions will be executed only once, even though each test will still have a separate
injector; so e.g. we'll still get a fresh instance of FakeKeyValueStorage
in each test instead of having one that was
potentially filled by a previous test (as would be the case if we had a static Injector
).
This concludes the tutorial. Now you should be able to use Fruit effectively.
For more details on each feature of Fruit, see the Quick reference or the FAQ. If something is still unclear, don't hesitate to send me an email.