Pages

13 May 2010

GuiceModules and TestComponents, a powerful combination



Important update!! [21/05/2010, see at the bottom]


Sorry everyone, that doesn't work

In this previous post, I was mentioning a way to inject the dependencies of a TestComponent without having to inject the component itself.

The objective was, if I pick up the example I was using, to have a TestComponent called "CustomerOrder". That TestComponent is responsible for setting up a proper Customer and its Order, so that they both can be injecting into the TestCases needing them. But the additional requirement was not to have to inject the CustomerOrder component itself.

It turns out that what I was advising to do, create a binding of the CustomerOrder in the CustomerOrderModule doesn't work at all!

And all my other attempts at solving this issue usually ended up in initialization errors where a TestComponent was relying on another for his setup, but that other one was not initialized.

The proper trick

It is actually simple and I don't understand why I haven't been able to come up with it in the first place.
The idea is to:
  • get a set of all the modules, including their transitive dependencies
  • create an injector for this set of modules
  • inject the modules themselves!

/**
* Use all the bindings of this module, including the additional ones
* to inject dependencies
*
*/
public T inject(final T object) {
injector().injectMembers(object);
return object;
}
/**
* @return an injector initialized with all the transitive bindings of this module
*/
private Injector injector() {
if (injector == null) {
injector = Guice.createInjector(getModules());
injectModules();
}
return injector;
}
/**
* modules are injected one after the other following their dependencies order:
* If module A depends on module B, then module B must be injected first
*/
private void injectModules() {
final List sorted = new ArrayList(getModules());
Collections.sort(sorted, new GuiceModuleComparator());
for (final GuiceModule m : sorted)
injector.injectMembers(m);
}


Because the modules are now injected, if you add a dependency inside the module, that dependency will be initialized when the Module is injected. So nothing stops me to inject the CustomerOrder TestComponent into its module so that anytime someone uses the module, the CustomerOrder TestComponent will be fully set-up.

Now, how does the code above solves the initialization issues I've been mentioning?

Well, I guess that you noticed something in that code, the modules are sorted before they're injected. Indeed they are sorted following the "natural" pre-order of their transitive dependencies with the following Comparator:

private static class GuiceModuleComparator implements Comparator {
public int compare(final GuiceModule o1, final GuiceModule o2) {
if (o1.getModules().contains(o2))
return 1;
else if (o2.getModules().contains(o1))
return -1;
return 0;
}
}

This sorting ensures that "low-level" modules will be injected first, then modules depending on them will be injected, respecting the actual dependencies.

So now I can write:

/**
* The bindings definitions
*/
public class CustomerOrderModule extends GuiceModule {
// this injection will setup the Customer/Order before they're used anywhere
@Inject CustomerOrder customerOrder;
@Override protected void configure() {
bind(Customer.class).toInstance(new Customer());
bind(Order.class).toInstance(new Order());
}
}
/**
* The CustomerOrder TestComponent setting up a "typical"
* Customer and its Order
*/
public class CustomerOrder extends TestComponent {
@Inject Customer customer;
@Inject Order order;
@Override protected void before() {
// set up the Customer and order instances here
order.setCustomer(customer);
}
}
/**
* A test case using the Customer and its Order
*/
public class OrderingTestCaseextends TestComponent {
// the customer and order can be injected without having to
// inject the CustomerOrder TestComponent!!
@Inject Customer customer;
@Inject Order order;

@Test public void anOrderMustHaveACustomer() {
assertEquals(customer, order.getCustomer());
}
@Override public GuiceModule module() {
return new CustomerOrderModule();
}
}

Please ask

That's all there is to it. I understand that this may be a bit difficult to grasp at first, but then you realize that you only have a few concepts:
  • a GuiceModule specifies bindings
  • GuiceModules can to have dependencies to other GuiceModules
  • a TestComponent is either setting data (aka TestFixture) or is a TestCase
  • a TestComponent specifies its GuiceModule to define its required bindings

Give it a go, I've been using that for a while now on my project, and the flexibility we got for defining and reusing TestComponents has been really great. And if you have any trouble don't hesitate to ask, I'll be happy to provide some more complete code samples and explanations.

Important update!! [21/05/2010]

Live and learn, they say. Well the sorting of modules proposed above doesn't work either. What I really want is a topological sort. I'm glad that Cedric Beust posted this article once, explaining how he was dealing with the dependencies in TestNG, because that's essentially the same problem here.

So, for the courageous who want to follow the GuiceModules/TestComponent way, here is the sorting code you will need:

/**
* This sort algorithm is taken from http://en.wikipedia.org/wiki/Topological_sorting
* @return the list of sorted modules according to the Topological order
*/
public List sortedModules() {
final List result = new ArrayList();
final Map visited = new HashMap();
for (final GuiceModule m : getModules())
visited.put(m, false);
for (final GuiceModule m : getModules())
visit(m, result, visited);
return new ArrayList(result);
}

/**
* visit a module and add it only if it comes after other modules
*/
private void visit(final GuiceModule m, final List result, final Map visited) {
if (!visited.get(m)) {
visited.put(m, true);
for (final GuiceModule other : getModules()) {
if (!m.equals(other) && !visited.get(other) && GuiceModuleComparator.compare(other, m) < 0)
visit(other, result, visited);
result.add(m);
}
}
}

This is exactly the time when I don't regret reading blog posts as a morning routine in the morning!

No comments: