1. import com.btc.mft.balanceenergy.infrastructure.service.DatabaseBalEnergyConfigQueryService
  2. import com.btc.mft.calculationprocedure.domain.service.BeDemandAssignmentService
  3. import com.btc.mft.calculationprocedure.domain.service.CalculationProcedureConfigQueryService
  4. import com.btc.mft.calculationprocedure.infrastructure.client.NetFlowDistributionSolverClient
  5. import com.btc.mft.calculationprocedure.infrastructure.client.TimeoutSolverApiBuilder
  6. import com.btc.mft.commons.domain.model.GasDay
  7. import com.btc.mft.commons.temporal.DateUtils
  8. import com.btc.mft.demandassessment.trigger.DemandAssessmentRestController
  9. import com.btc.mft.masterdata.domain.service.MasterDataConfigQueryService
  10. import com.btc.mft.node.domain.model.NodeId
  11. import com.btc.mft.shared.logging.domain.service.MftLogger
  12. import com.btc.mft.shared.state.ThreadUtils
  13. import com.btc.mft.system.domain.model.LockKey
  14. import com.btc.mft.system.domain.service.LockHolder
  15. import com.btc.mft.system.domain.service.LockingService
  16. import com.btc.mft.test.TestUtils
  17. import com.btc.mft.test.annotation.TimeBubble
  18. import com.btc.mft.timeseries.domain.service.TimeSeriesLockService
  19. import com.btc.mft.timeseries.domain.service.TimeSeriesWriteProtectionQueryService
  20. import com.btc.mft.zone.domain.model.GasQuality
  21. import com.btc.mft.zone.domain.model.Zone
  22. import com.hazelcast.cp.lock.FencedLock
  23. import com.the.mftsolver.rest.api.SolverApi
  24. import com.the.mftsolver.rest.model.ProblemDto
  25. import org.springframework.http.HttpStatus
  26. import org.springframework.http.ResponseEntity
  27. import spock.lang.*
  28. import spock.util.time.MutableClock
  29.  
  30. import java.time.Duration
  31. import java.time.ZonedDateTime
  32.  
  33. // Please install Intellij IDEA plugin 'Spock Framework Enhancements' and configure your preferred color for spock labels/blocks
  34.  
  35. // see documentation: https://spockframework.org/spock/docs/2.0/all_in_one.html#_spock_primer
  36.  
  37. // [optional] shows which classes are the target of this spec or with multiple @Subject(<clazz>)
  38. @Subject([Object, TimeSeriesLockService])
  39. // [optional]
  40. @Title("This is a more readable name for this demonstrative specification")
  41. // [optional]
  42. @Narrative("""
  43. As a developer in this project
  44. I want new developer to have an good introduction into spock
  45. So they can write good specification early on
  46. """)
  47. // [optional] also on feature method possible
  48. @Issue("https://oge.atlassian.net/browse/MFT-000")
  49. // [optional] also on feature method possible
  50. @See("https://oge.atlassian.net/wiki/spaces/MFT")
  51. // enforce test execution in declaration order
  52. @Stepwise
  53. // all spock tests (specifications) must extends spock.lang.Specification
  54. class DemoSpec extends Specification { // (Spec = JUnit: Test)
  55.  
  56. // instance field - not shared between feature methods unless annotated with @Shared
  57. @Shared
  58. def obj = new Object()
  59.  
  60. // fixture methods
  61. // note for inheritance: supers setup is called first, subs cleanup is called first
  62.  
  63. // runs once - before the first feature method (JUnit 4/5: @BeforeClass/@BeforeAll, TestNG: @BeforeClass)
  64. def setupSpec() {}
  65.  
  66. // runs before every feature method (JUnit 4/5: @Before/@BeforeEach, TestNG: @BeforeMethod)
  67. def setup() {
  68. assert obj // explicit condition besides implicits in 'then' and 'expect' blocks
  69. }
  70.  
  71. // runs after every feature method (JUnit 4/5: @After/@AfterEach, TestNG: @AfterMethod)
  72. def cleanup() {}
  73.  
  74. // runs once - after the last feature method (JUnit 4/5: @AfterClass/@AfterAll, TestNG: @AfterClass)
  75. def cleanupSpec() {}
  76.  
  77.  
  78. // feature methods (JUnit: test method)
  79.  
  80. def "one plus one should equal two"() {
  81. expect: // 'stimulus + response phase': use 'expect' block if when-then block are more of a overhead
  82. 1 + 1 == 2 // a condition is a plain boolean expression
  83. }
  84.  
  85.  
  86. def "two plus two should equal four"() {
  87. given: // 'setup phase': use 'given' block to setup your feature method, should not be preceded by others blocks
  88. int left = 2
  89. int right = 2
  90.  
  91. // when-then pair always comes together
  92. when: // 'stimulus phase': contains arbitrary code which is the test subject
  93. int result = left + right
  94. then: // 'response phase': contains only conditions, exception conditions, interactions and variable defs as expectation
  95. result == 4
  96.  
  97. // multiple when-then blocks can be present in a feature method
  98. when:
  99. int result2 = right + left
  100. then:
  101. result2 == 4
  102. }
  103.  
  104. def "Should be able to remove from list"() {
  105. given:
  106. def list = [1, 2, 3, 4]
  107.  
  108. when:
  109. list.remove(0)
  110.  
  111. then:
  112. list == [2, 3, 4] // list can be tested directly
  113. }
  114.  
  115. def "Should get an IndexOutOfBoundsException when removing a non-existent item from array"() {
  116. given:
  117. def list = [1, 2, 3, 4]
  118.  
  119. when:
  120. list.remove(20)
  121.  
  122. then:
  123. thrown(IndexOutOfBoundsException) // exception condition: other conditions and when-then blocks can follow that
  124. // alternative to use the exception instance:
  125. //IndexOutOfBoundsException e = thrown()
  126. list.size() == 4
  127. }
  128.  
  129. def "Should not throw an IndexOutOfBoundsException when removing an existent item from array"() {
  130. given:
  131. def list = [1, 2, 3, 4]
  132.  
  133. when:
  134. list.remove(0)
  135.  
  136. then:
  137. notThrown(IndexOutOfBoundsException) // exception condition: feature method will fail if any other exception is thrown
  138. list.size() == 3
  139. }
  140.  
  141. @Unroll("[#iterationIndex] #a divided by zero fails")
  142. // use this to give iteration a different name
  143. def "division through zero fails"() {
  144. when:
  145. a / 0
  146.  
  147. then:
  148.  
  149. where:
  150. a << [1, 2, 3, 4, 5] // this is a data pipe we use if multiple inputs expected to behave the same
  151. // or as data table
  152. // a | _
  153. // 1 | _
  154. // 2 | _
  155. // 3 | _
  156. // 4 | _
  157. // 5 | _
  158. }
  159.  
  160. //@Rollup // use this if you do not want each iteration logged separately
  161. // parameter declaration is not necessary BUT it helps with IDE support
  162. def "#a to the power of two equals #c"(int a, int b, int c) { // by using a hashtag we can reference our variable
  163. expect:
  164. Math.pow(a, b) == c
  165.  
  166. where:
  167. // this is a data table we use if we have multiple inputs and/or expected values for each iteration
  168. // syntax: input | input || expected value
  169. a | b || c
  170. 1 | 2 || 1
  171. 2 | 2 || 4
  172. 3 | 2 || 9
  173. // in data tables you can access the own column vars, other data table vars, static fields and @Shared instance fields
  174. }
  175.  
  176. // mocks can be used for mocking and stubbing, stubs only for stubbing. use stubs if mocking is not needed
  177. def "A mock tracks its calls"() {
  178. given:
  179. DatabaseBalEnergyConfigQueryService balEnergyConfigurationService = Mock() // mocks are useful to observer/check the code flow
  180.  
  181. when:
  182. balEnergyConfigurationService.getMessageMaximumAgeInMinutes()
  183. balEnergyConfigurationService.getCapacityQuantitiesFollowUpTime()
  184. balEnergyConfigurationService.getCapacityQuantitiesFollowUpTime()
  185. balEnergyConfigurationService.getCapacityQuantitiesFollowUpTime()
  186.  
  187. then:
  188. 1 * balEnergyConfigurationService.getMessageMaximumAgeInMinutes() // only one call
  189. (2.._) * balEnergyConfigurationService.getCapacityQuantitiesFollowUpTime() // at least two calls (_ is any)
  190. }
  191.  
  192. def "[lenient mocking] mocks will return defaults (false, 0 or null)"() {
  193. given:
  194. def balEnergyConfigurationService = Mock(DatabaseBalEnergyConfigQueryService)
  195. MasterDataConfigQueryService masterDataConfigQueryService = Mock()
  196. BeDemandAssignmentService beDemandAssignmentService = Mock() // preferred style since it may give better IDE support
  197. TimeSeriesWriteProtectionQueryService timeSeriesWriteProtectionQueryService = Mock()
  198.  
  199. when:
  200. def longDefault = balEnergyConfigurationService.getNodeStatusChangedAtThreshold()
  201. //def LongDefault = balEnergyConfigurationService.getCapacityQuantitiesFollowUpTime()
  202. def customClassDefault = masterDataConfigQueryService.getHgasNorthZoneId()
  203. def booleanDefault = beDemandAssignmentService.ignoreLocalZonesNeededForAssignments(Mock(Zone))
  204. def optionalDefault = beDemandAssignmentService.getAssignedZoneId(GasQuality.H_GAS, ZonedDateTime.now(), ZonedDateTime.now())
  205. def listDefault = timeSeriesWriteProtectionQueryService.findAll(Mock(NodeId), Mock(GasDay))
  206.  
  207. // more then around five conditions => check if you test different features you may should split in smaller feature methods
  208. then:
  209. longDefault == 0
  210. //LongDefault == null
  211. customClassDefault == null
  212. !booleanDefault
  213. optionalDefault == null
  214. listDefault == null
  215. }
  216.  
  217. def "[lenient mocking] stubs will return sensible defaults"() {
  218. given:
  219. DatabaseBalEnergyConfigQueryService balEnergyConfigurationService = Stub()
  220. MasterDataConfigQueryService masterDataConfigQueryService = Stub()
  221. BeDemandAssignmentService beDemandAssignmentService = Stub()
  222. TimeSeriesWriteProtectionQueryService timeSeriesWriteProtectionQueryService = Stub()
  223.  
  224. when:
  225. def longDefault = balEnergyConfigurationService.getNodeStatusChangedAtThreshold()
  226. def LongDefault = balEnergyConfigurationService.getCapacityQuantitiesFollowUpTime()
  227. def customClassDefault = masterDataConfigQueryService.getHgasNorthZoneId()
  228. def booleanDefault = beDemandAssignmentService.ignoreLocalZonesNeededForAssignments(Mock(Zone))
  229. def optionalDefault = beDemandAssignmentService.getAssignedZoneId(GasQuality.H_GAS, ZonedDateTime.now(), ZonedDateTime.now())
  230. def listDefault = timeSeriesWriteProtectionQueryService.findAll(Mock(NodeId), Mock(GasDay))
  231.  
  232. then:
  233. longDefault == 0
  234. LongDefault == 0
  235. customClassDefault
  236. customClassDefault.getValue() == 0
  237. !booleanDefault
  238. optionalDefault.isEmpty()
  239. listDefault.size() == 0
  240. }
  241.  
  242. def "Stub will return what we want (fixed)"() {
  243. given: "a procedure id" // we can label any block to provide additional information
  244. Long procedureId = 0L
  245.  
  246. and: "a stubbed rest controller" // using labels and 'and' is preferred
  247. DemandAssessmentRestController controller = Stub()
  248. controller.verifyCalculationProcedureSubstituteValues(procedureId, []) >> ResponseEntity.accepted().build()
  249.  
  250. expect: "calling the stubbed method behave as expected"
  251. controller.verifyCalculationProcedureSubstituteValues(procedureId, []) == ResponseEntity.accepted().build()
  252. }
  253.  
  254. def "Stub will return what we want (sequence)"() {
  255. given: "a procedure id"
  256. Long procedureId = 0L
  257.  
  258. and: "a stubbed rest controller returning always notFound except on the first call"
  259. DemandAssessmentRestController controller = Stub()
  260. controller.verifyCalculationProcedureSubstituteValues(procedureId, []) >>> [
  261. ResponseEntity.accepted().build(),
  262. ResponseEntity.notFound().build()
  263. ]
  264.  
  265. when: "we call it three times"
  266. def callOne = controller.verifyCalculationProcedureSubstituteValues(procedureId, [])
  267. def callTwo = controller.verifyCalculationProcedureSubstituteValues(procedureId, [])
  268. def callThree = controller.verifyCalculationProcedureSubstituteValues(procedureId, [])
  269.  
  270. then: "the first response is accepted"
  271. callOne == ResponseEntity.accepted().build()
  272.  
  273. and: "the next responses are notFound"
  274. callTwo == ResponseEntity.notFound().build()
  275. callThree == ResponseEntity.notFound().build()
  276. }
  277.  
  278. /*
  279.   more one stubbing (in short)
  280.   details: https://spockframework.org/spock/docs/2.0/all_in_one.html#_stubbing
  281.  
  282.   computing result:
  283.   subscriber.receive(_) >> { args -> args[0].size() > 3 ? "ok" : "fail" }
  284.   or
  285.   subscriber.receive(_) >> { String message -> message.size() > 3 ? "ok" : "fail" }
  286.  
  287.   side effects: (can contain more abitrary code)
  288.   subscriber.receive(_) >> { throw new InternalError("ouch") }
  289.  
  290.   chaining resulst:
  291.   subscriber.receive(_) >>> ["ok", "fail", "ok"] >> { throw new InternalError() } >> "ok"
  292.  
  293.   default non-null result:
  294.   subscriber.receive(_) >> _
  295.  
  296.   combining mocking and stubbing: (must be in one statement)
  297.   1 * subscriber.receive("message1") >> "ok"
  298.   */
  299.  
  300. def "Using a helper method works"() {
  301. given: "a stubbed lockingService always returning a ignored lock with a given key"
  302. LockingService lockingService = Stub()
  303. lockingService.lock(_ as LockKey, _ as boolean) >> { LockKey key ->
  304. FencedLock lock = Stub()
  305. lock.getName() >> key.getKey()
  306. return LockHolder.forIgnoredLock(Mock(MftLogger), lock)
  307. }
  308.  
  309. and: "a real timeSeriesLockService"
  310. @Subject TimeSeriesLockService lockService = new TimeSeriesLockService(lockingService)
  311.  
  312. when: "we call to create a lock for distrivution"
  313. LockHolder lock = lockService.lockDistribution(GasDay.now())
  314.  
  315. then: "this lock contains the expected key"
  316. checkKeyExists(lock)
  317. // or use 'with' to check multiple properties of an object, which has the same behavior like a helper methods
  318. with(lock) {
  319. keyString.size() >= 0
  320. metaClass != null
  321. }
  322. // with you want to check all assertions once use verifyAll
  323. verifyAll(lock) {
  324. keyString.size() >= 0
  325. metaClass != null
  326. }
  327. }
  328.  
  329. // in this case we must use the assert keyword, also test fails on first failed assertion
  330. private static void checkKeyExists(LockHolder lock) {
  331. assert lock
  332. assert lock.getKeyString().size() >= 0
  333. assert lock.metaClass != null
  334. }
  335.  
  336. def "working with a mutable clock works"() {
  337. given: "a clock to play with"
  338. MutableClock clock = new MutableClock(DateUtils.from(2021, 7, 9, 0, 0, 0))
  339.  
  340. and: "we put the clock into the application"
  341. TestUtils.startTimeBubble(clock)
  342.  
  343. and: "we remember a birthday"
  344. ZonedDateTime birthday = DateUtils.from(2021, 7, 10, 0, 0, 0)
  345.  
  346. expect: "its not the birthday day and threads execution time is now"
  347. DateUtils.now() != birthday
  348. ThreadUtils.executionTimestamp() == DateUtils.now()
  349.  
  350. when: "we tell the clock to be a day later"
  351. clock + Duration.ofDays(1)
  352.  
  353. then: "it's birthday time but thread execution time is kept to now"
  354. DateUtils.now() == birthday
  355. ThreadUtils.executionTimestamp() != birthday
  356.  
  357. cleanup: "is important after playing with the application clock"
  358. TestUtils.stopTimeBubble()
  359. }
  360.  
  361. // @TimeBubble is also allowed on spec (class) level
  362. @TimeBubble(year = 2021, month = 4, day = 4, hour = 5, minutes = 11, seconds = 0)
  363. def "testing in a time bubble by annotation works"() {
  364. expect:
  365. DateUtils.now() == DateUtils.from(2021, 4, 4, 5, 11, 0)
  366. }
  367.  
  368. def "testing in a time bubble by util works"() {
  369. given: "a timeout is configured"
  370. CalculationProcedureConfigQueryService calculationProcedureConfigurationService = Stub()
  371. calculationProcedureConfigurationService.getSolverTimeoutInSeconds() >> timeoutInSeconds
  372.  
  373. and: "a solverApi mock is provided"
  374. SolverApi solverApi = Mock()
  375. def timeoutSolverApiBuilder = Stub(TimeoutSolverApiBuilder)
  376. timeoutSolverApiBuilder.build(timeoutInSeconds, _ as List<HttpStatus>) >> solverApi
  377.  
  378. and: "our test subject: solver client"
  379. NetFlowDistributionSolverClient uut = new NetFlowDistributionSolverClient(
  380. Stub(MftLogger),
  381. calculationProcedureConfigurationService,
  382. timeoutSolverApiBuilder
  383. )
  384.  
  385. when: "solver client internal method is called"
  386. // we can use 'doInTimeBubble' instead of manual set start and end of fixed time,
  387. // if we do not have checks in 'then' block which require the fixed time
  388. TestUtils.doInTimeBubble(fixedTime, time -> {
  389. uut.callSolverWithTimeout(Mock(ProblemDto), timeoutInSeconds)
  390. })
  391.  
  392. then: "solver api is called with correct timeout"
  393. 1 * solverApi.solveWithHttpInfo(_ as ProblemDto, expectedTimeoutTime)
  394.  
  395. where: "different starting time are tested"
  396. timeoutInSeconds | fixedTime || expectedTimeoutTime
  397. 0L | DateUtils.from(2021, 4, 4, 5, 11, 0) || null
  398. 100L | DateUtils.from(2021, 4, 4, 5, 11, 0) || DateUtils.from(2021, 4, 4, 5, 12, 40).toOffsetDateTime().toString()
  399. 100L | DateUtils.from(2021, 12, 31, 23, 59, 0) || fixedTime.plusSeconds(timeoutInSeconds).toOffsetDateTime().toString()
  400. }
  401.  
  402. /*
  403.   Use @Retry(<retryCount>) if you have a weird test case that sometimes fails due to non-deterministic integration test.
  404.   <rertyCount> default is: 3
  405.  
  406.   Use @Timeout(<threshold>) to stop an iteration which may can take to long
  407.   */
  408. }