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.lang.* import spock.util.time.MutableClock import java.time.Duration 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() @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 def obj = new Object() // 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) def setupSpec() {} // runs before every feature method (JUnit 4/5: @Before/@BeforeEach, TestNG: @BeforeMethod) def setup() { assert obj // explicit condition besides implicits in 'then' and 'expect' blocks } // runs after every feature method (JUnit 4/5: @After/@AfterEach, TestNG: @AfterMethod) def cleanup() {} // runs once - after the last feature method (JUnit 4/5: @AfterClass/@AfterAll, TestNG: @AfterClass) def cleanupSpec() {} // 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 int left = 2 int right = 2 // 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: def list = [1, 2, 3, 4] 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: def list = [1, 2, 3, 4] when: list.remove(20) then: thrown(IndexOutOfBoundsException) // exception condition: other conditions and when-then blocks can follow that // alternative to use the exception instance: //IndexOutOfBoundsException e = thrown() list.size() == 4 } def "Should not throw an IndexOutOfBoundsException when removing an existent item from array"() { given: def list = [1, 2, 3, 4] when: list.remove(0) then: notThrown(IndexOutOfBoundsException) // exception condition: feature method will fail if any other exception is thrown list.size() == 3 } @Unroll("[#iterationIndex] #a divided by zero fails") // use this to give iteration a different name def "division through zero fails"() { when: a / 0 then: thrown(ArithmeticException) 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: Math.pow(a, b) == c 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 optionalDefault == null listDefault == null } 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() listDefault.size() == 0 } def "Stub will return what we want (fixed)"() { given: "a procedure id" // we can label any block to provide additional information Long procedureId = 0L 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" Long procedureId = 0L 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 -> FencedLock lock = Stub() 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) { keyString.size() >= 0 metaClass != null } // with you want to check all assertions once use verifyAll verifyAll(lock) { keyString.size() >= 0 metaClass != null } } // in this case we must use the assert keyword, also test fails on first failed assertion private static void checkKeyExists(LockHolder lock) { assert lock assert lock.getKeyString().size() >= 0 assert lock.metaClass != null } 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) >> solverApi and: "our test subject: solver client" NetFlowDistributionSolverClient uut = new NetFlowDistributionSolverClient( Stub(MftLogger), 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() if you have a weird test case that sometimes fails due to non-deterministic integration test. default is: 3 Use @Timeout() to stop an iteration which may can take to long */ }