Hexagonal Testing

Hexagonal Architecture Concepts

Domain

<Data>
Demand

Domain

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

Application

Does orchestration

Domain

<Service>
DemandService

<Data>
Demand

Application

Application

Does orchestration

Knows the domain

Framework-specific
@Service, @Component, @Configuration

Domain

<Service>
DemandService

<Data>
Demand

Application

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

Application

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

Driving Adapters

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

Driven Adapters

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

Hexagonal Unit Tests

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

Hexagonal Integration Tests

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

Hexagonal Testing

Domain

Application

Domain

Very well and easily covered by simple Unit Tests

Application

Testing port-to-port

Requires stubbing

Test data builders

Still fast and simple unit tests

Adapters

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