Skip to content

Commit 22aab6f

Browse files
committed
integration testsing bit
1 parent b8c46ba commit 22aab6f

1 file changed

Lines changed: 148 additions & 1 deletion

File tree

_posts/2020-01-25-testing_external_api_calls.md

Lines changed: 148 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -237,8 +237,155 @@ cons:
237237
so you need a sandbox, or some way of faking it out irl
238238

239239

240+
# Step 1: Build an Adapter (a wrapper for the external API)
240241

241-
# step 1: DI
242+
```python
243+
class RealCargoAPI:
244+
API_URL = 'https://example.org'
245+
246+
def sync(self, shipment: Shipment) -> None:
247+
external_shipment_id = self._get_shipment_id(shipment.reference)
248+
if external_shipment_id is None:
249+
requests.post(f'{self.API_URL}/shipments/', json={
250+
...
251+
252+
else:
253+
requests.put(f'{self.API_URL}/shipments/{external_shipment_id}/', json={
254+
...
255+
256+
257+
def _get_shipment_id(self, our_reference) -> Optional[str]:
258+
try:
259+
their_shipments = requests.get(f"{self.API_URL}/shipments/").json()['items']
260+
return next(
261+
...
262+
except requests.exceptions.RequestException:
263+
...
264+
265+
```
266+
267+
Now how do our tests look?
268+
269+
270+
```python
271+
def test_create_shipment_syncs_to_api():
272+
with mock.patch('use_cases.cargo_api') as mock_cargo_api:
273+
shipment = create_shipment({'sku1': 10}, incoterm='EXW')
274+
assert mock_cargo_api.sync.call_args == mock.call(shipment)
275+
```
276+
277+
Much more manageable!
278+
279+
280+
But:
281+
282+
* we still have the `mock.patch` brittleness, meaning if we change our mind about how
283+
we import things, we need to change our mocks
284+
285+
* and we still need to test the api adapters itself:
286+
287+
288+
```python
289+
def test_sync_does_post_for_new_shipment():
290+
api = RealCargoAPI()
291+
line = OrderLine('sku1', 10)
292+
shipment = Shipment(reference='ref', lines=[line], eta=None, incoterm='foo')
293+
with mock.patch('cargo_api.requests') as mock_requests:
294+
api.sync(shipment)
295+
296+
expected_data = {
297+
'client_reference': shipment.reference,
298+
'arrival_date': None,
299+
'products': [{'sku': 'sku1', 'quantity': 10}],
300+
}
301+
assert mock_requests.post.call_args == mock.call(
302+
API_URL + '/shipments/', json=expected_data
303+
)
304+
```
305+
306+
307+
# Use integration tests to test your Adapter
308+
309+
310+
Now we can test our adapter separately from our main application code, we
311+
can have a think about what the best way to test it is. Since it's just
312+
a thin wrapper around an external system, the best kinds of tests are integration
313+
tests:
314+
315+
```python
316+
def test_can_create_new_shipment():
317+
api = RealCargoAPI('http://localhost:8543')
318+
line = OrderLine('sku1', 10)
319+
ref = random_reference()
320+
shipment = Shipment(reference=ref, lines=[line], eta=None, incoterm='foo')
321+
322+
api.sync(shipment)
323+
324+
shipments = requests.get(api.api_url + '/shipments/').json()['items']
325+
new_shipment = next(s for s in shipments if s['client_reference'] == ref)
326+
assert new_shipment['arrival_date'] is None
327+
assert new_shipment['products'] == [{'sku': 'sku1', 'quantity': 10}]
328+
329+
330+
def test_can_update_a_shipment():
331+
api = RealCargoAPI('http://localhost:8543')
332+
line = OrderLine('sku1', 10)
333+
ref = random_reference()
334+
shipment = Shipment(reference=ref, lines=[line], eta=None, incoterm='foo')
335+
336+
api.sync(shipment)
337+
338+
shipment.lines[0].qty = 20
339+
340+
api.sync(shipment)
341+
342+
shipments = requests.get(api.api_url + '/shipments/').json()['items']
343+
new_shipment = next(s for s in shipments if s['client_reference'] == ref)
344+
assert new_shipment['products'] == [{'sku': 'sku1', 'quantity': 20}]
345+
```
346+
347+
That relies on your third-party api having a decent sandbox that you can test against
348+
349+
350+
351+
352+
# Build a fake
353+
354+
355+
```python
356+
from flask import Flask, request
357+
358+
app = Flask('fake-cargo-api')
359+
360+
SHIPMENTS = {} # type: Dict[str, Dict]
361+
362+
@app.route('/shipments/', methods=["GET"])
363+
def list_shipments():
364+
print('returning', SHIPMENTS)
365+
return {'items': list(SHIPMENTS.values())}
366+
367+
368+
@app.route('/shipments/', methods=["POST"])
369+
def create_shipment():
370+
new_id = uuid.uuid4().hex
371+
refs = {s['client_reference'] for s in SHIPMENTS.values()}
372+
if request.json['client_reference'] in refs:
373+
return 'already exists', 400
374+
SHIPMENTS[new_id] = {'id': new_id, **request.json}
375+
print('saved', SHIPMENTS)
376+
return 'ok', 201
377+
378+
379+
@app.route('/shipments/<shipment_id>/', methods=["PUT"])
380+
def update_shipment(shipment_id):
381+
existing = SHIPMENTS[shipment_id]
382+
SHIPMENTS[shipment_id] = {**existing, **request.json}
383+
print('updated', SHIPMENTS)
384+
return 'ok', 200
385+
```
386+
387+
388+
# step 2: DI
242389

243390
example
244391

0 commit comments

Comments
 (0)