Skip to content

Commit 0f18f16

Browse files
authored
Merge pull request #1 from lmirabal/list-accounts
List accounts
2 parents 9ebd103 + 9402093 commit 0f18f16

23 files changed

Lines changed: 474 additions & 1 deletion

File tree

README.md

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
# Testing using hexagonal architecture
2+
3+
Use a bank example to demonstrate how using a hexagonal architecture can help to test an application.
4+
5+
## Structure
6+
7+
The application is split into 3 modules:
8+
9+
- domain: business logic driving how the bank behaves.
10+
- http: api to expose the bank functionality via http.
11+
- web: dummy web ui driving the application.
12+
13+
## Solution
14+
15+
The business logic of the bank is fully implemented as part of the domain. As per typical hexagonal architecture, where
16+
an interaction is needed with the outside world, a contract (port) is defined to state how the application expects that
17+
to happen. In this example only 2 are defined:
18+
19+
- one that allows to drive the application by exposing all the banks capabilities - driving or primary port;
20+
- and another to define how the bank accounts data is stored and queried - driven or secondary port.
21+
22+
The isolation of the business logic that the hexagonal architecture provides, allows components to have a single
23+
purpose, only mapping from the technology in use to business terms. This is considered one of its main benefits but here
24+
we attempt to show how it can also greatly help testing.
25+
26+
Having every component in the system defined in terms of contracts means that writing tests based on those very same
27+
contracts allows to have a single suite of tests that can be run against any layer of the system, providing excellent
28+
test coverage for the whole system with small effort.
29+
30+
This example mainly focuses in the contract defining how the business behaves as it highlights the benefits of the
31+
approach, but the same strategy can be applied to any other port. For examples, if there were multiple ways to
32+
store/query accounts, a single test could be defined to test all of them.
33+
34+
## Technology
35+
36+
The intention is to have as simple as possible code so very few external dependencies are used. It's implemented in
37+
[kotlin](https://kotlinlang.org), my preferred programming language, and the only production dependency is the
38+
[http4k](http://http4k.org/) library, which is used to define the http api as well as the server templating in the ui.
39+
40+
For testing, only [junit5](https://junit.org/junit5/) and [hamkrest](https://github.com/npryce/hamkrest) are used.

build.gradle

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ subprojects {
1515
dependencies {
1616
testImplementation 'org.jetbrains.kotlin:kotlin-test-junit5'
1717
testImplementation 'org.junit.jupiter:junit-jupiter-api:5.7.0'
18+
testImplementation 'com.natpryce:hamkrest:1.8.0.1'
1819
testRuntimeOnly 'org.junit.jupiter:junit-jupiter-engine:5.7.0'
1920
}
2021

domain/build.gradle

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
plugins {
2+
id 'java-test-fixtures'
3+
}
4+
5+
dependencies {
6+
testFixturesImplementation 'org.jetbrains.kotlin:kotlin-test-junit5'
7+
testFixturesImplementation 'org.junit.jupiter:junit-jupiter-api:5.7.0'
8+
testFixturesImplementation 'com.natpryce:hamkrest:1.8.0.1'
9+
}
10+
11+
compileTestFixturesKotlin {
12+
kotlinOptions.jvmTarget = '11'
13+
}
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
package lmirabal.bank
2+
3+
import lmirabal.bank.model.BankAccount
4+
5+
interface Bank {
6+
fun createAccount(): BankAccount
7+
fun listAccounts(): List<BankAccount>
8+
}
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
package lmirabal.bank
2+
3+
import lmirabal.bank.data.BankAccountRepository
4+
import lmirabal.bank.model.Amount
5+
import lmirabal.bank.model.BankAccount
6+
import lmirabal.bank.model.BankAccountId
7+
8+
class BankService(
9+
private val accountRepository: BankAccountRepository,
10+
private val idFactory: () -> BankAccountId = { BankAccountId.random() }
11+
) : Bank {
12+
override fun createAccount(): BankAccount {
13+
return BankAccount(idFactory(), Amount.ZERO)
14+
.also { newAccount -> accountRepository.add(newAccount) }
15+
}
16+
17+
override fun listAccounts(): List<BankAccount> = accountRepository.list()
18+
}
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
package lmirabal.bank.data
2+
3+
import lmirabal.bank.model.BankAccount
4+
5+
interface BankAccountRepository {
6+
fun add(account: BankAccount)
7+
fun list(): List<BankAccount>
8+
}
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
package lmirabal.bank.data
2+
3+
import lmirabal.bank.model.BankAccount
4+
5+
class InMemoryBankAccountRepository : BankAccountRepository {
6+
private val accounts: MutableList<BankAccount> = arrayListOf()
7+
8+
override fun add(account: BankAccount) {
9+
accounts.add(account)
10+
}
11+
12+
override fun list(): List<BankAccount> = accounts
13+
}
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
package lmirabal.bank.model
2+
3+
data class Amount(val minorUnits: Long) {
4+
5+
companion object {
6+
val ZERO = Amount(0)
7+
}
8+
}
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
package lmirabal.bank.model
2+
3+
import java.util.UUID
4+
5+
data class BankAccount(val id: BankAccountId, val balance: Amount)
6+
7+
data class BankAccountId(val value: UUID) {
8+
9+
companion object {
10+
fun random() = BankAccountId(UUID.randomUUID())
11+
}
12+
}
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
package lmirabal.bank
2+
3+
import com.natpryce.hamkrest.assertion.assertThat
4+
import com.natpryce.hamkrest.equalTo
5+
import lmirabal.bank.data.InMemoryBankAccountRepository
6+
import lmirabal.bank.model.Amount
7+
import lmirabal.bank.model.BankAccount
8+
import lmirabal.bank.model.BankAccountId
9+
import org.junit.jupiter.api.Test
10+
11+
class BankServiceTest : BankTest() {
12+
private val id = BankAccountId.random()
13+
private val idFactory = { id }
14+
override val bank = BankService(InMemoryBankAccountRepository(), idFactory)
15+
16+
@Test
17+
fun createsAnAccount() {
18+
val account = bank.createAccount()
19+
20+
assertThat(account, equalTo(BankAccount(id, Amount.ZERO)))
21+
}
22+
}

0 commit comments

Comments
 (0)