This repo demonstrates an example of using the hexagonal/ports and adaptors/onion architecture and the advantages it provides for making testing easier. In particular this focuses on an example of using contract testing to verify that in-memory implementations (sometimes referred to as "fakes") have the equivalent behaviour of the external services; which can then be used to make unit testing easier.
Our service retrieves the current weather data from Weather Station, it then adds a comment based on the current weather data, which it then sends to a Weather Reporter service.
flowchart LR
WeatherStation["Weather Station"] --> OurService("Our Service")
OurService --> WeatherReporter["Weather Reporter"]
We can create equivalent in-memory implementations of the external services for use in unit tests. Contract tests are created to verify that both the in-memory and live service implementations have the same behaviour i.e. they conform to the same contract.
graph LR
subgraph WeatherStation["Weather Station"]
InMemoryWeatherStation["In-Memory"]
RemoteWeatherStation["Remote Service"]
end
subgraph OurService["Our Service"]
WeatherStationForOurService["Weather Station"]
WeatherReporterForOurService["Weather Reporter"]
end
subgraph WeatherReporter["Weather Reporter"]
InMemoryWeatherReporter["In-Memory"]
RemoteWeatherReporter["Remote Service"]
end
InMemoryWeatherStation & RemoteWeatherStation -.-> ContractTestingForWeatherStation("Contract Testing")
InMemoryWeatherReporter & RemoteWeatherReporter -.-> ContractTestingForWeatherReporter("Contract Testing")
WeatherStation ~~~ OurService
OurService ~~~ WeatherReporter
WeatherStation --> WeatherStationForOurService
WeatherReporter --> WeatherReporterForOurService
I offer a hand-drawn diagram as the Mermaid rendering engine currently isn't rendering a sensible diagram:
Now that we've proven that both implementations obey the same contract. We can then use the in-memory implementations in unit-testing!
From What's Hexagonal Architecture? by Luis Soares
In this repo, we emulate remote services via Docker and Docker Compose. In real life, these would be remote services that your service interacts with, but only exist remotely i.e. cannot be spun up locally for local development.
Navigate to our service at our-service/.
cd our-serviceNext start up the remote services:
npm run start-servicesFinally we can start up our service. Note that you need to provide the base URLs of the remote services via the following environment variables:
REMOTE_WEATHER_STATION_URLREMOTE_WEATHER_REPORTER_URL
The default values are provided below (see containerization/docker-compose.yaml for further configuration)
REMOTE_WEATHER_STATION_URL=http://localhost:80 REMOTE_WEATHER_REPORTER_URL=http://localhost:82 node index.jsThe external services can be stopped with
npm run stop-servicesTo run unit tests:
npm run test:unitAnd likewise, for contract tests:
npm run test:contract-
Contract Test, Martin Fowler, https://martinfowler.com/bliki/ContractTest.html
-
Consumer-Driven Contract Testing (CDC), Microsoft Engineering Fundamentals Playbook, https://microsoft.github.io/code-with-engineering-playbook/automated-testing/cdc-testing/
-
Ports & Adapters (aka hexagonal) architecture explained, Code Soapbox by Daniel Frąk, https://codesoapbox.dev/ports-adapters-aka-hexagonal-architecture-explained/