refactor: migrate :src:protocol:bolt Groovy tests to Kotlin
diff --git a/src/protocol/bolt/build.gradle.kts b/src/protocol/bolt/build.gradle.kts
index 431d0d6..86be43a 100644
--- a/src/protocol/bolt/build.gradle.kts
+++ b/src/protocol/bolt/build.gradle.kts
@@ -28,4 +28,5 @@
     implementation("com.fasterxml.jackson.core:jackson-databind")
 
     testImplementation(testFixtures(projects.src.core))
+    testImplementation("io.mockk:mockk")
 }
diff --git a/src/protocol/bolt/src/test/kotlin/org/apache/jmeter/protocol/bolt/sampler/BoltSamplerTest.kt b/src/protocol/bolt/src/test/kotlin/org/apache/jmeter/protocol/bolt/sampler/BoltSamplerTest.kt
index 65e6b09..96859fd 100644
--- a/src/protocol/bolt/src/test/kotlin/org/apache/jmeter/protocol/bolt/sampler/BoltSamplerTest.kt
+++ b/src/protocol/bolt/src/test/kotlin/org/apache/jmeter/protocol/bolt/sampler/BoltSamplerTest.kt
@@ -17,10 +17,19 @@
 
 package org.apache.jmeter.protocol.bolt.sampler
 
+import io.mockk.every
+import io.mockk.justRun
+import io.mockk.mockk
 import org.apache.jmeter.protocol.bolt.config.BoltConnectionElement
 import org.apache.jmeter.samplers.Entry
+import org.apache.jmeter.samplers.SampleResult
 import org.apache.jmeter.threads.JMeterContextService
 import org.apache.jmeter.threads.JMeterVariables
+import org.junit.jupiter.api.Assertions.assertEquals
+import org.junit.jupiter.api.Assertions.assertFalse
+import org.junit.jupiter.api.Assertions.assertTrue
+import org.junit.jupiter.api.BeforeEach
+import org.junit.jupiter.api.Test
 import org.neo4j.driver.Driver
 import org.neo4j.driver.Record
 import org.neo4j.driver.Result
@@ -29,166 +38,156 @@
 import org.neo4j.driver.summary.ResultSummary
 import org.neo4j.driver.summary.SummaryCounters
 
