1

Consider a simple web application, I'll use a Python(ish) example, but i think the question is relevant for other languages as well.

The user is trying to fetch a page, and in order to render that page, the application has to make an external call to a remote service. In the example, I'll try to separate concerns: collecting input parameters, and calling the actual remote service.

def view(request):
  foo = float(request.POST['foo'])
  return Response(process_foo(foo))

def process_foo(foo):
  return remote_service.get('/bar', data={'foo': foo})

In the example, view is a function that's responsible for transforming the incoming request to a Response, and process_foo is responsible for performing some business logic.

What kind of unit tests make sense here?

A few preferences:

  • In view, I'd expect process_foo to be a black-box, so I'd like to replace it with something during testing, so I can refactor process_foo without breaking unit tests for view.
  • In process_foo, I'd like to replace the call remote_service.get, as it's an expensive operation.

Considering the restrictions above, I'm not sure what kinds of unit tests make sense here. In view, I could make an assertion about that process_foo was called with foo, but when I make changes to process_foo, this test will not break. The same is true for testing process_foo: if I make an assertion on that remote_service.get was called with the right URL and the right parameters, that'll not break when the remote URL changes (or it no longer accepts the same parameters).

To me it feels like that somehow I should test that foo was extracted from request.POST, but it seems that there's no reasonable way to do this.

I'm aware that integration tests can solve this problem, I'm looking for a possible solution about how the problem above could be solved with unit tests (if there's a solution at all).

  • 1
    not sure i understand your problem. Whats wrong with comparing the returned object from each function with an expected response. if it matches, pass. – Ewan Jan 23 at 15:44
0

Your ideas make sense.

In view, I could make an assertion about that process_foo was called with foo, but when I make changes to process_foo, this test will not break.

So it is a good thing that process_foo has its own tests that will fail on an improper refactoring.

if I make an assertion on that remote_service.get was called with the right URL and the right parameters, that'll not break when the remote URL changes (or it no longer accepts the same parameters).

If you are concerned about how an external program responds to what you give it, you pretty much by definition are writing an integration test, not a unit test. It is not in the scope of unit testing to detect if an external API changes.

To me it feels like that somehow I should test that foo was extracted from request.POST, but it seems that there's no reasonable way to do this.

Python makes this easy. You just make your own object that has a dictionary attribute named POST and load it with whatever data you need. When you call view with it, mock out process_foo, and use assert_called_once_with()

Finally, if you wish to treat process_foo() as a black-blox within view(), consider refactoring to make it clear this is an "external" dependency. I generally consider using mock.patch to be a code smell (which doesn't mean that it's never the right choice!)

  • I see. Would it make sense to swap (mock.patch) process_foo out with some kind of dummy implementation, and assert instead of the return value of view? That way I could avoid the assert_called_once_with call (which I consider a code smell). – lennoff Jan 23 at 16:31
  • I think I like that better, yeah. – JETM Jan 23 at 16:45
0

I'm aware that integration tests can solve this problem, I'm looking for a possible solution about how the problem above could be solved with unit tests (if there's a solution at all).

Where I would start from is a minor re-design.

def process_foo(foo):
  return remote_service.get('/bar', data={'foo': foo})

The problem here is that we're combining computation and logic (which you want to test) with an expensive side effect (which you don't).

A common answer here is "configurable dependencies", which is a hipster spelling of "dependency injection", which in turn is just a pretentious way of saying "pass an argument".

def process_foo(foo, remote_service):
  return remote_service.get('/bar', data={'foo': foo})

This implementation allows you, in your test, to provide an inexpensive implementation of the remote service (for instance, a test double that just returns a constant).

If the expensive side effect is coming by way of a third party library, then remote_service won't usually be the third party client itself, but your own wrapper around it.

def get(uri, data):
    return third_party_client.get(uri, data)

This kind of wrapper is probably more common in languages like Java with a compiler that is fussy about types. It sometimes looks a bit odd (why bother?), but the motivation is that only a relatively small amount of our code should be coupled to the decision that we have made about how to implement our connection -- see Parnas 1972.

Note that this wrapper is deliberately simple; the third party client being relatively expensive to test means that this code isn't going to be tested as often, and we want to mitigate the risks.

  • replacing remote_service with a mock is not an issue, python has standard tools for this (patch). i also agree that dependency injection makes this a bit easier. i just wasn't sure if it's worth to make an assertion on that remote_service.get was called with the right params, as the tests won't break in case the remote service API changes. i think in general it's worth to make such assertions (so you can refactor with confidence), but that alone won't be sufficient to refactor remote_service, you need integration tests for that. – lennoff Jan 28 at 20:31

Your Answer

By clicking "Post Your Answer", you acknowledge that you have read our updated terms of service, privacy policy and cookie policy, and that your continued use of the website is subject to these policies.

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