Michael Kutz
Quality Engineer at REWE digital, Conference Speaker about QA & Agile, Founder of Agile QA Cologne meetup, Freelance QA Consultant
Domain
<Data>
Demand
Not an original Hexagonal concept
Just POJOs
No dependencies (but utils)
No framework-specific code
Knows only itself,
but is known to all
Domain
<Data>
Demand
Application
Does orchestration
Domain
<Service>
DemandService
<Data>
Demand
Application
Does orchestration
Knows the domain
Framework-specific
@Service, @Component, @Configuration
Domain
<Service>
DemandService
<Data>
Demand
Application
Does orchestration
Knows the domain
Framework-specific
@Service, @Component, @Configuration
Implementation of in ports
Knows nothing about Kafka Consumers, HTTP-APIs, etc.
<InPort>
ToImportDemands
Does orchestration
Knows the domain
Framework-specific
@Service, @Component, @Configuration
Implementation of in ports
Knows nothing about Kafka Consumers, HTTP-APIs, etc.
Uses out ports
Knows nothing about Postgres, Kafka Producers, etc.
Domain
<Service>
DemandService
<Data>
Demand
<InPort>
ToImportDemands
<OutPort>
ToStoreDemands
Application
Configures/implements provided interfaces, e.g. Kafka
May use the domain,
might have its own data classes
Uses in ports
Domain
<Service>
DemandService
<Data>
Demand
<InPort>
ToImportDemands
<OutPort>
ToStoreDemands
Application
Demand
Consumer
<Data>
Demand
Message
<Data>
Demand
Message
Configures/implements outer dependencies, e.g. Postgres
May use the domain,
might have its own data classes
Implements out ports
Domain
<Service>
DemandService
<Data>
Demand
<InPort>
ToImportDemands
<OutPort>
ToStoreDemands
Application
Demand
Consumer
<Data>
Demand
Message
Demand
Store
Demand
Repo.
Demand
Entity
<Data>
Demand
Message
Domain
<Service>
DemandService
<Data>
Demand
<InPort>
ToImportDemands
<OutPort>
ToStoreDemands
Application
Demand
Consumer
<Data>
Demand
Message
Demand
Store
Demand
Repo.
Demand
Entity
Fronten Service
<Data>
Demand
Message
Application
Driving
Adapter
Driven
Adapter
Fronten Service
<Service>
Service
<InPort>
ToImport
<OutPort>
ToStore
Consumer
Store
<Data>
Entity
Repository
<Data>
Message
Controller
<Data>
Dto
<InPort>
ToQuery
KafkaProducer
<Data>
Message
<OutPort>
ToPublish
<Service>
Service
Domain
<Service>
DemandService
<Data>
Demand
<InPort>
ToImportDemands
<OutPort>
ToStoreDemands
Application
Demand
Consumer
<Data>
Demand
Message
Demand
Store
Demand
Repo.
Demand
Entity
<Data>
Demand
Message
<Data>
Demand
Domain
<Service>
DemandService
<Data>
Demand
<InPort>
ToImportDemands
<OutPort>
ToStoreDemands
Application
Demand
Consumer
<Data>
Demand
Message
Demand
Store
Demand
Repo.
Demand
Entity
<Data>
Demand
Message
<Service>
DemandService
<Data>
Demand
<InPort>
ToImportDemands
<OutPort>
ToStoreDemands
Demand
Consumer
<Data>
Demand
Message
Demand
Store
Demand
Repo.
Demand
Entity
<Data>
Demand
Message
<Service>
DemandService
<Data>
Demand
<InPort>
ToImportDemands
<OutPort>
ToStoreDemands
Demand
Store
Demand
Repo.
Demand
Entity
Demand
Repo.Stub
<Service>
DemandService
<Data>
Demand
<InPort>
ToImportDemands
<OutPort>
ToStoreDemands
Demand
Store
class DemandRepositoryStub :
CrudRepositoryStub<DemandEntity, String>(),
DemandRepository {
fun findByOrderId(orderId: String) =
data.values.find {
entity -> demand.orderId == orderId
}
}
Demand
Repo.Stub
<Service>
DemandService
<Data>
Demand
<InPort>
ToImportDemands
<OutPort>
ToStoreDemands
Demand
Store
Demand
Repo.Stub
<Service>
DemandService
<Data>
Demand
<InPort>
ToImportDemands
<OutPort>
ToStoreDemands
Demand
Store
class DemandImporterTest {
private val demandStore : ToStoreDemands =
DemandStore(DemandRepositoryStub())
private val demandImporter : ToImportDemands =
DemandService(demandStore)
}
Demand
Repo.Stub
<Service>
DemandService
<Data>
Demand
<InPort>
ToImportDemands
<OutPort>
ToStoreDemands
Demand
Store
class DemandImporterTest {
private val demandStore : ToStoreDemands =
DemandStore(DemandRepositoryStub())
private val demandImporter : ToImportDemands =
DemandService(demandStore)
fun `importDemand`() {
val result = demandImporter.save(aDemand().build())
assertThat(result.isSuccessful()).isTrue()
assertThat(demandStore.findById(preStoredDemand.id))
.isNotNull()
}
}
Demand
Repo.Stub
<Service>
DemandService
<Data>
Demand
<OutPort>
ToStoreDemands
Demand
Store
<InPort>
ToImportDemands
class DemandImporterTest {
private val demandStore : ToStoreDemands =
DemandStore(DemandRepositoryStub())
private val demandImporter : ToImportDemands =
DemandService(demandStore)
fun `importDemand`() {
val result = demandImporter.save(aDemand().build())
assertThat(result.isSuccessful()).isTrue()
assertThat(demandStore.findById(preStoredDemand.id))
.isNotNull()
}
fun `importDemand conflict`() {
val preStoredDemand = aDemand().build()
demandStore.save(preStoredDemand)
val result = demandImporter.save(preStoredDemand)
assertThat(result.isConflict()).isTrue()
}
}
Demand
Repo.Stub
<Service>
DemandService
<Data>
Demand
<OutPort>
ToStoreDemands
Demand
Store
<InPort>
ToImportDemands
class DemandImporterTest {
private val demandStore : ToStoreDemands =
DemandStore(DemandRepositoryStub())
private val demandImporter : ToImportDemands =
DemandService(demandStore)
fun `importDemand`() {
val result = demandImporter.save(aDemand().build())
assertThat(result.isSuccessful()).isTrue()
assertThat(demandStore.findById(preStoredDemand.id))
.isNotNull()
}
fun `importDemand conflict`() {
val preStoredDemand = aDemand().build()
demandStore.save(preStoredDemand)
val result = demandImporter.save(preStoredDemand)
assertThat(result.isConflict()).isTrue()
}
fun `deleteDemand`() {
val preStoredDemand = aDemand().build()
demandStore.save(preStoredDemand))
val result = demandImporter.delete(preStoredDemand)
assertThat(result.isSuccessful()).isTrue()
assertThat(demandStore.findById(preStoredDemand.id))
.isNull()
}
}
Demand
Repo.Stub
<Service>
DemandService
<Data>
Demand
<OutPort>
ToStoreDemands
Demand
Store
<InPort>
ToImportDemands
class DemandImporterTest {
private val demandStore : ToStoreDemands =
DemandStore(DemandRepositoryStub())
private val demandImporter : ToImportDemands =
DemandService(demandStore)
fun `importDemand`() {
val result = demandImporter.save(aDemand().build())
assertThat(result.isSuccessful()).isTrue()
assertThat(demandStore.findById(preStoredDemand.id))
.isNotNull()
}
fun `importDemand conflict`() {
val preStoredDemand = aDemand().build()
demandStore.save(preStoredDemand)
val result = demandImporter.save(preStoredDemand)
assertThat(result.isConflict()).isTrue()
}
fun `deleteDemand`() {
val preStoredDemand = aDemand().build()
demandStore.save(preStoredDemand))
val result = demandImporter.delete(preStoredDemand)
assertThat(result.isSuccessful()).isTrue()
assertThat(demandStore.findById(preStoredDemand.id))
.isNull()
}
}
Demand
Repo.Stub
<Service>
DemandService
<Data>
Demand
<OutPort>
ToStoreDemands
Demand
Store
<InPort>
ToImportDemands
class DemandImporterTest {
private val demandStore : ToStoreDemands =
DemandStore(DemandRepositoryStub())
private val demandImporter : ToImportDemands =
DemandService(demandStore)
fun `importDemand`() {
val result = demandImporter.save(aDemand().build())
assertThat(result.isSuccessful()).isTrue()
assertThat(demandStore.findById(preStoredDemand.id))
.isNotNull()
}
fun `importDemand conflict`() {
val preStoredDemand = aDemand().build()
demandStore.save(preStoredDemand)
val result = demandImporter.save(preStoredDemand)
assertThat(result.isConflict()).isTrue()
}
fun `deleteDemand`() {
val preStoredDemand = aDemand().build()
demandStore.save(preStoredDemand))
val result = demandImporter.delete(preStoredDemand)
assertThat(result.isSuccessful()).isTrue()
assertThat(demandStore.findById(preStoredDemand.id))
.isNull()
}
}
class DemandBuilder private constructor {
private var id: String = UUID.randomUUID().toString()
private var version: Long = 1
private var referenceId: String = randomReferenceId()
// …
companion object {
fun aDemand() = DemandBuilder()
}
fun id(id: String) = apply { this.id = id }
fun version(version: Long) = apply { this.version = version }
fun referenceId(referenceId: String) = apply {
this.referenceId = referenceId }
// …
fun build() = Demand(
id = id,
version = version,
referenceId = referenceId,
// …
)
}
Demand
Repo.Stub
<Service>
DemandService
<Data>
Demand
<OutPort>
ToStoreDemands
Demand
Store
<InPort>
ToImportDemands
Domain
<Service>
DemandService
<Data>
Demand
<InPort>
ToImportDemands
<OutPort>
ToStoreDemands
Application
Demand
Consumer
<Data>
Demand
Message
Demand
Store
Demand
Repo.
Demand
Entity
<Data>
Demand
Message
<InPort>
ToImportDemands
<OutPort>
ToStoreDemands
Demand
Consumer
<Data>
Demand
Message
Demand
Store
Demand
Repo.
Demand
Entity
<Data>
Demand
Message
Demand
Entity
<Data>
Demand
Message
<InPort>
ToImportDemands
<OutPort>
ToStoreDemands
Demand
Consumer
<Data>
Demand
Message
Demand
Store
Demand
Repo.
Demand
Entity
<Data>
Demand
Message
Domain
<Service>
DemandService
<Data>
Demand
<InPort>
ToImportDemands
<OutPort>
ToStoreDemands
Application
Demand
Consumer
<Data>
Demand
Message
Demand
Store
Demand
Repo.
Demand
Entity
<Data>
Demand
Message
Domain
<Service>
DemandService
<Data>
Demand
<InPort>
ToImportDemands
<OutPort>
ToStoreDemands
Application
Demand
Consumer
<Data>
Demand
Message
Demand
Store
Demand
Repo.
Demand
Entity
<Data>
Demand
Message
Domain
<Service>
DemandService
<Data>
Demand
<OutPort>
ToStoreDemands
Application
Demand
Consumer
<Data>
Demand
Message
Demand
Store
Demand
Repo.
Demand
Entity
<Data>
Demand
Message
@SpringBootTest
@EmbeddedKafka
class DemandCosumptionTest() : AbstractServiceTest {
}
<InPort>
ToImportDemands
Domain
<Service>
DemandService
<Data>
Demand
<OutPort>
ToStoreDemands
Application
Demand
Consumer
<Data>
Demand
Message
Demand
Store
Demand
Repo.
Demand
Entity
<Data>
Demand
Message
@SpringBootTest
@EmbeddedKafka
class DemandCosumptionTest(
@Autowired private val demandProducer: DemandProducer,
) : AbstractServiceTest {
}
<InPort>
ToImportDemands
@Component
class DemandProducer(
@Autowired kafkaTemplate: KafkaTemplate<String, String>,
@Value("\${kafka.topics.demand.name}") topicName: String
) {
fun send() {
val result: CompletableFuture<SendResult<String, String>> =
kafkaTemplate.send(topic, key, data)
kafkaTemplate.flush()
result.get(10, SECONDS)
}
}
Domain
<Service>
DemandService
<Data>
Demand
<OutPort>
ToStoreDemands
Application
Demand
Consumer
<Data>
Demand
Message
Demand
Store
Demand
Repo.
Demand
Entity
<Data>
Demand
Message
@SpringBootTest
@EmbeddedKafka
class DemandCosumptionTest(
@Autowired private val demandProducer: DemandProducer,
@Autowired private val demandStore: ToStoreDemands
) : AbstractServiceTest {
}
<InPort>
ToImportDemands
Domain
<Service>
DemandService
<Data>
Demand
<OutPort>
ToStoreDemands
Application
Demand
Consumer
<Data>
Demand
Message
Demand
Store
Demand
Repo.
Demand
Entity
<Data>
Demand
Message
@SpringBootTest
@EmbeddedKafka
class DemandCosumptionTest(
@Autowired private val demandProducer: DemandProducer,
@Autowired private val demandStore: ToStoreDemands
) : AbstractServiceTest {
fun `consume demand`() {
val demandMessage = aDemandMessage().build()
demandProducer.send(demandMessage)
await().untilAsserted {
assertThat(demandStore.findById(demandMessage.id))
.isNotNull()
}
}
}
<InPort>
ToImportDemands
Domain
<Service>
DemandService
<Data>
Demand
<OutPort>
ToStoreDemands
Application
Demand
Consumer
<Data>
Demand
Message
Demand
Store
Demand
Repo.
Demand
Entity
<Data>
Demand
Message
@SpringBootTest
@EmbeddedKafka
class DemandCosumptionTest(
@Autowired private val demandProducer: DemandProducer,
@Autowired private val demandStore: ToStoreDemands
) : AbstractServiceTest {
fun `consume demand`() {
val demandMessage = aDemandMessage().build()
demandProducer.send(demandMessage)
await().untilAsserted {
assertThat(demandStore.findById(demandMessage.id))
.isNotNull()
}
}
fun `consume tombstone`() {
val preStoredDemand = aDemand().build()
demandStore.save(preStoredDemand)
demandProducer.sendTombstone(preStoredDemand.id)
await().untilAsserted {
assertThat(demandStore.findById(demandMessage.id))
.isNull()
}
}
}
<InPort>
ToImportDemands
Domain
<Service>
DemandService
<Data>
Demand
<InPort>
ToImportDemands
<OutPort>
ToStoreDemands
Application
Demand
Consumer
<Data>
Demand
Message
Demand
Store
Demand
Repo.
Demand
Entity
<Data>
Demand
Message
Domain
Application
Very well and easily covered by simple Unit Tests
Testing port-to-port
Requires stubbing
Test data builders
Still fast and simple unit tests
Mostly about infrastructure
Unit Tests rather pointless
Focussed Integration Tests or Service Tests necessary
@SpringBootTest
Testcontainers
ports
application
adapters
SpringBootApplication
driven
driving
domain
driven
driving
By Michael Kutz
Are you tired of brittle test suites, that keep breaking for no good reasons at all? Does maintaining your test suite feels like doubling your efforts on a service? Do you avoid big refactoring because you're afraid of your own tests? Well, I've been there… In this talk I demonstrate a concept of how to achieve a great test coverage on hexagonal services written in Spring Boot with minimal test code, no mocks and great expression of intent. Never done a hexagonal service? No problem! I'll explain all core concepts in the first part of the talk to make sure we're all on the same page. After hearing this, you should have a good idea of how to refactor your tests and know some new helpful libraries and design patterns for tests.
Quality Engineer at REWE digital, Conference Speaker about QA & Agile, Founder of Agile QA Cologne meetup, Freelance QA Consultant