Isolating external services when unit testing

327 views
Skip to first unread message

James Gregory

unread,
Sep 18, 2011, 3:51:52 PM9/18/11
to play-fr...@googlegroups.com
Hello everyone,

I was hoping someone could give me some advice on unit testing when using Play! and external services.

Specifically, how do you test your code when it interacts with an external service that you don't want to have running when your tests are executing? Think along the lines of a message queue, an SMTP server, or a web service.

From what I've seen so far, Play! doesn't seem to have the concept of a service layer; I can't create a service and inject that into a controller because Play! uses static methods for its actions and so constructor injection isn't available.

Without constructor injection, my code looks like this:

public class User extends Controller {
  public static void register() {
    models.User user = new models.User(...);
    user.save();
    
    MessageQueue.publish(new UserCreatedMessage(user.id));
  }
}

MessageQueue is static, how would I test interactions with that?

I could use Guice or Spring and inject into a static field, but that then necessitates using Guice in my tests and somehow supplying a mocked MessageQueue instance. I could probably live with doing that, but it doesn't feel as nice as using constructor injection.

Then there's the model approach. I understand the anaemic domain model argument, "put it in your model", which is fine, but then my model has a hard-dependency on a service which similarly cannot be replaced at test-time.

In Ruby and other dynamic languages this isn't really an issue. By all means, have your model reference an external service, you can just replace the call dynamically in the test. Sadly, this is Java and we don't have that option without resorting to something like PowerMock, which seems overkill.

With the model approach, it'd look something like this:

public class User extends Model {
  @PostPersist
  void onCreated() {
    MessageQueue.publish(new UserCreatedMessage(this.id));
  }
}

How would you test interactions with the MessageQueue without resorting to querying a real-live queue in your test?

Looking into how the SMTP service is implemented, Mail.java, I see that there's a hard-coded switch (https://github.com/playframework/play/blob/master/framework/src/play/libs/Mail.java#L36) to return a hand-rolled Mock if the config is set to "mock". That feels very hacky, and I wouldn't really want to do that kind of thing in my own production code, especially if I have to deal with several external services.

Does Play! really not support truly isolated tests? What am I missing here? This seems like a fairly big issue to me. Integration tests are important, but I also feel you should be able to test in isolation, especially when it comes to dealing with external services.

Many thanks,
James

mauro.michele

unread,
Sep 22, 2011, 5:31:16 AM9/22/11
to play-fr...@googlegroups.com
Play! supports the @Inject annotation on static fields. The idea is that you inject at startup the service beans you need; for testing pourposes, you can have testing services injected. However, implementing this stuff is more or less up to you: if you don't want to depend on Guice, you have to bring your own implementation. And don't forget that the Injector has a bug:

https://play.lighthouseapp.com/projects/57987-play-framework/tickets/1116-calling-more-than-once-injectorinject-overwrites-already-injected-values

You can call it only once.

My actual setup involves using plugins that inject themselves as interfaces in the controllers that need them (for example to interact with a BPM engine or a CMIS repository) with different credentials for test, dev and production, so that I don't really mock the external services: I point to the test instances.

You can have the same result implementing some sort of service registry, instantiating the correct class (real or mock) at startup, and call Injector.inject to distribute it. The controllers just have to declare @Inject static fields, and they won't know who or what they will find there.
It's true that you are reinventing a small piece of Spring or Guice, but that's all you really need; and you won't need that many lines of code. DI is not a matter of framework, is a matter of architecture.

Reply all
Reply to author
Forward
0 new messages