16

I've been going round in circles trying to figure out the best way to unit test an API client library I'm developing. The library has a Client class which basically has a 1:1 mapping with the API, and an additional Wrapper class which provides a more user-friendly interface over the top of the Client.

Wrapper --> Client --> External API

I first wrote a bunch of tests against both Client and Wrapper, effectively just testing that that they forward to the appropriate functions of whatever the operate on (Wrapper operates on Client, and Client operates on a HTTP connection). I started feeling uncomfortable with this, however, because I feel like I'm testing the implementation of these classes, rather than the interface. In theory, I could change the classes to have another perfectly valid implementation, but my tests would fail because the functions I expected to be called aren't being called. That sounds like fragile tests to me.

After this, I thought about the interface of the classes. The tests should verify that the classes actually do the job they're meant to do, rather than how they do it. So how can I do this? The first thing that comes to mind is stubbing the external API requests. However, I'm nervous about oversimplifying the external service. A lot of the examples of stubbed APIs I've seen just give canned responses, which sounds like a really easy way to only test that your code happens to run correctly against your fake API. The alternative is mocking the service, which is just infeasible, and would need to be kept up to date whenever the real service changes - that feels like overkill and waste of time.

Finally, I read this from another answer on the programmers SE:

The job of a remote API client is to issue certain calls - no more, no less. Therefore, its test should verify that it issues those calls - no more, no less.

And now I'm more or less convinced - when testing Client, all I need to test is that it makes the correct requests to the API (Of course, there's always the possibility that the API will change but my tests continue to pass - but that's where integration tests would come in useful). Since Client is just a 1:1 mapping with the API, my concern before about changing from one valid implementation to another doesn't really apply - there's only one valid implementation for each method of Client.

However, I'm still stuck with the Wrapper class. I see the following options:

  1. I stub out the Client class and just test that the appropriate methods are called. In this way, I'm doing the same as above but treating the Client as a stand-in for the API. This puts me right back where I started. Once again, this gives me the uncomfortable feeling of testing implementation, not interface. The Wrapper could very well be implemented using a completely different client.

  2. I create a mock Client. Now I have to decide how far to go with mocking it - creating a complete mock of the service would take a lot of effort (more work than has gone into the library itself). The API itself is simple, but the service is quite complex (it's essentially a datastore with operations on that data). And again, I will have to keep my mock in sync with the real Client.

  3. I just test that the appropriate HTTP requests are being made. This means that Wrapper will be calling through a real Client object to make those HTTP requests, so I'm not actually testing it in isolation. This makes it a bit of a terrible unit test.

So I'm not particularly happy with any of these solutions. What would you do? Is there a right way to go about this?

1
  • I tend to avoid unit testing in these scenarios where there's a third party library doing most of the gruntwork and I merely have a wrapper (mainly because I have no idea how to do this in a way that tests anything really meaningful). Generally I do integration testing in those cases, possibly with a mock service. Maybe someone knows how to make a really meaningful unit test for these -- I tend to prioritize the most critical components in the system under our control. Here the critical part is out of our control. :-(
    – user204677
    Dec 23, 2015 at 5:37

1 Answer 1

11

TLDR: Despite the difficulty, you should stub the service and use for client unit testing.


I'm not as certain that the "job of a remote API client is to issue certain calls, no more, no less...", unless the API only consists of endpoints which always return a fixed status, and neither consume nor produce any data. This would not be the most useful API...

You'd also want to check that the client not only sends the correct requests, but that it properly handles response content, errors, redirects, etc. And test for all these cases.

As you note, you should have integration tests that cover the full stack from wrapper -> client -> service -> DB and beyond, but to answer your main question, unless you have an environment where integration tests can be run as part of every CI build without a lot of headaches (shared test databases, etc.), you should invest the time in creating a stub of the API.

The stub will let you create a working implementation of the service, but without having to implement any layer below the service itself.

You could consider using a DI-based solution to accomplish this, with an implementation of the Repository pattern underneath the REST resources:

  • Replace all functional code in the REST handlers with calls to an IWhateverRepository.
  • Create a ProductionWhateverRepository with the code that was extracted from the REST resources, and a TestWhateverRespository which returns canned responses for use during unit testing.
  • Use the DI container to inject either the ProductionWhateverRepository or TestWhateverRepository DLL/class, etc. depending on configuration.

Anyway, unless stubbing and/or refactoring the service is out of the question either politically or practically, I'd probably undertake something similar to the above. If not possible, then I'd make sure to have really good integration coverage and run them as often as possibly given your available test setup.

HTH

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge that you have read and understand our privacy policy and code of conduct.

Not the answer you're looking for? Browse other questions tagged or ask your own question.