An example of a design pattern I like to call the "double hexagon". Inspired by the hexagonal architecture the double hexagon involves running the exact same tests against the application domain logic and the external user interface. Using ports and adapters in both the test and application code means two hexagons are in use, one for the tests and one for the application code.
@aslakhellesoy taught me how to do this.
The app test runs the same two simple test cases against both the Account domain object and the HTTP API by enumerating two different configurations generated by factories.
There are two implementations of the AccountStore
contract:
These are covered by the same contract test that ensures they are interchangeable.
There are three implementations of the BankApp
interface, so the same tests
can run against three different targets:
- BankApp talks to the domain (via a store)
- BankServer exposes the same behaviour as a collection of web routes
- BankDomApp exposes the same behaviour as a hyperdom browser app
To make the same tests run against these alternative implementations, the tests themselves need adapters (hence the double hexagon):
- BankApiClient connects the tests to the HTTP API using httpism.
- BankDomClient connects the same tests to the browser app, using browser-monkey.
Run the core tests using the mocha shim:
./mocha
...which passes the --harmony
flag to node, for async/await
support as well
as any additional standard mocha arguments.
With no arguments mocha will run all tests against two configurations of the app, first directly targeting the domain logic, secondly targeting the HTTP API, third via the browser app.
The third hexagon pair uses a browser, so it will only run under electron-mocha via a similar shim:
./electron-mocha
AccountStore (MemoryAccountStore)
✓ assigns accountNumbers when creating accounts
✓ stores and retrieves accounts
✓ gets different account objects for the same account number
✓ stores copies of accounts
AccountStore (FlatFileAccountStore)
✓ assigns accountNumbers when creating accounts
✓ stores and retrieves accounts
✓ gets different account objects for the same account number
✓ stores copies of accounts
AppCore (MemoryAccountStore)
making a transfer
✓ decreases the balance of the sender account
✓ increases the balance of the receiver account
AppCore (FlatFileAccountStore)
making a transfer
✓ decreases the balance of the sender account
✓ increases the balance of the receiver account
AppViaApi (MemoryAccountStore)
making a transfer
✓ decreases the balance of the sender account
✓ increases the balance of the receiver account
AppViaApi (FlatFileAccountStore)
making a transfer
✓ decreases the balance of the sender account
✓ increases the balance of the receiver account
AppViaDom (MemoryAccountStore)
making a transfer
✓ decreases the balance of the sender account (110ms)
✓ increases the balance of the receiver account (115ms)
AppViaDom (FlatFileAccountStore)
making a transfer
✓ decreases the balance of the sender account (96ms)
✓ increases the balance of the receiver account (113ms)
20 passing (968ms)
Use the -f
flag to run a subset for a particular hexagon-pair:
./mocha -f Core
./mocha -f Api
./electron-mocha -f Dom
...or all configurations using a particular AccountStore
implementation:
./mocha -f FlatFileAccountStore
./electron-mocha -f MemoryAccountStore