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 expectprocess_foo
to be a black-box, so I'd like to replace it with something during testing, so I can refactorprocess_foo
without breaking unit tests forview
. - In
process_foo
, I'd like to replace the callremote_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).
-
1not sure i understand your problem. Whats wrong with comparing the returned object from each function with an expected response. if it matches, pass.Ewan– Ewan2019年01月23日 15:44:03 +00:00Commented Jan 23, 2019 at 15:44
2 Answers 2
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 ofview
? That way I could avoid theassert_called_once_with
call (which I consider a code smell).lennoff– lennoff2019年01月23日 16:31:07 +00:00Commented Jan 23, 2019 at 16:31 -
I think I like that better, yeah.user214290– user2142902019年01月23日 16:45:37 +00:00Commented Jan 23, 2019 at 16:45
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 thatremote_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 refactorremote_service
, you need integration tests for that.lennoff– lennoff2019年01月28日 20:31:00 +00:00Commented Jan 28, 2019 at 20:31
Explore related questions
See similar questions with these tags.