Skip to content

Tutorial: Testing

Marco Poletti edited this page Aug 27, 2018 · 3 revisions

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.