-import spock.lang.Specification
+class BoltSamplerTest {
+    lateinit var sampler: BoltSampler
+    lateinit var entry: Entry
+    lateinit var session: Session
 
-class BoltSamplerSpec extends Specification {
-
-    BoltSampler sampler
-    Entry entry
-    Session session
-
-    def setup() {
-        sampler = new BoltSampler()
-        entry = new Entry()
-        def driver = Mock(Driver)
-        def boltConfig = new BoltConnectionElement()
-        def variables = new JMeterVariables()
+    @BeforeEach
+    fun setup() {
+        sampler = BoltSampler()
+        entry = Entry()
+        session = mockk<Session> {
+            justRun { close() }
+        }
+        val driver = mockk<Driver> {
+            every { session(any()) } returns session
+        }
+        val boltConfig = BoltConnectionElement()
+        val variables = JMeterVariables()
         // ugly but could not find a better way to pass the driver to the sampler...
         variables.putObject(BoltConnectionElement.BOLT_CONNECTION, driver)
-        JMeterContextService.getContext().setVariables(variables)
+        JMeterContextService.getContext().variables = variables
         entry.addConfigElement(boltConfig)
-        session = Mock(Session)
-        driver.session(_) >> session
     }
 
-    def "should execute return success on successful query"() {
-        given:
-            sampler.setCypher("MATCH x")
-            session.run("MATCH x", [:], _) >> getEmptyQueryResult()
-        when:
-            def response = sampler.sample(entry)
-        then:
-            response.isSuccessful()
-            response.isResponseCodeOK()
-            def str = response.getResponseDataAsString()
-            str.contains("Summary:")
-            str.endsWith("Records: Skipped")
-            response.getSampleCount() == 1
-            response.getErrorCount() == 0
-            //  The sampler was executed, so start and end times should be set
-            response.getStartTime() > 0
-            response.getEndTime() > 0
+    @Test
+    fun `should execute return success on successful query`() {
+        sampler.cypher = "MATCH x"
+        every {
+            session.run("MATCH x", mapOf(), any())
+        } returns getEmptyQueryResult()
+        val response = sampler.sample(entry)
+
+        assertSuccessResult(response, "Records: Skipped")
     }
 
-    def "should not display results by default"() {
-        given:
-            sampler.setCypher("MATCH x")
-            session.run("MATCH x", [:], _) >> getPopulatedQueryResult()
-        when:
-            def response = sampler.sample(entry)
-        then:
-            response.isSuccessful()
-            response.isResponseCodeOK()
-            def str = response.getResponseDataAsString()
-            str.contains("Summary:")
-            str.endsWith("Records: Skipped")
-            response.getSampleCount() == 1
-            response.getErrorCount() == 0
+    private fun assertSuccessResult(response: SampleResult, responseDataTail: String) {
+        val str = response.responseDataAsString
+        assertTrue(str.contains("Summary:"), "response contains 'Summary:', got '$str'")
+        assertTrue(str.endsWith(responseDataTail), "response ends with '$responseDataTail', got '$str'")
+        assertTrue(response.isSuccessful, ".isSuccessful()")
+        assertTrue(response.isResponseCodeOK, ".isResponseCodeOK()")
+        assertEquals(1, response.sampleCount, ".sampleCount")
+        assertEquals(0, response.errorCount, ".errorCount")
+        assertSamplerStarted(response)
     }
 
-    def "should display results if asked"() {
-        given:
-            sampler.setCypher("MATCH x")
-            sampler.setRecordQueryResults(true)
-            session.run("MATCH x", [:], _) >> getPopulatedQueryResult()
-        when:
-            def response = sampler.sample(entry)
-        then:
-            response.isSuccessful()
-            response.isResponseCodeOK()
-            def str = response.getResponseDataAsString()
-            str.contains("Summary:")
-            str.endsWith("Mock for type 'Record'")
-            response.getSampleCount() == 1
-            response.getErrorCount() == 0
-            //  The sampler was executed, so start and end times should be set
-            response.getStartTime() > 0
-            response.getEndTime() > 0
+    private fun assertSamplerStarted(response: SampleResult) {
+        assertTrue(response.startTime > 0, "The sampler was executed, so start and end times should be set")
+        assertTrue(response.endTime > 0, "The sampler was executed, so start and end times should be set")
     }
 
-    def "should return error on failed query"() {
-        given:
-            sampler.setCypher("MATCH x")
-            session.run("MATCH x", [:], _) >> { throw new RuntimeException("a message") }
-        when:
-            def response = sampler.sample(entry)
-        then:
-            !response.isSuccessful()
-            !response.isResponseCodeOK()
-            response.getResponseCode() == "500"
-            def str = response.getResponseDataAsString()
-            str.contains("a message")
-            response.getSampleCount() == 1
-            response.getErrorCount() == 1
-            //  The sampler was executed, so start and end times should be set
-            response.getStartTime() > 0
-            response.getEndTime() > 0
+    private fun assertFailureResult(
+        response: SampleResult,
+        responseCode: String,
+        message: String,
+        samplerStarted: Boolean
+    ) {
+        val str = response.responseDataAsString
+        assertFalse(str.contains("Summary:"), "response contains 'Summary:', got $str")
+        assertTrue(str.contains(message), "response contains '$message', got $str")
+        assertFalse(response.isSuccessful, ".isSuccessful()")
+        assertFalse(response.isResponseCodeOK, ".isResponseCodeOK()")
+        assertEquals(responseCode, response.responseCode, ".responseCode")
+        assertEquals(1, response.sampleCount, ".sampleCount")
+        assertEquals(1, response.errorCount, ".errorCount")
+        if (samplerStarted) {
+            assertSamplerStarted(response)
+        } else {
+            assertSamplerNotStarted(response)
+        }
     }
 
-    def "should return error on invalid parameters"() {
-        given:
-            sampler.setCypher("MATCH x")
-            sampler.setParams("{invalid}")
-        when:
-            def response = sampler.sample(entry)
-        then:
-            !response.isSuccessful()
-            !response.isResponseCodeOK()
-            response.getResponseCode() == "500"
-            def str = response.getResponseDataAsString()
-            str.contains("Unexpected character")
-            response.getSampleCount() == 1
-            response.getErrorCount() == 1
-            // The sampler fails at parameter preparation, so no time is recorded
-            response.getStartTime() == 0
-            response.getEndTime() == 0
-            response.getTime() == 0
+    private fun assertSamplerNotStarted(response: SampleResult) {
+        assertEquals(0, response.startTime, "The sampler fails at parameter preparation, so no time is recorded")
+        assertEquals(0, response.endTime, "The sampler fails at parameter preparation, so no time is recorded")
     }
 
-    def "should return db error code"() {
-        given:
-            sampler.setCypher("MATCH x")
-            session.run("MATCH x", [:], _) >> { throw new ClientException("a code", "a message") }
-        when:
-            def response = sampler.sample(entry)
-        then:
-            response.getResponseCode() == "a code"
+    @Test
+    fun `should not display results by default`() {
+        sampler.cypher = "MATCH x"
+        every {
+            session.run("MATCH x", mapOf(), any())
+        } returns getPopulatedQueryResult()
+        val response = sampler.sample(entry)
+
+        assertSuccessResult(response, "Records: Skipped")
     }
 
-    def "should ignore invalid timeout values"() {
-        given:
-            sampler.setCypher("MATCH x")
-            sampler.setTxTimeout(-1)
-            session.run("MATCH x", [:], _) >> getEmptyQueryResult()
-        when:
-            def response = sampler.sample(entry)
-        then:
-            response.isSuccessful()
-            response.isResponseCodeOK()
-            def str = response.getResponseDataAsString()
-            str.contains("Summary:")
-            str.endsWith("Records: Skipped")
-            response.getSampleCount() == 1
-            response.getErrorCount() == 0
+    @Test
+    fun `should display results if asked`() {
+        sampler.cypher = "MATCH x"
+        sampler.isRecordQueryResults = true
+        every {
+            session.run("MATCH x", mapOf(), any())
+        } returns getPopulatedQueryResult()
+        val response = sampler.sample(entry)
+        assertSuccessResult(response, "Mock for type 'Record'")
     }
 
-    def getEmptyQueryResult() {
-        def queryResult = Mock(Result)
-        def summary = Mock(ResultSummary)
-        queryResult.consume() >> summary
-        SummaryCounters counters = Mock(SummaryCounters)
-        summary.counters() >> counters
-        return queryResult
+    @Test
+    fun `should return error on failed query`() {
+        sampler.cypher = "MATCH x"
+        every { session.run("MATCH x", mapOf(), any()) } throws RuntimeException("a message")
+        val response = sampler.sample(entry)
+
+        assertFailureResult(response, "500", "a message", samplerStarted = true)
     }
 
-    def getPopulatedQueryResult() {
-        def queryResult = Mock(Result)
-        def summary = Mock(ResultSummary)
-        def list = [Mock(Record), Mock(Record), Mock(Record)]
-        queryResult.consume() >> summary
-        queryResult.list() >> list
-        SummaryCounters counters = Mock(SummaryCounters)
-        summary.counters() >> counters
-        return queryResult
+    @Test
+    fun `should return error on invalid parameters`() {
+        sampler.cypher = "MATCH x"
+        sampler.params = "{invalid}"
+        val response = sampler.sample(entry)
+
+        assertFailureResult(response, "500", "Unexpected character", samplerStarted = false)
     }
+
+    @Test
+    fun `should return db error code`() {
+        sampler.cypher = "MATCH x"
+        every { session.run("MATCH x", mapOf(), any()) } throws ClientException("a code", "a message")
+        val response = sampler.sample(entry)
+        assertEquals("a code", response.responseCode)
+    }
+
+    @Test
+    fun `should ignore invalid timeout values`() {
+        sampler.cypher = "MATCH x"
+        sampler.txTimeout = -1
+        every { session.run("MATCH x", mapOf(), any()) } returns getEmptyQueryResult()
+        val response = sampler.sample(entry)
+        assertSuccessResult(response, "Records: Skipped")
+    }
+
+    private fun getEmptyQueryResult() =
+        mockk<Result> {
+            every { consume() } returns mockk<ResultSummary> {
+                every { counters() } returns mockk<SummaryCounters>(relaxed = true)
+            }
+        }
+
+    @Suppress("LABEL_NAME_CLASH")
+    private fun getPopulatedQueryResult() =
+        mockk<Result> {
+            every { consume() } returns mockk<ResultSummary> {
+                every { counters() } returns mockk<SummaryCounters>(relaxed = true)
+            }
+            every { list() } returns listOf(
+                mockk<Record> { every { this@mockk.toString() } returns "Mock for type 'Record'" },
+                mockk<Record> { every { this@mockk.toString() } returns "Mock for type 'Record'" },
+                mockk<Record> { every { this@mockk.toString() } returns "Mock for type 'Record'" },
+            )
+        }
 }