@@ -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