import com.btc.mft.balanceenergy.infrastructure.service.DatabaseBalEnergyConfigQueryService import com.btc.mft.calculationprocedure.domain.service.BeDemandAssignmentService import com.btc.mft.calculationprocedure.domain.service.CalculationProcedureConfigQueryService import com.btc.mft.calculationprocedure.infrastructure.client.NetFlowDistributionSolverClient import com.btc.mft.calculationprocedure.infrastructure.client.TimeoutSolverApiBuilder import com.btc.mft.commons.domain.model.GasDay import com.btc.mft.commons.temporal.DateUtils import com.btc.mft.demandassessment.trigger.DemandAssessmentRestController import com.btc.mft.masterdata.domain.service.MasterDataConfigQueryService import com.btc.mft.node.domain.model.NodeId import com.btc.mft.shared.logging.domain.service.MftLogger import com.btc.mft.shared.state.ThreadUtils import com.btc.mft.system.domain.model.LockKey import com.btc.mft.system.domain.service.LockHolder import com.btc.mft.system.domain.service.LockingService import com.btc.mft.test.TestUtils import com.btc.mft.test.annotation.TimeBubble import com.btc.mft.timeseries.domain.service.TimeSeriesLockService import com.btc.mft.timeseries.domain.service.TimeSeriesWriteProtectionQueryService import com.btc.mft.zone.domain.model.GasQuality import com.btc.mft.zone.domain.model.Zone import com.hazelcast.cp.lock.FencedLock import com.the.mftsolver.rest.api.SolverApi import com.the.mftsolver.rest.model.ProblemDto import org.springframework.http.HttpStatus import org.springframework.http.ResponseEntity import spock.util.time.MutableClock
import java.time.ZonedDateTime
// Please install Intellij IDEA plugin 'Spock Framework Enhancements' and configure your preferred color for spock labels/blocks
// see documentation: https://spockframework.org/spock/docs/2.0/all_in_one.html#_spock_primer
// [optional] shows which classes are the target of this spec or with multiple @Subject(<clazz>)
@Subject
([Object, TimeSeriesLockService
])// [optional]
@Title("This is a more readable name for this demonstrative specification")
// [optional]
@Narrative("""
As a developer in this project
I want new developer to have an good introduction into spock
So they can write good specification early on
""")
// [optional] also on feature method possible
@Issue("https://oge.atlassian.net/browse/MFT-000")
// [optional] also on feature method possible
@See("https://oge.atlassian.net/wiki/spaces/MFT")
// enforce test execution in declaration order
@Stepwise
// all spock tests (specifications) must extends spock.lang.Specification
class DemoSpec
extends Specification
{ // (Spec = JUnit: Test)
// instance field - not shared between feature methods unless annotated with @Shared
@Shared
// fixture methods
// note for inheritance: supers setup is called first, subs cleanup is called first
// runs once - before the first feature method (JUnit 4/5: @BeforeClass/@BeforeAll, TestNG: @BeforeClass)
// runs before every feature method (JUnit 4/5: @Before/@BeforeEach, TestNG: @BeforeMethod)
assert obj
// explicit condition besides implicits in 'then' and 'expect' blocks }
// runs after every feature method (JUnit 4/5: @After/@AfterEach, TestNG: @AfterMethod)
// runs once - after the last feature method (JUnit 4/5: @AfterClass/@AfterAll, TestNG: @AfterClass)
// feature methods (JUnit: test method)
def "one plus one should equal two"() { expect: // 'stimulus + response phase': use 'expect' block if when-then block are more of a overhead
1 + 1 == 2 // a condition is a plain boolean expression
}
def "two plus two should equal four"() { given: // 'setup phase': use 'given' block to setup your feature method, should not be preceded by others blocks
// when-then pair always comes together
when: // 'stimulus phase': contains arbitrary code which is the test subject
int result
= left
+ right
then: // 'response phase': contains only conditions, exception conditions, interactions and variable defs as expectation
result == 4
// multiple when-then blocks can be present in a feature method
when:
int result2
= right
+ left
then:
result2 == 4
}
def "Should be able to remove from list"() { given:
when:
list.remove(0)
then:
list == [2, 3, 4] // list can be tested directly
}
def "Should get an IndexOutOfBoundsException when removing a non-existent item from array"() { given:
when:
list.remove(20)
then:
// alternative to use the exception instance:
//IndexOutOfBoundsException e = thrown()
}
def "Should not throw an IndexOutOfBoundsException when removing an existent item from array"() { given:
when:
list.remove(0)
then:
}
@Unroll("[#iterationIndex] #a divided by zero fails")
// use this to give iteration a different name
def "division through zero fails"() { when:
a / 0
then:
where:
a << [1, 2, 3, 4, 5] // this is a data pipe we use if multiple inputs expected to behave the same
// or as data table
// a | _
// 1 | _
// 2 | _
// 3 | _
// 4 | _
// 5 | _
}
//@Rollup // use this if you do not want each iteration logged separately
// parameter declaration is not necessary BUT it helps with IDE support
def "#a to the power of two equals #c"(int a,
int b,
int c
) { // by using a hashtag we can reference our variable expect:
where:
// this is a data table we use if we have multiple inputs and/or expected values for each iteration
// syntax: input | input || expected value
a | b || c
1 | 2 || 1
2 | 2 || 4
3 | 2 || 9
// in data tables you can access the own column vars, other data table vars, static fields and @Shared instance fields
}
// mocks can be used for mocking and stubbing, stubs only for stubbing. use stubs if mocking is not needed
def "A mock tracks its calls"() { given:
DatabaseBalEnergyConfigQueryService balEnergyConfigurationService = Mock() // mocks are useful to observer/check the code flow
when:
balEnergyConfigurationService.getMessageMaximumAgeInMinutes()
balEnergyConfigurationService.getCapacityQuantitiesFollowUpTime()
balEnergyConfigurationService.getCapacityQuantitiesFollowUpTime()
balEnergyConfigurationService.getCapacityQuantitiesFollowUpTime()
then:
1 * balEnergyConfigurationService.getMessageMaximumAgeInMinutes() // only one call
(2.._) * balEnergyConfigurationService.getCapacityQuantitiesFollowUpTime() // at least two calls (_ is any)
}
def "[lenient mocking] mocks will return defaults (false, 0 or null)"() { given:
def balEnergyConfigurationService
= Mock
(DatabaseBalEnergyConfigQueryService
) MasterDataConfigQueryService masterDataConfigQueryService = Mock()
BeDemandAssignmentService beDemandAssignmentService = Mock() // preferred style since it may give better IDE support
TimeSeriesWriteProtectionQueryService timeSeriesWriteProtectionQueryService = Mock()
when:
def longDefault
= balEnergyConfigurationService.
getNodeStatusChangedAtThreshold() //def LongDefault = balEnergyConfigurationService.getCapacityQuantitiesFollowUpTime()
def customClassDefault
= masterDataConfigQueryService.
getHgasNorthZoneId() def booleanDefault
= beDemandAssignmentService.
ignoreLocalZonesNeededForAssignments(Mock
(Zone
)) def optionalDefault
= beDemandAssignmentService.
getAssignedZoneId(GasQuality.
H_GAS, ZonedDateTime.
now(), ZonedDateTime.
now()) def listDefault
= timeSeriesWriteProtectionQueryService.
findAll(Mock
(NodeId
), Mock
(GasDay
))
// more then around five conditions => check if you test different features you may should split in smaller feature methods
then:
longDefault == 0
//LongDefault == null
customClassDefault
== null !booleanDefault
}
def "[lenient mocking] stubs will return sensible defaults"() { given:
DatabaseBalEnergyConfigQueryService balEnergyConfigurationService
= Stub() MasterDataConfigQueryService masterDataConfigQueryService
= Stub() BeDemandAssignmentService beDemandAssignmentService
= Stub() TimeSeriesWriteProtectionQueryService timeSeriesWriteProtectionQueryService
= Stub()
when:
def longDefault
= balEnergyConfigurationService.
getNodeStatusChangedAtThreshold() def LongDefault
= balEnergyConfigurationService.
getCapacityQuantitiesFollowUpTime() def customClassDefault
= masterDataConfigQueryService.
getHgasNorthZoneId() def booleanDefault
= beDemandAssignmentService.
ignoreLocalZonesNeededForAssignments(Mock
(Zone
)) def optionalDefault
= beDemandAssignmentService.
getAssignedZoneId(GasQuality.
H_GAS, ZonedDateTime.
now(), ZonedDateTime.
now()) def listDefault
= timeSeriesWriteProtectionQueryService.
findAll(Mock
(NodeId
), Mock
(GasDay
))
then:
longDefault == 0
LongDefault == 0
customClassDefault
customClassDefault.getValue() == 0
!booleanDefault
optionalDefault.isEmpty()
}
def "Stub will return what we want (fixed)"() { given: "a procedure id" // we can label any block to provide additional information
and: "a stubbed rest controller" // using labels and 'and' is preferred
DemandAssessmentRestController controller
= Stub() controller.verifyCalculationProcedureSubstituteValues(procedureId, []) >> ResponseEntity.accepted().build()
expect: "calling the stubbed method behave as expected"
controller.verifyCalculationProcedureSubstituteValues(procedureId, []) == ResponseEntity.accepted().build()
}
def "Stub will return what we want (sequence)"() { given: "a procedure id"
and: "a stubbed rest controller returning always notFound except on the first call"
DemandAssessmentRestController controller
= Stub() controller.verifyCalculationProcedureSubstituteValues(procedureId, []) >>> [
ResponseEntity.accepted().build(),
ResponseEntity.notFound().build()
]
when: "we call it three times"
def callOne
= controller.
verifyCalculationProcedureSubstituteValues(procedureId,
[]) def callTwo
= controller.
verifyCalculationProcedureSubstituteValues(procedureId,
[]) def callThree
= controller.
verifyCalculationProcedureSubstituteValues(procedureId,
[])
then: "the first response is accepted"
callOne == ResponseEntity.accepted().build()
and: "the next responses are notFound"
callTwo == ResponseEntity.notFound().build()
callThree == ResponseEntity.notFound().build()
}
/*
more one stubbing (in short)
details: https://spockframework.org/spock/docs/2.0/all_in_one.html#_stubbing
computing result:
subscriber.receive(_) >> { args -> args[0].size() > 3 ? "ok" : "fail" }
or
subscriber.receive(_) >> { String message -> message.size() > 3 ? "ok" : "fail" }
side effects: (can contain more abitrary code)
subscriber.receive(_) >> { throw new InternalError("ouch") }
chaining resulst:
subscriber.receive(_) >>> ["ok", "fail", "ok"] >> { throw new InternalError() } >> "ok"
default non-null result:
subscriber.receive(_) >> _
combining mocking and stubbing: (must be in one statement)
1 * subscriber.receive("message1") >> "ok"
*/
def "Using a helper method works"() { given: "a stubbed lockingService always returning a ignored lock with a given key"
LockingService lockingService
= Stub() lockingService.
lock(_
as LockKey, _
as boolean) >> { LockKey key
-> lock.getName() >> key.getKey()
return LockHolder.
forIgnoredLock(Mock
(MftLogger
), lock
) }
and: "a real timeSeriesLockService"
@Subject TimeSeriesLockService lockService
= new TimeSeriesLockService
(lockingService
)
when: "we call to create a lock for distrivution"
LockHolder lock = lockService.lockDistribution(GasDay.now())
then: "this lock contains the expected key"
checkKeyExists(lock)
// or use 'with' to check multiple properties of an object, which has the same behavior like a helper methods
with(lock) {
}
// with you want to check all assertions once use verifyAll
verifyAll(lock) {
}
}
// in this case we must use the assert keyword, also test fails on first failed assertion
}
def "working with a mutable clock works"() { given: "a clock to play with"
MutableClock clock
= new MutableClock
(DateUtils.
from(2021,
7,
9,
0,
0,
0))
and: "we put the clock into the application"
TestUtils.startTimeBubble(clock)
and: "we remember a birthday"
ZonedDateTime birthday = DateUtils.from(2021, 7, 10, 0, 0, 0)
expect: "its not the birthday day and threads execution time is now"
DateUtils.now() != birthday
ThreadUtils.executionTimestamp() == DateUtils.now()
when: "we tell the clock to be a day later"
clock + Duration.ofDays(1)
then: "it's birthday time but thread execution time is kept to now"
DateUtils.now() == birthday
ThreadUtils.executionTimestamp() != birthday
cleanup: "is important after playing with the application clock"
TestUtils.stopTimeBubble()
}
// @TimeBubble is also allowed on spec (class) level
@TimeBubble(year = 2021, month = 4, day = 4, hour = 5, minutes = 11, seconds = 0)
def "testing in a time bubble by annotation works"() { expect:
DateUtils.now() == DateUtils.from(2021, 4, 4, 5, 11, 0)
}
def "testing in a time bubble by util works"() { given: "a timeout is configured"
CalculationProcedureConfigQueryService calculationProcedureConfigurationService
= Stub() calculationProcedureConfigurationService.getSolverTimeoutInSeconds() >> timeoutInSeconds
and: "a solverApi mock is provided"
SolverApi solverApi = Mock()
def timeoutSolverApiBuilder
= Stub(TimeoutSolverApiBuilder
) timeoutSolverApiBuilder.
build(timeoutInSeconds, _
as List
<HttpStatus
>) >> solverApi
and: "our test subject: solver client"
NetFlowDistributionSolverClient uut
= new NetFlowDistributionSolverClient
( calculationProcedureConfigurationService,
timeoutSolverApiBuilder
)
when: "solver client internal method is called"
// we can use 'doInTimeBubble' instead of manual set start and end of fixed time,
// if we do not have checks in 'then' block which require the fixed time
TestUtils.doInTimeBubble(fixedTime, time -> {
uut.callSolverWithTimeout(Mock(ProblemDto), timeoutInSeconds)
})
then: "solver api is called with correct timeout"
1 * solverApi.
solveWithHttpInfo(_
as ProblemDto, expectedTimeoutTime
)
where: "different starting time are tested"
timeoutInSeconds | fixedTime || expectedTimeoutTime
0L
| DateUtils.
from(2021,
4,
4,
5,
11,
0) || null 100L | DateUtils.from(2021, 4, 4, 5, 11, 0) || DateUtils.from(2021, 4, 4, 5, 12, 40).toOffsetDateTime().toString()
100L | DateUtils.from(2021, 12, 31, 23, 59, 0) || fixedTime.plusSeconds(timeoutInSeconds).toOffsetDateTime().toString()
}
/*
Use @Retry(<retryCount>) if you have a weird test case that sometimes fails due to non-deterministic integration test.
<rertyCount> default is: 3
Use @Timeout(<threshold>) to stop an iteration which may can take to long
*/
}