Interfacing with hard-to-test third-party code
January 7th, 2009 | Published in Google Testing
by Miško Hevery
Shahar asks an excellent question about how to deal with frameworks which we use in our projects, but which were not written with testability in mind.
Unless these APIs are written with testability in mind, they will hamper your ability to write tests.
Calling Third-Party Libraries
I always try to separate myself from third party library with a Facade and an Adapter. Facade is an interface which has a simplified view of the third-party API. Let me give you an example. Have a look at javax.naming.ldap. It is a collection of several interfaces and classes, with a complex way in which you have to call them. If your code depends on this interface you will drown in mocking hell. Now I don't know why the API is so complex, but I do know that my application only needs a fraction of these calls. I also know that many of these calls are configuration specific and outside of bootstrapping code these APIs are cluttering what I have to mock out.
I start from the other end. I ask myself this question. 'What would an ideal API look like for my application?' The key here is 'my application' An application which only needs to authenticate will have a very different 'ideal API' than an application which needs to manage the LDAP. Because we are focusing on our application the resulting API is significantly simplified. It is very possible that for most applications the ideal interface may be something along these lines.
As you can see this interface is a lot simpler to mock and work with than the original one as a result it is a lot more testable. In essence the ideal interfaces are what separates the testable world from the legacy world.
Once we have an ideal interface all we have to do is implement the adapter which bridges our ideal interface with the actual one. This adapter may be a pain to test, but at least the pain is in a single location.
The benefit of this is that:
Plugging into an Existing Framework
Let's take servlets as an example of hard to test framework. Why are servlets hard to test?
At a high level I use the same strategy of separating myself from the servlet APIs. I implement my actions in a separate class
The code above is easy to test because:
What we have achieved is that all of our application logic is in the LoginPage and all of the untestable mess is in the LoginServlet which acts like an adapter. We can than test the LoginPage in depth. The LoginSevlet is not so simple, and in most cases I just don't bother testing it since there can only be wiring bug in that code. There should be no application logic in the LoginServlet since we have moved all of the application logic to LoginPage.
Let's look at the adapter class:
Notice the use of two constructors. One fully dependency injected and the other no argument. If I write a test I will use the dependency injected constructor which will than allow me to mock out all of my dependencies.
Also notice that the no argument constructor is forcing me to use global state, which is very bad, but in the case of servlets I have no choice. However, I make sure that only servlets access the global state and the rest of my application is unaware of this global variable and uses proper dependency injection techniques.
BTW there are many frameworks out there which sit on top of servlets and which provide you a very testable APIs. They all achieve this by separating you from the servlet implementation and from HttpServletRequest and HttpServletResponse. For example Waffle and WebWork
Shahar asks an excellent question about how to deal with frameworks which we use in our projects, but which were not written with testability in mind.
Hi Misko, First I would like to thank you for the “Guide to Writing Testable Code”, which really helped me to think about better ways to organize my code and architecture. Trying to apply the guide to the code I’m working on, I came up with some difficulties. Our code is based on external frameworks and libraries. Being dependent on external frameworks makes it harder to write tests, since test setup is much more complex. It’s not just a single class we’re using, but rather a whole bunch of classes, base classes, definitions and configuration files. Can you provide some tips about using external libraries or frameworks, in a manner that will allow easy testing of the code?
-- Thanks, Shahar
There are two different kind of situations you can get yourself into:
- Either your code calls a third-party library (such as you calling into LDAP authentication, or JDBC driver)
- Or a third party library calls you and forces you to implement an interface or extend a base class (such as when using servlets).
Unless these APIs are written with testability in mind, they will hamper your ability to write tests.
Calling Third-Party Libraries
I always try to separate myself from third party library with a Facade and an Adapter. Facade is an interface which has a simplified view of the third-party API. Let me give you an example. Have a look at javax.naming.ldap. It is a collection of several interfaces and classes, with a complex way in which you have to call them. If your code depends on this interface you will drown in mocking hell. Now I don't know why the API is so complex, but I do know that my application only needs a fraction of these calls. I also know that many of these calls are configuration specific and outside of bootstrapping code these APIs are cluttering what I have to mock out.
I start from the other end. I ask myself this question. 'What would an ideal API look like for my application?' The key here is 'my application' An application which only needs to authenticate will have a very different 'ideal API' than an application which needs to manage the LDAP. Because we are focusing on our application the resulting API is significantly simplified. It is very possible that for most applications the ideal interface may be something along these lines.
interface Authenticator {
boolean authenticate(String username,
String password);
}
As you can see this interface is a lot simpler to mock and work with than the original one as a result it is a lot more testable. In essence the ideal interfaces are what separates the testable world from the legacy world.
Once we have an ideal interface all we have to do is implement the adapter which bridges our ideal interface with the actual one. This adapter may be a pain to test, but at least the pain is in a single location.
The benefit of this is that:
- We can easily implement an InMemoryAuthenticator for running our application in the QA environment.
- If the third-party APIs change than those changes only affect our adapter code.
- If we now have to authenticate against a Kerberos or Windows registry the implementation is straight forward.
- We are less likely to introduce a usage bug since calling the ideal API is simpler than calling the original API.
Plugging into an Existing Framework
Let's take servlets as an example of hard to test framework. Why are servlets hard to test?
- Servlets require a no argument constructor which prevents us from using dependency injection. See how to think about the new operator.
- Servlets pass around HttpServletRequest and HttpServletResponse which are very hard to instantiate or mock.
At a high level I use the same strategy of separating myself from the servlet APIs. I implement my actions in a separate class
class LoginPage {
Authenticator authenticator;
boolean success;
String errorMessage;
LoginPage(Authenticator authenticator) {
this.authenticator = authenticator;
}
String execute(Mapparameters,
String cookie) {
// do some work
success = ...;
errorMessage = ...;
}
String render(Writer writer) {
if (success)
return "redirect URL";
else
writer.write(...);
}
}
The code above is easy to test because:
- It does not inherit from any base class.
- Dependency injection allows us to inject mock authenticator (Unlike the no argument constructor in servlets).
- The work phase is separated from the rendering phase. It is really hard to assert anything useful on the Writer but we can assert on the state of the LoginPage, such as success and errorMessage.
- The input parameters to the LoginPage are very easy to instantiate. (Map
, String for cookie, or a StringWriter for the writer).
What we have achieved is that all of our application logic is in the LoginPage and all of the untestable mess is in the LoginServlet which acts like an adapter. We can than test the LoginPage in depth. The LoginSevlet is not so simple, and in most cases I just don't bother testing it since there can only be wiring bug in that code. There should be no application logic in the LoginServlet since we have moved all of the application logic to LoginPage.
Let's look at the adapter class:
class LoginServlet extends HttpServlet {
ProviderloginPageProvider;
// no arg constructor required by
// Servlet Framework
LoginServlet() {
this(Global.injector
.getProvider(LoginPage.class));
}
// Dependency injected constructor used for testing
LoginServlet(ProviderloginPageProvider) {
this.loginPageProvider = loginPageProvider;
}
service(HttpServletRequest req,
HttpServletResponse resp) {
LoginPage page = loginPageProvider.get();
page.execute(req.getParameterMap(),
req.getCookies());
String redirect = page.render(resp.getWriter())
if (redirect != null)
resp.sendRedirect(redirect);
}
}
Notice the use of two constructors. One fully dependency injected and the other no argument. If I write a test I will use the dependency injected constructor which will than allow me to mock out all of my dependencies.
Also notice that the no argument constructor is forcing me to use global state, which is very bad, but in the case of servlets I have no choice. However, I make sure that only servlets access the global state and the rest of my application is unaware of this global variable and uses proper dependency injection techniques.
BTW there are many frameworks out there which sit on top of servlets and which provide you a very testable APIs. They all achieve this by separating you from the servlet implementation and from HttpServletRequest and HttpServletResponse. For example Waffle and WebWork