The sweetest fruit salad recipe for energy saving services
Chapter 7. Human-friendly software lifecycle
(This story begins in Chapter 0. Launching an energy-saving rocket to the stars)
Our goal is to apply the demonstrated benefits of Behaviour Driven Development (BDD) to the selected technical stack of this component.
Our user story, besides the context and feature description, contains the Acceptance Criteria. Let’s focus on this, as it will be used during most part of the feature lifecycle: from the user story definition until the feature’s quality assurance.
Given a user story which Acceptance Criterion is written in human language, like this example:
A Cucumber test is directly created from the user story’s acceptance criteria. Yes, we are talking about copy & paste, resulting in the following test, located at test/resources/cucumber.features/DatasourcesLookup.feature:
Feature: Datasources lookup DEX-2685Scenario: 1. Get datasource when not cachedGiven a device deviceId which datasource is not cached
And the device is assigned to a Datasource
When the lookup is called with device deviceId
Then the datasources API is called 1 times and datasource is returned with id ‘datasourceId’
And the datasource ‘datasourceId’ is added to the cache
Notice that we chose to mention the user story or epic at feature level to strengthen the relationship between code and product documentation. It will be useful in the future, when coming developers have to change this feature, so they will find in the code a direct reference to the original feature description, scope and background.
Now let’s make it work. First, we need to configure Cucumber, by adding its dependencies, in our case in maven’s pom.xml:
then, creating the file src/test/kotlin/CucumberTest.kt:
import io.cucumber.junit.Cucumber
import io.cucumber.junit.CucumberOptions
import org.junit.runner.RunWith@RunWith(Cucumber::class)
@CucumberOptions(
features = [“src/test/resources/cucumber/features”],
plugin = [“pretty”, “html:target/test/features”],
glue = [“com.dexma”],
tags = [“not @ignored”])
class CucumberTest
Now it’s time to implement each of the sentences our Product/Business Owner declared. We create the following definition steps at/src/test/kotlin/com/dexma/infrastructure/DatasourcesRepositoryStepDef.kt
import ...class DatasourcesRepositoryStepDef : En {
private val mockWebClient: WebClient = mockk(relaxed = true)
private val mockRequestHeadersUriSpec: WebClient.RequestHeadersUriSpec<*> = mockk(relaxed = true)
private val mockRequestHeadersSpec: WebClient.RequestHeadersSpec<*> = mockk()
private val mockResponseSpec: WebClient.ResponseSpec = mockk(relaxed = true)
private val mockCache: Cache<String, SimpleDataSource> = mockk(relaxed = true)private lateinit var datasourcesRepository: DatasourcesAPIRepository
private lateinit var lookupResponse: Mono<SimpleDataSource>@Before
fun setUp() {
initDatasourcesRepository()
every { mockWebClient.get()} returns mockRequestHeadersUriSpec
every { mockRequestHeadersUriSpec.uri(any<URI>())} returns mockRequestHeadersSpec
every { mockRequestHeadersSpec.header(any(), any())} returns mockRequestHeadersSpec
every { mockRequestHeadersSpec.accept(any())} returns mockRequestHeadersSpec
every { mockRequestHeadersSpec.retrieve() } returns mockResponseSpec
}init { Given("a device {word} which datasource is not cached") { deviceId: String ->
every { mockCache.getIfPresent(deviceId) } returns null
} And("the device is assigned to a Datasource") {
every { mockResponseSpec.bodyToMono(Array<DataSource>::class.java) } returns Mono.just(arrayOf(buildDatasource()))
} When("the lookup is called with device {word}") { deviceId: String ->
lookupResponse = datasourcesRepository.lookup(deviceId)
}Then("the datasources API is called {int} times and datasource is returned with mac {word}") { times: Int, datasourceName: String ->
lookupResponse.subscribe {dataSource: SimpleDataSource ->
verify(exactly = times) { mockWebClient.get() }
assertNotNull(dataSource, "Datasource should have been retrieved from the API")
assertNotNull(dataSource.mac, "Datasource should have been retrieved from the API")
assertEquals(datasourceName, dataSource.mac, "Wrong Datasource retrieved")
}
} And("the datasource 'datasourceId' is added to the cache") {
lookupResponse.subscribe {
verify(exactly = 1) { mockCache.put(any(), any()) }
}}
Notice that we are mocking the collaborators (both the WebClient and the Cache) and uniquely testing the repository layer. There are different approaches on how to define unit tests, for simplicity’s sake we used a simple unitary test that completely mocks its next layer (collaborators).
You can then run the test, by just typing:
mvn clean test
Or from your IDE, for instance to Run/Debug with IntelliJ, just Run/Debug it from the main cucumber test class src/test/kotlin/CucumberTest.kt
And you can now show to your product owner (or another reporter) how the code now responds as they requested in the use story’s acceptance criteria.
Our trip continues in Chapter 8: Discoverable/Testable