16. The answer
final class Reading
{
// ...
public function low(): int;
public function high(): int;
}
17. The implementation
final class EnerjiraMeterReadings implements MeterReadings
{
public function getForCustomer(CustomerId $customerId): Reading
{
// use the HTTP client here
}
}
20. Use case tests
Given customer "C" has a meter reading of 0 (low) and 100 (high)
And the high tariff for customer "C" is 0.05
When the system creates an invoice for customer "C"
Then the total amount on that invoice should be 5.00
21. use BehatBehatContextContext;
final class InvoicingContext implements Context
{
/**
* @When the system creates an invoice for customer :customerId
*/
public function createAnInvoice(string $customerId): void
{
// The step succeeds if it doesn't throw an exception
}
}
22. Don't use external systems
● Database
● Remote API
● File system
● System clock
● Random data
23. Testing
● If your dependency is an HTTP client, you'd have
to "mock" the response
24.
25. Testing
● This ties your test to the specs of the Enerjira
API, even though that is really an
implementation detail.
26. Testing
● What if their API changes?
● What if we need to migrate to a different
service?
27. Testing
● If your dependency is a MeterReader, you can
create a simple stub (or fake).
28.
29. The tests are better off with an
abstraction
● The high-level use case test doesn't have to deal
with low-level details
30. The tests are better off with an
abstraction
● Test doubles are easier to set up
31. The tests are better off with an
abstraction
● Application service and test code all speak
"domain language"
35. It's your abstraction
Infrastructure code can be changed at any time:
– When we have to switch to Elektrickery, which uses
FTP
– Once we start installing our own smart meters
36. Question: do I need an abstraction?
When you reach outside your application,
introduce an abstraction.