refactor: replace Groovy tests in :src:core with Kotlin
diff --git a/checksum.xml b/checksum.xml
index a29e0ba..ce13b9d 100644
--- a/checksum.xml
+++ b/checksum.xml
@@ -94,6 +94,7 @@
     <trusted-key id='d477d51812e692011db11e66a6ea2e2bf22e0543' group='io.github.java-diff-utils' />
     <trusted-key id='e52567d2589415bd74eb4c2867631bc0568801c3' group='io.github.microutils' />
     <trusted-key id='050a37a2e0577f4baa095b52602ec18d20c4661c' group='io.github.x-stream' />
+    <trusted-key id='59b06224fd8912e36603be79fefe78456eddc34a' group='io.mockk' />
     <trusted-key id='6dd3b8c64ef75253beb2c53ad908a43fb7ec07ac' group='jakarta.activation' />
     <trusted-key id='a1483f88ba771993ab609c43590c2310cee1c9be' group='jakarta.jms' />
     <trusted-key id='fc411cd3cb7dcb0abc9801058118b3bcdb1a5000' group='jakarta.xml.bind' />
diff --git a/gradle/checksum-dependency-plugin/cached-pgp-keys/4a/59b06224fd8912e36603be79fefe78456eddc34a.asc b/gradle/checksum-dependency-plugin/cached-pgp-keys/4a/59b06224fd8912e36603be79fefe78456eddc34a.asc
new file mode 100644
index 0000000..9768ac8
--- /dev/null
+++ b/gradle/checksum-dependency-plugin/cached-pgp-keys/4a/59b06224fd8912e36603be79fefe78456eddc34a.asc
@@ -0,0 +1,21 @@
+-----BEGIN PGP PUBLIC KEY BLOCK-----
+
+mQGNBF+y5lsBDAC5h0qk+OBAscHc/ac3A9C8ZPohXcTVpsOjds73soUAH+QCKO0y
+gAUuG/hUUU9xkm9PgTwWOEl2qDDcOFXY+9ykeYNUUcCWfs+JmVRfRod4W5pntaT4
+g9Z+T6LbXKNAfZgPvTv4rr7UjD05N4XS4vckrS4taYLtBRJAmqT3pt43KxlyoTbh
+f4xcO2rWeXsPqgzTYIHH7M5mYPeqA2gc9NBAhkHjesFuYfWXqUfOVcOLvzULxrra
+pZDOyrINr83WikC8DkuDrAav4mIWjIhYmfBWzuNeYJsusVnFENeOxpEHV8RT+8uE
+v1gPjbjAKUPfZoa7egvz3EmDkshpNIIym0XxNGTj5ntJWR2SLT7mDrSYPeHrZKW6
+/aKuAcOxpGLpdVOMM+y4N5mTQfdlL81G9kbGQarMmwGaJb2a82PaF20wRwVgiVfO
+p/GWgwXr0XdJNLqx13LdM8BMM5vmLomOQOjnpQBOlJWRgrYUJQOReKAEAQqNMsxS
+IW9laXkrewJtblEAEQEAAbkBjQRfsuZbAQwAr/owOCxebZMTjp9coUsDjFabJowZ
+H0F++vuh73UY/v2z+vqwug89ABjrPUIccE9/t5dRyEjBt0ED2KFM3mi692QhyYE8
+uRaUKWz0kYaMPuw0W7RTblhhXErBror1m00ibNDGSf3fIr9FZ7u5ep/RTFDCDaZg
+qtWQGVD+uLnqA5EHdwUyieWDSgeThRddM3hKXB1eld4JKJPzQ0XwXLgv8MmbOc9W
+5pxb1ULD44s17SKDjlBtRvrSRdgExgfpKU63HH0D6mn+kJP066knYddunNw7HsKg
+v2mPudTVIQXYGb8xDLhP6UYJowdvJEbM8vp1t/d/CgCMXyVVJOlTZwdJaECE0n4g
+MJ2ihDZ8qbDl81OQhTiWvgoKOFHLTwZgmyhKpt36RvkXkoywqlKNGaZVNi9Ct1Xx
+fprj4sozA5Mcf8BAaxXV9sQfZLKH7NhGrFzc/r1v2R37RYWfmYL79dJj1xsrcEN8
+26cSoyLi69+XOxvbdo4KfyopRxECTdd6Oqk/ABEBAAE=
+=+/j7
+-----END PGP PUBLIC KEY BLOCK-----
diff --git a/gradle/checksum-dependency-plugin/cached-pgp-keys/4a/fefe78456eddc34a.fingerprints b/gradle/checksum-dependency-plugin/cached-pgp-keys/4a/fefe78456eddc34a.fingerprints
new file mode 100644
index 0000000..cc05455
--- /dev/null
+++ b/gradle/checksum-dependency-plugin/cached-pgp-keys/4a/fefe78456eddc34a.fingerprints
@@ -0,0 +1 @@
+59b06224fd8912e36603be79fefe78456eddc34a
diff --git a/gradle/checksum-dependency-plugin/cached-pgp-keys/7c/c3720ddc2e713b7c.fingerprints b/gradle/checksum-dependency-plugin/cached-pgp-keys/7c/c3720ddc2e713b7c.fingerprints
new file mode 100644
index 0000000..cc05455
--- /dev/null
+++ b/gradle/checksum-dependency-plugin/cached-pgp-keys/7c/c3720ddc2e713b7c.fingerprints
@@ -0,0 +1 @@
+59b06224fd8912e36603be79fefe78456eddc34a
diff --git a/src/bom-testing/build.gradle.kts b/src/bom-testing/build.gradle.kts
index 0f28bf8..65ddd38 100644
--- a/src/bom-testing/build.gradle.kts
+++ b/src/bom-testing/build.gradle.kts
@@ -38,6 +38,7 @@
         // then it should be declared as "api" here since we use useCompileClasspathVersions
         // to make runtime classpath consistent with the compile one.
         api("com.github.tomakehurst:wiremock-jre8:2.35.1")
+        api("io.mockk:mockk:1.13.7")
         api("junit:junit:4.13.2")
         api("net.bytebuddy:byte-buddy:1.14.11")
         api("nl.jqno.equalsverifier:equalsverifier:3.15.5")
diff --git a/src/core/build.gradle.kts b/src/core/build.gradle.kts
index 1d3e73d..b5e0d3d 100644
--- a/src/core/build.gradle.kts
+++ b/src/core/build.gradle.kts
@@ -120,6 +120,7 @@
     runtimeOnly("xml-apis:xml-apis")
 
     testImplementation("commons-net:commons-net")
+    testImplementation("io.mockk:mockk")
     testRuntimeOnly("org.spockframework:spock-core")
 
     testFixturesApi(testFixtures(projects.src.jorphan))
diff --git a/src/core/src/main/java/org/apache/jmeter/services/FileServer.java b/src/core/src/main/java/org/apache/jmeter/services/FileServer.java
index 1f9b836..efd4ad7 100644
--- a/src/core/src/main/java/org/apache/jmeter/services/FileServer.java
+++ b/src/core/src/main/java/org/apache/jmeter/services/FileServer.java
@@ -81,7 +81,7 @@
     private volatile String scriptName;
 
     // Cannot be instantiated
-    private FileServer() {
+    FileServer() {
         base = new File(DEFAULT_BASE);
         log.info("Default base='{}'", DEFAULT_BASE);
     }
diff --git a/src/core/src/test/groovy/org/apache/jmeter/gui/HtmlReportGUISpec.groovy b/src/core/src/test/groovy/org/apache/jmeter/gui/HtmlReportGUISpec.groovy
deleted file mode 100644
index a0caec0..0000000
--- a/src/core/src/test/groovy/org/apache/jmeter/gui/HtmlReportGUISpec.groovy
+++ /dev/null
@@ -1,47 +0,0 @@
-/*
- * Licensed to the Apache Software Foundation (ASF) under one or more
- * contributor license agreements.  See the NOTICE file distributed with
- * this work for additional information regarding copyright ownership.
- * The ASF licenses this file to you under the Apache License, Version 2.0
- * (the "License"); you may not use this file except in compliance with
- * the License.  You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package org.apache.jmeter.gui;
-
-import org.apache.jmeter.gui.tree.JMeterTreeListener
-import org.apache.jmeter.gui.tree.JMeterTreeModel
-import org.apache.jmeter.junit.spock.JMeterSpec
-
-import spock.lang.IgnoreIf
-
-@IgnoreIf({ JMeterSpec.isHeadless() })
-class HtmlReportGUISpec extends JMeterSpec {
-
-    def "test HtmlReportUI initialization"() {
-        given:
-            def sut = new HtmlReportUI();
-            def treeModel = new JMeterTreeModel();
-            def treeListener = new JMeterTreeListener(treeModel);
-            GuiPackage.initInstance(treeListener, treeModel);
-            GuiPackage.getInstance().setMainFrame(new MainFrame(treeModel, treeListener));
-        when:
-            sut.showInputDialog(GuiPackage.getInstance().getMainFrame())
-            Thread.sleep(50) // https://bugs.openjdk.java.net/browse/JDK-5109571
-            sut.messageDialog.setVisible(false)
-        then:
-            sut.csvFilePathTextField.getText() == ""
-            sut.userPropertiesFilePathTextField.getText() == ""
-            sut.outputDirectoryPathTextField.getText() == ""
-            sut.reportArea.getText() == ""
-            sut.messageDialog.getComponents().length == 1;
-    }
-}
diff --git a/src/core/src/test/groovy/org/apache/jmeter/testelement/AbstractTestElementSpec.groovy b/src/core/src/test/groovy/org/apache/jmeter/testelement/AbstractTestElementSpec.groovy
deleted file mode 100644
index 91ddecf..0000000
--- a/src/core/src/test/groovy/org/apache/jmeter/testelement/AbstractTestElementSpec.groovy
+++ /dev/null
@@ -1,99 +0,0 @@
-/*
- * Licensed to the Apache Software Foundation (ASF) under one or more
- * contributor license agreements.  See the NOTICE file distributed with
- * this work for additional information regarding copyright ownership.
- * The ASF licenses this file to you under the Apache License, Version 2.0
- * (the "License"); you may not use this file except in compliance with
- * the License.  You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package org.apache.jmeter.testelement
-
-import org.apache.commons.lang3.NotImplementedException
-import org.apache.jmeter.junit.spock.JMeterSpec
-import org.apache.jmeter.testelement.property.JMeterProperty
-import org.apache.jmeter.testelement.property.MultiProperty
-import org.apache.jmeter.testelement.property.PropertyIterator
-import org.apache.jmeter.testelement.property.TestElementProperty
-
-import spock.lang.Unroll
-
-@Unroll
-class AbstractTestElementSpec extends JMeterSpec {
-
-    def "set outer properties as temporary when using a TestElementProperty"() {
-        given:
-            AbstractTestElement sut = Spy(AbstractTestElement.class)
-            def outerElement = Mock(TestElement.class)
-            def innerElement = Mock(TestElement.class)
-            def outerProp = new TestElementProperty("outerProp", outerElement)
-            def innerProp = new TestElementProperty("innerProp", innerElement)
-            outerProp.addProperty(innerProp)
-        when:
-            sut.setTemporary(outerProp)
-        then:
-            sut.isTemporary(outerProp)
-            !sut.isTemporary(innerProp)
-    }
-
-    def "set all properties as temporary when using a MultiProperty"() {
-        given:
-            AbstractTestElement sut = Spy(AbstractTestElement.class)
-            def outerProp = new MinimalMultiProperty()
-            def innerProp = new MinimalMultiProperty()
-            outerProp.addProperty(innerProp)
-        when:
-            sut.setTemporary(outerProp)
-        then:
-            sut.isTemporary(outerProp)
-            sut.isTemporary(innerProp)
-    }
-
-    private class MinimalMultiProperty extends MultiProperty {
-
-        Set<JMeterProperty> props = new HashSet<>()
-
-        @Override
-        void recoverRunningVersion(TestElement owner) {
-            throw new NotImplementedException()
-        }
-
-        @Override
-        String getStringValue() {
-            throw new NotImplementedException()
-        }
-
-        @Override
-        Object getObjectValue() {
-            return null
-        }
-
-        @Override
-        void setObjectValue(Object value) {
-            throw new NotImplementedException()
-        }
-
-        @Override
-        PropertyIterator iterator() {
-            return props.iterator() as PropertyIterator
-        }
-
-        @Override
-        void addProperty(JMeterProperty prop) {
-            props.add(prop)
-        }
-
-        @Override
-        void clear() {
-            props.clear()
-        }
-    }
-}
diff --git a/src/core/src/test/kotlin/org/apache/jmeter/engine/util/FunctionParserTest.kt b/src/core/src/test/kotlin/org/apache/jmeter/engine/util/FunctionParserTest.kt
index 039f123..0b4d0f7 100644
--- a/src/core/src/test/kotlin/org/apache/jmeter/engine/util/FunctionParserTest.kt
+++ b/src/core/src/test/kotlin/org/apache/jmeter/engine/util/FunctionParserTest.kt
@@ -17,46 +17,53 @@
 
 package org.apache.jmeter.engine.util
 
+import com.google.auto.service.AutoService
 import org.apache.jmeter.functions.Function
 import org.apache.jmeter.samplers.SampleResult
 import org.apache.jmeter.samplers.Sampler
+import org.junit.jupiter.api.Assertions
+import org.junit.jupiter.params.ParameterizedTest
+import org.junit.jupiter.params.provider.ValueSource
 
-import spock.lang.Specification
-import spock.lang.Unroll
+class FunctionParserTest {
+    @ParameterizedTest
+    @ValueSource(
+        strings = [
+            "\${__func()}",
+            "\${ __func()}",
+            "\${__func() }",
+            "\${ __func() }",
+        ]
+    )
+    fun `simple function call`(input: String) {
+        val parser = FunctionParser()
+        val result = parser.compileString(input)
 
-@Unroll
-class FunctionParserSpec extends Specification {
-    def "function '#value' gets compiled"() {
-        given:
-            CompoundVariable.functions.put('__func', Func.class)
-            def parser = new FunctionParser()
-        when:
-            def result = parser.compileString(value)
-        then:
-            "$result" == "$expected"
-        where:
-            value           | expected
-            '${__func()}'   | [new Func()]
-            '${ __func()}'  | [new Func()]
-            '${__func() }'  | [new Func()]
-            '${ __func() }' | [new Func()]
+        Assertions.assertEquals(arrayListOf(Func()), result) {
+            "FunctionParser.compileString($input)"
+        }
     }
 
-    public static class Func implements Function {
-        void setParameters(Collection params) {
-            // do nothing
+    @AutoService(Function::class)
+    class Func : Function {
+        override fun execute(previousResult: SampleResult?, currentSampler: Sampler?): String {
+            TODO()
         }
-        String getReferenceKey() {
-            return "__func"
+
+        override fun setParameters(parameters: MutableCollection<CompoundVariable>) {
         }
-        List<String> getArgumentDesc() {
-            return Collections.emptyList()
+
+        override fun getReferenceKey(): String = "__func"
+
+        override fun getArgumentDesc(): List<String> = listOf()
+        override fun equals(other: Any?): Boolean {
+            if (this === other) return true
+            if (javaClass != other?.javaClass) return false
+            return true
         }
-        String execute(SampleResult result, Sampler sampler) {
-            return "done"
-        }
-        String toString() {
-            return "__func()"
+
+        override fun hashCode(): Int {
+            return javaClass.hashCode()
         }
     }
 }
diff --git a/src/core/src/test/kotlin/org/apache/jmeter/report/core/ConvertersTest.kt b/src/core/src/test/kotlin/org/apache/jmeter/report/core/ConvertersTest.kt
index 7ed9bcd..118a913 100644
--- a/src/core/src/test/kotlin/org/apache/jmeter/report/core/ConvertersTest.kt
+++ b/src/core/src/test/kotlin/org/apache/jmeter/report/core/ConvertersTest.kt
@@ -17,46 +17,57 @@
 
 package org.apache.jmeter.report.core
 
-import spock.lang.Specification
-import spock.lang.Unroll
+import org.junit.jupiter.api.Assertions.assertEquals
+import org.junit.jupiter.params.ParameterizedTest
+import org.junit.jupiter.params.provider.MethodSource
 
-@Unroll
-class ConvertersSpec extends Specification {
+class ConvertersTest {
+    data class NumberCase(val input: String, val type: Class<*>?, val expected: Number, val precision: Number)
+    data class SimpleCase(val input: String, val type: Class<*>?, val expected: Any)
+    companion object {
+        @JvmStatic
+        fun numberCases() = listOf(
+            NumberCase(" 42", Int::class.javaObjectType, Integer.valueOf(42), 0),
+            NumberCase("-5", Int::class.javaPrimitiveType, Integer.valueOf(-5), 0),
+            NumberCase("5000000", Long::class.javaObjectType, java.lang.Long.valueOf(5_000_000), 0),
+            NumberCase("500 ", Long::class.javaPrimitiveType, java.lang.Long.valueOf(500), 0),
+            NumberCase("3.14", Double::class.javaObjectType, java.lang.Double.valueOf(3.14), 0.0001),
+            NumberCase(" 100 ", Double::class.javaPrimitiveType, java.lang.Double.valueOf(100.0), 0.0001),
+            NumberCase("+5", Float::class.javaObjectType, java.lang.Float.valueOf(5.0f), 0.0001),
+            NumberCase(" 1.2E16 ", Float::class.javaPrimitiveType, java.lang.Float.valueOf(1.2E16f), 0.0001),
+        )
 
-    def "Convert number-like (#input) to instance of (#conversionDestClass)"() {
-        given:
-            def converter = Converters.getConverter(conversionDestClass)
-        when:
-            def result = converter.convert(input)
-        then:
-            result.class == expectedResult.class
-            result == expectedResult || Math.abs(expectedResult - result) < precision
-        where:
-            input      | conversionDestClass | expectedResult          | precision
-            " 42"      | Integer.class       | Integer.valueOf(42)     | 0
-            "-5"       | int.class           | Integer.valueOf(-5)     | 0
-            "5000000"  | Long.class          | Long.valueOf(5_000_000) | 0
-            "500 "     | long.class          | Long.valueOf(500)       | 0
-            "3.14"     | Double.class        | Double.valueOf(3.14)    | 0.0001
-            " 100 "    | double.class        | Double.valueOf(100)     | 0.0001
-            "+5"       | Float.class         | Float.valueOf(5)        | 0.0001
-            " 1.2E16 " | float.class         | Float.valueOf(1.2E16)   | 0.0001
+        @JvmStatic
+        fun simpleCases() = listOf(
+            SimpleCase("FALSE", Boolean::class.javaObjectType, false),
+            SimpleCase("true", Boolean::class.javaPrimitiveType, true),
+            SimpleCase(" true", Boolean::class.javaPrimitiveType, false),
+            SimpleCase("fAlSe ", Boolean::class.javaPrimitiveType, false),
+            SimpleCase("a", Char::class.javaObjectType, 'a'),
+            SimpleCase("ä", Char::class.javaPrimitiveType, 'ä'),
+        )
     }
 
-    def "Convert (#input) to instance of (#conversionDestClass)"() {
-        given:
-            def converter = Converters.getConverter(conversionDestClass)
-        when:
-            def result = converter.convert(input)
-        then:
-            result == expectedResult
-        where:
-            input    | conversionDestClass | expectedResult
-            "FALSE"  | Boolean.class       | Boolean.FALSE
-            "true"   | boolean.class       | Boolean.TRUE
-            " true"  | boolean.class       | Boolean.FALSE
-            "fAlSe " | boolean.class       | Boolean.FALSE
-            "a"      | Character.class     | 'a'
-            "ä"      | char.class          | 'ä'
+    @ParameterizedTest
+    @MethodSource("numberCases")
+    fun convertNumbers(case: NumberCase) {
+        val result = Converters.getConverter(case.type).convert(case.input)
+        if (result != case.expected) {
+            assertEquals(case.expected.toDouble(), (result as Number).toDouble(), case.precision.toDouble()) {
+                "Converters.getConverter(${case.type}).convert(${case.input}) should be within ${case.precision} of ${case.expected}"
+            }
+        }
+        assertEquals(case.expected::class.java, result::class.java) {
+            "type of Converters.getConverter(${case.type}).convert(${case.input})"
+        }
+    }
+
+    @ParameterizedTest
+    @MethodSource("simpleCases")
+    fun convertSimple(case: SimpleCase) {
+        val result = Converters.getConverter(case.type).convert(case.input)
+        assertEquals(case.expected, result) {
+            "Converters.getConverter(${case.type}).convert(${case.input})"
+        }
     }
 }
diff --git a/src/core/src/test/kotlin/org/apache/jmeter/report/core/SampleMetadataParserTest.kt b/src/core/src/test/kotlin/org/apache/jmeter/report/core/SampleMetadataParserTest.kt
index fe1ed72..6806ac6 100644
--- a/src/core/src/test/kotlin/org/apache/jmeter/report/core/SampleMetadataParserTest.kt
+++ b/src/core/src/test/kotlin/org/apache/jmeter/report/core/SampleMetadataParserTest.kt
@@ -17,42 +17,37 @@
 
 package org.apache.jmeter.report.core
 
-import spock.lang.Specification
-import spock.lang.Unroll
+import org.junit.jupiter.api.Assertions
+import org.junit.jupiter.params.ParameterizedTest
+import org.junit.jupiter.params.provider.MethodSource
 
-@Unroll
-class SampleMetadataParserSpec extends Specification {
-
-    def "Parse headers (#headers) with separator (#separator) and get (#expectedColumns)"() {
-        given:
-            def sut = new SampleMetaDataParser(separator as char)
-        when:
-            def columns = sut.parse(headers).columns
-        then:
-            columns == expectedColumns
-        where:
-            separator | headers           | expectedColumns
-            ';'       | "a;b;c;d;e"       | ["a", "b", "c", "d", "e"]
-            '|'       | "a|b|c|d|e"       | ["a", "b", "c", "d", "e"]
-            '|'       | "aa|bb|cc|dd|eef" | ["aa", "bb", "cc", "dd", "eef"]
-            '&'       | "a&b&c&d&e"       | ["a", "b", "c", "d", "e"]
-            '\t'      | "a\tb c\td\te"    | ["a", "b c", "d", "e"]
-            ','       | "abcdef"          | ["abcdef"]
+class SampleMetadataParserTest {
+    data class ParseCase(val separator: Char, val headers: String, val expected: List<String>)
+    companion object {
+        @JvmStatic
+        fun headerCases() = listOf(
+            ParseCase(';', "a;b;c;d;e", listOf("a", "b", "c", "d", "e")),
+            ParseCase(',', "a|b|c|d|e", listOf("a", "b", "c", "d", "e")),
+            ParseCase(',', "aa|bb|cc|dd|eef", listOf("aa", "bb", "cc", "dd", "eef")),
+            ParseCase('&', "a&b&c&d&e", listOf("a", "b", "c", "d", "e")),
+            ParseCase('\t', "a\tb c\td\te", listOf("a", "b c", "d", "e")),
+            ParseCase(',', "abcdef", listOf("abcdef")),
+            // Wrong separator
+            ParseCase(',', "a;b;c;d;e", listOf("a", "b", "c", "d", "e")),
+            ParseCase(',', "a|b|c|d|e", listOf("a", "b", "c", "d", "e")),
+            ParseCase(',', "aa|bb|cc|dd|eef", listOf("aa", "bb", "cc", "dd", "eef")),
+            ParseCase(',', "a&b&c&d&e", listOf("a", "b", "c", "d", "e")),
+            ParseCase(',', "a\tb c\td\te", listOf("a", "b c", "d", "e")),
+            ParseCase(',', "abcdef", listOf("abcdef")),
+        )
     }
 
-    def "Parse headers (#headers) with wrong separator (#separator) and get (#expectedColumns)"() {
-        given:
-        def sut = new SampleMetaDataParser(separator as char)
-        when:
-        def columns = sut.parse(headers).columns
-        then:
-        columns == expectedColumns
-        where:
-        separator | headers           | expectedColumns
-        ','       | "a;b;c;d;e"       | ["a", "b", "c", "d", "e"]
-        ','       | "a|b|c|d|e"       | ["a", "b", "c", "d", "e"]
-        ','       | "aa|bb|cc|dd|eef" | ["aa", "bb", "cc", "dd", "eef"]
-        ','       | "a&b&c&d&e"       | ["a", "b", "c", "d", "e"]
-        ','       | "a\tb c\td\te"    | ["a", "b c", "d", "e"]
+    @ParameterizedTest
+    @MethodSource("headerCases")
+    fun parseHeaders(case: ParseCase) {
+        val result = SampleMetaDataParser(case.separator).parse(case.headers).columns
+        Assertions.assertEquals(case.expected, result) {
+            "SampleMetaDataParser(${case.separator}).parse(${case.headers})"
+        }
     }
 }
diff --git a/src/core/src/test/kotlin/org/apache/jmeter/report/processor/ApdexSummaryConsumerTest.kt b/src/core/src/test/kotlin/org/apache/jmeter/report/processor/ApdexSummaryConsumerTest.kt
index 595085e..179003b 100644
--- a/src/core/src/test/kotlin/org/apache/jmeter/report/processor/ApdexSummaryConsumerTest.kt
+++ b/src/core/src/test/kotlin/org/apache/jmeter/report/processor/ApdexSummaryConsumerTest.kt
@@ -17,29 +17,32 @@
 
 package org.apache.jmeter.report.processor
 
-import spock.lang.Specification
+import org.junit.jupiter.api.Assertions.assertEquals
+import org.junit.jupiter.api.Test
 
-class ApdexSummaryConsumerSpec extends Specification {
+class ApdexSummaryConsumerTest {
+    val sut = ApdexSummaryConsumer()
 
-    def sut = new ApdexSummaryConsumer()
+    @Test
+    fun `createDataResult contains apdex, satisfied, tolerated, key`() {
+        val info = ApdexThresholdsInfo()
+        info.satisfiedThreshold = 3L
+        info.toleratedThreshold = 12L
+        val data = ApdexSummaryData(info).apply {
+            satisfiedCount = 60L
+            toleratedCount = 30L
+            totalCount = 100L
+        }
+        val expectedApdex = 0.75
 
-    def "createDataResult contains apdex, satisfied, tolerated, key"() {
-        given:
-            def info = new ApdexThresholdsInfo()
-            info.setSatisfiedThreshold(3L)
-            info.setToleratedThreshold(12L)
-            def data = new ApdexSummaryData(info)
-            data.satisfiedCount = 60L
-            data.toleratedCount = 30L
-            data.totalCount = 100L
-            def expectedApdex = 0.75
-        when:
-            def result = sut.createDataResult("key", data)
-        then:
-            def resultValues = result.asList().collect {
-                ((ValueResultData) it).value
-            }
-            // [apdex, satisfied, tolerated, key]
-            resultValues == [expectedApdex, 3L, 12L, "key"]
+        val result = sut.createDataResult("key", data)
+
+        val resultValues = result.map { (it as ValueResultData).value }
+        assertEquals(
+            listOf(expectedApdex, 3L, 12L, "key"),
+            resultValues
+        ) {
+            "ApdexSummaryConsumer().createDataResult(\"key\", $data) should yield [apdex, satisfied, tolerated, key]"
+        }
     }
 }
diff --git a/src/core/src/test/kotlin/org/apache/jmeter/report/processor/FieldSampleComparatorTest.kt b/src/core/src/test/kotlin/org/apache/jmeter/report/processor/FieldSampleComparatorTest.kt
index d645cf0..9346f8e 100644
--- a/src/core/src/test/kotlin/org/apache/jmeter/report/processor/FieldSampleComparatorTest.kt
+++ b/src/core/src/test/kotlin/org/apache/jmeter/report/processor/FieldSampleComparatorTest.kt
@@ -19,45 +19,52 @@
 
 import org.apache.jmeter.report.core.Sample
 import org.apache.jmeter.report.core.SampleMetadata
+import org.junit.jupiter.api.Assertions
+import org.junit.jupiter.api.Test
+import kotlin.math.sign
 
-import spock.lang.Specification
+class FieldSampleComparatorTest {
+    companion object {
+        const val separator = ','
+    }
+    private val multiColSampleMeta = SampleMetadata(separator, "col1", "col2")
 
-class FieldSampleComparatorSpec extends Specification {
-
-    static char separator = ',' as char
-    def multiColSampleMeta = new SampleMetadata(separator, "col1", "col2")
-
-    def testCompare() {
-        given:
-            def sampleMetadata = new SampleMetadata(separator, "col1")
-            def firstRow = new Sample(0, sampleMetadata, "1")
-            def secondRow = new Sample(1, sampleMetadata, "2")
-            def sut = new FieldSampleComparator("col1")
-            sut.initialize(sampleMetadata)
-        expect:
-            sut.compare(firstRow, secondRow) < 0
-            sut.compare(secondRow, firstRow) > 0
-            sut.compare(firstRow, firstRow) == 0
-            sut.compare(secondRow, secondRow) == 0
+    fun assertCompare(comparator: FieldSampleComparator, a: Sample, b: Sample, expectedSign: Int) {
+        Assertions.assertEquals(expectedSign.sign, comparator.compare(a, b).sign) {
+            "$comparator.compare($a, $b)"
+        }
     }
 
-    def "initialize ensures correct column is compared"() {
-        given:
-            def sut = new FieldSampleComparator("col2")
-            def firstRow = new Sample(0, multiColSampleMeta, "1", "3")
-            def secondRow = new Sample(1, multiColSampleMeta, "2", "3")
-            sut.initialize(multiColSampleMeta)
-        expect:
-            sut.compare(firstRow, secondRow) == 0
+    @Test
+    fun testCompare() {
+        val sampleMetadata = SampleMetadata(separator, "col1")
+        val firstRow = Sample(0, sampleMetadata, "1")
+        val secondRow = Sample(1, sampleMetadata, "2")
+        val sut = FieldSampleComparator("col1")
+        sut.initialize(sampleMetadata)
+
+        assertCompare(sut, firstRow, secondRow, -1)
+        assertCompare(sut, secondRow, firstRow, 1)
+        assertCompare(sut, firstRow, firstRow, 0)
+        assertCompare(sut, secondRow, secondRow, 0)
     }
 
-    def "Incorrectly uses first column if initialize isn't called"() {
-        given:
-            def sut = new FieldSampleComparator("col2")
-            def firstRow = new Sample(0, multiColSampleMeta, "1", "3")
-            def secondRow = new Sample(1, multiColSampleMeta, "2", "3")
-        expect:
-            sut.compare(firstRow, secondRow) != 0
+    @Test
+    fun `initialize ensures correct column is compared`() {
+        val sut = FieldSampleComparator("col2")
+        val firstRow = Sample(0, multiColSampleMeta, "1", "3")
+        val secondRow = Sample(1, multiColSampleMeta, "2", "3")
+        sut.initialize(multiColSampleMeta)
+
+        assertCompare(sut, firstRow, secondRow, 0)
     }
 
+    @Test
+    fun `Incorrectly uses first column if initialize isn't called`() {
+        val sut = FieldSampleComparator("col2")
+        val firstRow = Sample(0, multiColSampleMeta, "1", "3")
+        val secondRow = Sample(1, multiColSampleMeta, "2", "3")
+
+        assertCompare(sut, firstRow, secondRow, -1)
+    }
 }
diff --git a/src/core/src/test/kotlin/org/apache/jmeter/report/processor/ListResultDataTest.kt b/src/core/src/test/kotlin/org/apache/jmeter/report/processor/ListResultDataTest.kt
index 9f4e574..7dc6452 100644
--- a/src/core/src/test/kotlin/org/apache/jmeter/report/processor/ListResultDataTest.kt
+++ b/src/core/src/test/kotlin/org/apache/jmeter/report/processor/ListResultDataTest.kt
@@ -17,27 +17,36 @@
 
 package org.apache.jmeter.report.processor
 
-import spock.lang.Specification
-import spock.lang.Unroll
+import org.junit.jupiter.api.Assertions.assertEquals
+import org.junit.jupiter.api.Assertions.assertTrue
+import org.junit.jupiter.api.Test
+import org.junit.jupiter.params.ParameterizedTest
+import org.junit.jupiter.params.provider.MethodSource
 
-@Unroll
-class ListResultDataSpec extends Specification {
-
-    def sut = new ListResultData()
-
-    def "a new ListResultData is empty"() {
-        expect:
-            new ListResultData().size() == 0
+class ListResultDataTest {
+    companion object {
+        @JvmStatic
+        fun addResultInputs() = listOf(
+            null,
+            ValueResultData(),
+            ListResultData(),
+        )
     }
 
-    def "addResult adds #object to list and returns true"() {
-        when:
-            def result = sut.addResult(object)
-        then:
-            result
-            sut.getSize() == 1
-            sut.get(0) == object
-        where:
-            object << [null, Mock(ResultData), new ListResultData()]
+    val sut = ListResultData()
+
+    @Test
+    fun `a new ListResultData is empty`() {
+        assertEquals(0, sut.size)
+        assertEquals(listOf<Any>(), sut.toList())
+    }
+
+    @ParameterizedTest
+    @MethodSource("addResultInputs")
+    fun addResult(input: ResultData?) {
+        assertTrue(sut.addResult(input), "addResult should return true")
+        assertEquals(listOf(input), sut.toList()) {
+            "ListResultData().add($input).toList()"
+        }
     }
 }
diff --git a/src/core/src/test/kotlin/org/apache/jmeter/report/processor/Top5ErrorsBySamplerConsumerTest.kt b/src/core/src/test/kotlin/org/apache/jmeter/report/processor/Top5ErrorsBySamplerConsumerTest.kt
index f43c41d..f4284f9 100644
--- a/src/core/src/test/kotlin/org/apache/jmeter/report/processor/Top5ErrorsBySamplerConsumerTest.kt
+++ b/src/core/src/test/kotlin/org/apache/jmeter/report/processor/Top5ErrorsBySamplerConsumerTest.kt
@@ -17,62 +17,74 @@
 
 package org.apache.jmeter.report.processor
 
-
+import io.mockk.every
+import io.mockk.mockk
 import org.apache.jmeter.report.core.Sample
 import org.apache.jmeter.report.utils.MetricUtils
+import org.junit.jupiter.api.Assertions.assertEquals
+import org.junit.jupiter.api.Test
 
-import spock.lang.Specification
+class Top5ErrorsBySamplerConsumerTest {
+    val sut = Top5ErrorsBySamplerConsumer()
 
-class Top5ErrorsBySamplerConsumerSpec extends Specification {
+    @Test
+    fun `summary info data updated with non-controller passing sample`() {
+        val mockSummaryInfo = mockk<AbstractSummaryConsumer<Top5ErrorsSummaryData>.SummaryInfo> {
+            every { getData() } answers { callOriginal() }
+            every { setData(any()) } answers { callOriginal() }
+        }
+        val mockSample = mockk<Sample>(relaxed = true) {
+            every { success } returns true
+        }
+        sut.updateData(mockSummaryInfo, mockSample)
 
-    def sut = new Top5ErrorsBySamplerConsumer()
-
-    def "summary info data updated with non-controller passing sample"() {
-        given:
-            def mockSummaryInfo = Mock(AbstractSummaryConsumer.SummaryInfo)
-            def mockSample = Mock(Sample) {
-                getSuccess() >> true
-            }
-        when:
-            sut.updateData(mockSummaryInfo, mockSample)
-        then:
-            def data = (Top5ErrorsSummaryData) mockSummaryInfo.getData()
-            data.getTotal() == 1
+        val data = mockSummaryInfo.getData()
+        assertEquals(1, data.total, "data.total")
     }
 
-    def "summary info data updated with non-controller failing sample"() {
-        given:
-            def mockSummaryInfo = Mock(AbstractSummaryConsumer.SummaryInfo)
-            def mockSample = Mock(Sample) {
-                getResponseCode() >> "200"
-            }
-        when:
-            sut.updateData(mockSummaryInfo, mockSample)
-        then:
-            def data = (Top5ErrorsSummaryData) mockSummaryInfo.getData()
-            data.getTotal() == 1
-            data.getErrors() == 1
-            data.top5ErrorsMetrics[0][0] == MetricUtils.ASSERTION_FAILED
-            def overallData = (Top5ErrorsSummaryData) sut.getOverallInfo().getData()
-            overallData.getTotal() == 1
-            overallData.getErrors() == 1
-            overallData.top5ErrorsMetrics[0][0] == MetricUtils.ASSERTION_FAILED
+    @Test
+    fun `summary info data updated with non-controller failing sample`() {
+        val mockSummaryInfo = mockk<AbstractSummaryConsumer<Top5ErrorsSummaryData>.SummaryInfo> {
+            every { getData() } answers { callOriginal() }
+            every { setData(any()) } answers { callOriginal() }
+        }
+        val mockSample = mockk<Sample>(relaxed = true) {
+            every { responseCode } returns "200"
+        }
+        sut.updateData(mockSummaryInfo, mockSample)
+
+        val data = mockSummaryInfo.getData()
+        assertEquals(1, data.total, "data.total")
+        assertEquals(1, data.errors, "data.errors")
+        assertEquals(MetricUtils.ASSERTION_FAILED, data.top5ErrorsMetrics[0][0], "data.top5ErrorsMetrics[0][0]")
+
+        val overallData = sut.overallInfo.getData()
+        assertEquals(1, overallData.total, "data.total")
+        assertEquals(1, overallData.errors, "data.errors")
+        assertEquals(
+            MetricUtils.ASSERTION_FAILED,
+            overallData.top5ErrorsMetrics[0][0],
+            "overallData.top5ErrorsMetrics[0][0]"
+        )
     }
 
-    def "key from sample is name"() {
-        given:
-            def mockSample = Mock(Sample)
-        when:
-            def key = sut.getKeyFromSample(mockSample)
-        then:
-            1 * mockSample.getName() >> "name"
-            key == "name"
+    @Test
+    fun `key from sample is name`() {
+        val sample = mockk<Sample> {
+            every { name } returns "name"
+        }
+        assertEquals("name", sut.getKeyFromSample(sample)) {
+            "getKeyFromSample(sample) should return the name of the sample"
+        }
     }
 
-    def "there are 3 + 2n expected results title columns"() {
-        expect:
-            sut.createResultTitles().size ==
-                    3 + 2 * sut.MAX_NUMBER_OF_ERRORS_IN_TOP
+    @Test
+    fun `there are 3 + 2n expected results title columns`() {
+        assertEquals(
+            3 + 2 * Top5ErrorsBySamplerConsumer.MAX_NUMBER_OF_ERRORS_IN_TOP,
+            sut.createResultTitles().size
+        ) {
+            ".createResultTitles().size should be 3 + 2 * ${Top5ErrorsBySamplerConsumer.MAX_NUMBER_OF_ERRORS_IN_TOP}"
+        }
     }
-
 }
diff --git a/src/core/src/test/kotlin/org/apache/jmeter/report/processor/Top5ErrorsSummaryDataTest.kt b/src/core/src/test/kotlin/org/apache/jmeter/report/processor/Top5ErrorsSummaryDataTest.kt
index 3ffd87c..f47fb67 100644
--- a/src/core/src/test/kotlin/org/apache/jmeter/report/processor/Top5ErrorsSummaryDataTest.kt
+++ b/src/core/src/test/kotlin/org/apache/jmeter/report/processor/Top5ErrorsSummaryDataTest.kt
@@ -17,44 +17,59 @@
 
 package org.apache.jmeter.report.processor
 
-import spock.lang.Specification
+import org.junit.jupiter.api.Assertions.assertArrayEquals
+import org.junit.jupiter.api.Assertions.assertEquals
+import org.junit.jupiter.api.Test
 
-class Top5ErrorsSummaryDataSpec extends Specification {
+class Top5ErrorsSummaryDataTest {
+    val sut = Top5ErrorsSummaryData()
 
-    def sut = new Top5ErrorsSummaryData()
-
-    def "error and total count start at 0"() {
-        expect:
-            sut.getErrors() == 0
-            sut.getTotal() == 0
+    @Test
+    fun `error and total count start at 0`() {
+        assertEquals(0, sut.errors, "errors")
+        assertEquals(0, sut.total, "total")
     }
 
-    def "error and total count increment by one each time"() {
-        when:
-            sut.incErrors()
-            sut.incTotal()
-        then:
-            sut.getErrors() == 1
-            sut.getTotal() == 1
+    @Test
+    fun `incErrors increments errors`() {
+        sut.incErrors()
+        assertEquals(1, sut.errors, "errors")
     }
 
-    def "when no errors are registered an array with null values is returned"() {
-        expect:
-            sut.getTop5ErrorsMetrics() == new Object[0][0]
+    @Test
+    fun `incTotal increments total`() {
+        sut.incTotal()
+        assertEquals(1, sut.total, "total")
     }
 
-    def "error messages with the same frequency are preserved up until the size limit"() {
-        given:
-            ["A", "B", "C", "D", "E", "F"].each { sut.registerError(it) }
-        expect:
-            sut.getTop5ErrorsMetrics() == [["A", 1], ["B", 1], ["C", 1], ["D", 1], ["E", 1]]
+    @Test
+    fun `when no errors are registered an array with null values is returned`() {
+        assertArrayEquals(arrayOf<Array<Any>>(), sut.getTop5ErrorsMetrics(), "getTop5ErrorsMetrics")
     }
 
-    def "error messages are sorted by size, descending"() {
-        given:
-            ["A", "A", "A", "B", "B", "C"].each { sut.registerError(it) }
-        expect:
-            sut.getTop5ErrorsMetrics() == [["A", 3], ["B", 2], ["C", 1]]
+    @Test
+    fun `error messages with the same frequency are preserved up until the size limit`() {
+        val input = listOf("A", "B", "C", "D", "E", "F")
+        input.forEach { sut.registerError(it) }
+        assertArrayEquals(
+            input.take(5).map { arrayOf(it, 1L) }.toTypedArray(),
+            sut.getTop5ErrorsMetrics(),
+            "registerErrors $input, then call getTop5ErrorsMetrics"
+        )
     }
 
+    @Test
+    fun `error messages are sorted by size, descending`() {
+        val input = listOf("A", "A", "A", "B", "B", "C")
+        input.forEach { sut.registerError(it) }
+        assertArrayEquals(
+            arrayOf(
+                arrayOf("A", 3L),
+                arrayOf("B", 2L),
+                arrayOf("C", 1L)
+            ),
+            sut.top5ErrorsMetrics,
+            "registerErrors $input, then call getTop5ErrorsMetrics"
+        )
+    }
 }
diff --git a/src/core/src/test/kotlin/org/apache/jmeter/report/processor/graph/impl/ResponseTimePercentilesOverTimeGraphConsumerTest.kt b/src/core/src/test/kotlin/org/apache/jmeter/report/processor/graph/impl/ResponseTimePercentilesOverTimeGraphConsumerTest.kt
index 2036bf0..918764e 100644
--- a/src/core/src/test/kotlin/org/apache/jmeter/report/processor/graph/impl/ResponseTimePercentilesOverTimeGraphConsumerTest.kt
+++ b/src/core/src/test/kotlin/org/apache/jmeter/report/processor/graph/impl/ResponseTimePercentilesOverTimeGraphConsumerTest.kt
@@ -17,39 +17,37 @@
 
 package org.apache.jmeter.report.processor.graph.impl
 
-import java.util.stream.Collectors
+import org.junit.jupiter.api.Assertions.assertEquals
+import org.junit.jupiter.api.Assertions.assertFalse
+import org.junit.jupiter.api.Test
 
-import org.apache.jmeter.junit.spock.JMeterSpec
+class ResponseTimePercentilesOverTimeGraphConsumerTest {
+    val sut = ResponseTimePercentilesOverTimeGraphConsumer()
 
-class ResponseTimePercentilesOverTimeGraphConsumerSpec extends JMeterSpec {
+    @Test
+    fun `GroupInfos have only the required keys`() {
+        val groupInfosMap = sut.createGroupInfos()
 
-    static def EXPECTED_KEYS =
-            ['aggregate_report_min',
-             'aggregate_report_max',
-             'aggregate_rpt_pct1',
-             'aggregate_rpt_pct2',
-             'aggregate_rpt_pct3',
-            'aggregate_report_median'] as Set
-
-    def sut = new ResponseTimePercentilesOverTimeGraphConsumer()
-
-    def "GroupInfos have only the required keys"() {
-        when:
-            def groupInfosMap = sut.createGroupInfos()
-        then:
-            groupInfosMap.keySet() == EXPECTED_KEYS
+        assertEquals(
+            setOf(
+                "aggregate_report_min",
+                "aggregate_report_max",
+                "aggregate_rpt_pct1",
+                "aggregate_rpt_pct2",
+                "aggregate_rpt_pct3",
+                "aggregate_report_median",
+            ),
+            groupInfosMap.keys,
+            "createGroupInfos().keys"
+        )
     }
 
-    def "GroupInfos have the expected settings"() {
-        when:
-            def groupInfos = sut.createGroupInfos()
-            def groupInfoValues = groupInfos
-                    .entrySet().stream()
-                    .map { it.value }
-                    .collect(Collectors.toList())
-        then:
-            groupInfoValues.every { !it.enablesAggregatedKeysSeries() }
-            groupInfoValues.every { !it.enablesOverallSeries() }
+    @Test
+    fun `GroupInfos have the expected settings`() {
+        val groupInfos = sut.createGroupInfos()
+        groupInfos.values.forEach {
+            assertFalse(it.enablesAggregatedKeysSeries(), "enablesAggregatedKeysSeries()")
+            assertFalse(it.enablesOverallSeries(), "enablesOverallSeries()")
+        }
     }
-
 }
diff --git a/src/core/src/test/kotlin/org/apache/jmeter/services/FileServerTest.kt b/src/core/src/test/kotlin/org/apache/jmeter/services/FileServerTest.kt
index d02c2dc..c48e619 100644
--- a/src/core/src/test/kotlin/org/apache/jmeter/services/FileServerTest.kt
+++ b/src/core/src/test/kotlin/org/apache/jmeter/services/FileServerTest.kt
@@ -17,221 +17,249 @@
 
 package org.apache.jmeter.services
 
-import org.apache.jmeter.junit.spock.JMeterSpec
+import org.apache.jmeter.junit.JMeterTestCase
+import org.junit.jupiter.api.AfterEach
+import org.junit.jupiter.api.Assertions.assertArrayEquals
+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.junit.jupiter.api.assertThrows
+import org.junit.jupiter.api.fail
+import org.junit.jupiter.params.ParameterizedTest
+import org.junit.jupiter.params.provider.MethodSource
+import java.io.EOFException
+import java.io.File
+import java.io.IOException
 
-import spock.lang.Unroll
+class FileServerTest : JMeterTestCase() {
+    val sut = FileServer()
 
-@Unroll
-class FileServerSpec extends JMeterSpec {
+    val testFile = getResourceFilePath("testfiles/unit/FileServerSpec.csv")
+    val emptyFile = getResourceFilePath("testfiles/empty.csv")
+    val bomFile = getResourceFilePath("testfiles/bomData.csv")
 
-    def sut = new FileServer()
+    data class SetBaseForScriptCase(val file: File, val expectedBaseDirRelative: String)
 
-    def testFile = getResourceFilePath("testfiles/unit/FileServerSpec.csv")
-    def emptyFile = getResourceFilePath("testfiles/empty.csv")
-    def bomFile = getResourceFilePath("testfiles/bomData.csv")
+    companion object {
+        @JvmStatic
+        fun setBaseForScript(): List<SetBaseForScriptCase> {
+            val baseFile = File(FileServer.getDefaultBase())
+            return listOf(
+                SetBaseForScriptCase(baseFile, "."),
+                SetBaseForScriptCase(baseFile.getParentFile(), "."),
+                SetBaseForScriptCase(File(baseFile.getParentFile(), "abcd/defg.jmx"), "."),
+                SetBaseForScriptCase(File(baseFile, "abcd/defg.jmx"), "abcd"),
+            )
+        }
+    }
 
-    def setup() {
+    @BeforeEach
+    fun setup() {
         sut.resetBase()
     }
 
-    def tearDown() {
+    @AfterEach
+    fun tearDown() {
         sut.closeFiles()
     }
 
-    def "reading a non-existent file throws an exception"() {
-        when:
+    @Test
+    fun `reading a non-existent file throws an exception`() {
+        assertThrows<IOException> {
             sut.readLine("test")
-        then:
-            thrown(IOException)
+        }
     }
 
-    def "writing to a non-exisent file throws an exception"() {
-        when:
+    @Test
+    fun `writing to a non-exisent file throws an exception`() {
+        assertThrows<IOException> {
             sut.write("test", "")
-        then:
-            thrown(IOException)
+        }
     }
 
-    def "no files should be open following resetBase"() {
-        expect:
-            !sut.filesOpen()
+    @Test
+    fun `no files should be open following resetBase`() {
+        assertNoFilesOpen()
     }
 
-    def "closing unrecognised files are ignored"() {
-        when:
-            sut.closeFile("xxx")
-        then:
-            !sut.filesOpen()
-            noExceptionThrown()
+    @Test
+    fun `closing unrecognised files are ignored`() {
+        sut.closeFile("xxx")
+        assertNoFilesOpen()
     }
 
-    def "file is not opened until read from"() {
-        given:
-            sut.reserveFile(testFile) // Does not open file
-            assert !sut.filesOpen()
-        when:
-            def line = sut.readLine(testFile)
-        then:
-            line == "a1,b1,c1,d1"
-            sut.filesOpen()
+    @Test
+    fun `file is not opened until read from`() {
+        sut.reserveFile(testFile) // Does not open file
+        assertNoFilesOpen()
+        assertEquals("a1,b1,c1,d1", sut.readLine(testFile)) {
+            "readLine($testFile)"
+        }
+        assertFilesOpen()
     }
 
-    def "reading lines loops to start once last line is read"() {
-        given:
-            sut.reserveFile(testFile)
-        when:
-            def firstPass = [sut.readLine(testFile), sut.readLine(testFile), sut.readLine(testFile), sut.readLine(testFile)]
-            def secondPass = [sut.readLine(testFile), sut.readLine(testFile), sut.readLine(testFile), sut.readLine(testFile)]
-        then:
-            firstPass == secondPass
+    private fun assertNoFilesOpen() {
+        assertFalse(sut.filesOpen(), "filesOpen")
     }
 
-    def "cannot write to reserved file after reading"() {
-        given:
-            sut.reserveFile(testFile)
-            sut.readLine(testFile)
-        when:
+    private fun assertFilesOpen() {
+        assertTrue(sut.filesOpen(), "filesOpen")
+    }
+
+    @Test
+    fun `reading lines loops to start once last line is read`() {
+        sut.reserveFile(testFile)
+        val firstPass = Array(4) { sut.readLine(testFile) }
+        val secondPass = Array(4) { sut.readLine(testFile) }
+        assertArrayEquals(firstPass, secondPass)
+    }
+
+    @Test
+    fun `cannot write to reserved file after reading`() {
+        sut.reserveFile(testFile)
+        sut.readLine(testFile)
+        assertThrows<IOException> {
             sut.write(testFile, "")
-        then:
-            thrown(IOException)
+        }
     }
 
-    def "closing reserved file after reading resets"() {
-        given:
-            sut.reserveFile(testFile)
+    @Test
+    fun `closing reserved file after reading resets`() {
+        sut.reserveFile(testFile)
+        sut.readLine(testFile)
+        sut.closeFile(testFile) // does not remove the entry
+        assertNoFilesOpen()
+        assertEquals("a1,b1,c1,d1", sut.readLine(testFile), "re-read first line")
+        assertFilesOpen()
+    }
+
+    @Test
+    fun `closeFiles() prevents reading of reserved file`() {
+        sut.reserveFile(testFile)
+        sut.readLine(testFile)
+        sut.closeFiles() // removes all entries
+        assertThrows<IOException> {
             sut.readLine(testFile)
-        when:
-            sut.closeFile(testFile) // does not remove the entry
-        then:
-            !sut.filesOpen()
-            sut.readLine(testFile) == "a1,b1,c1,d1" // Re-read first line
-            sut.filesOpen()
+        }
+        assertNoFilesOpen()
     }
 
-    def "closeFiles() prevents reading of reserved file"() {
-        given:
-            sut.reserveFile(testFile)
-            sut.readLine(testFile)
-        when:
-            sut.closeFiles() // removes all entries
-            sut.readLine(testFile)
-        then:
-            !sut.filesOpen()
-            thrown(IOException)
+    @Test
+    fun `baseDir is the defaultBasedir`() {
+        assertEquals(FileServer.getDefaultBase(), sut.getBaseDir()) {
+            "getBaseDir should be FileServer.getDefaultBase()"
+        }
     }
 
-    def "baseDir is the defaultBasedir"() {
-        expect:
-            sut.getBaseDir() == FileServer.getDefaultBase()
-    }
+    @Test
+    fun `setBaseDir doesn't error when no files are open`() {
+        sut.setBasedir("testfiles/unit/FileServerSpec.csv")
 
-    def "setBaseDir doesn't error when no files are open"() {
-        when:
-            sut.setBasedir("testfiles/unit/FileServerSpec.csv")
-        then:
-            sut.getBaseDir().replaceAll("\\\\", "/").endsWith("testfiles/unit")
+        val result = sut.baseDir.replace("\\", "/")
+        if (!result.endsWith("testfiles/unit")) {
+            fail("baseDir should start with testfiles/unit, but was $result")
+        }
     }
 
     // TODO: what about throwing an exception in setBaseDir?
-    def "setBaseDir doesn't set base when passed a directory"() {
-        def dir = "does-not-exist"
-        given:
-            sut.setBasedir(dir)
-        when:
-            sut.getBaseDir().endsWith(dir)
-        then:
-            thrown(NullPointerException)
+    @Test
+    fun `setBaseDir doesn't set base when passed a directory`() {
+        val dir = "does-not-exist"
+        sut.setBasedir(dir)
+        assertThrows<NullPointerException> {
+            sut.baseDir.endsWith(dir)
+        }
     }
 
-    def "cannot set baseDir when files are open"() {
-        given:
-            sut.reserveFile(testFile)
-            sut.readLine(testFile) == "a1,b1,c1,d1"
-        when:
+    @Test
+    fun `cannot set baseDir when files are open`() {
+        sut.reserveFile(testFile)
+        assertEquals("a1,b1,c1,d1", sut.readLine(testFile), "sut.readLine($testFile)")
+        assertThrows<IllegalStateException> {
             sut.setBasedir("testfiles")
-        then:
-            thrown(IllegalStateException)
+        }
     }
 
-    static def baseFile = new File(FileServer.getDefaultBase())
-
-    def "setting base to #file gives getBaseDirRelative == #expectedBaseDirRelative"() {
-        when:
-            sut.setBaseForScript(file)
-        then:
-            sut.getBaseDirRelative().toString() == expectedBaseDirRelative
-        where:
-            file                                                | expectedBaseDirRelative
-            baseFile                                            | "."
-            baseFile.getParentFile()                            | "."
-            new File(baseFile.getParentFile(), "abcd/defg.jmx") | "."
-            new File(baseFile, "abcd/defg.jmx")                 | "abcd"
+    @ParameterizedTest
+    @MethodSource("setBaseForScript")
+    fun `setting base to #file gives getBaseDirRelative == #expectedBaseDirRelative`(case: SetBaseForScriptCase) {
+        sut.setBaseForScript(case.file)
+        assertEquals(case.expectedBaseDirRelative, sut.getBaseDirRelative().toString())
     }
 
-    def "non-existent filename to reserveFile will throw exception"() {
-        given:
-            def missing = "no-such-file"
-            def alias = "missing"
-            def charsetName = "UTF-8"
-            def hasHeader = true
-        when:
+    @Test
+    fun `non-existent filename to reserveFile will throw exception`() {
+        val missing = "no-such-file"
+        val alias = "missing"
+        val charsetName = "UTF-8"
+        val hasHeader = true
+        val ex = assertThrows<IllegalArgumentException> {
             sut.reserveFile(missing, charsetName, alias, hasHeader)
-        then:
-            def ex = thrown(IllegalArgumentException)
-            ex.getMessage() == "Could not read file header line for file $missing"
-            ex.getCause().getMessage() == "File $missing must exist and be readable"
+        }
+        assertEquals("Could not read file header line for file $missing", ex.message) {
+            "ex.message"
+        }
+        assertEquals("File $missing must exist and be readable", ex.cause?.message) {
+            "ex.cause?.message"
+        }
     }
 
-    def "reserving a file with no header will throw an exception if the header is expected"() {
-        given:
-            def alias = "empty"
-            def charsetName = "UTF-8"
-        when:
+    @Test
+    fun `reserving a file with no header will throw an exception if the header is expected`() {
+        val alias = "empty"
+        val charsetName = "UTF-8"
+        val e = assertThrows<IllegalArgumentException> {
             sut.reserveFile(emptyFile, charsetName, alias, true)
-        then:
-            def e = thrown(IllegalArgumentException)
-            e.getCause() instanceof EOFException
+        }
+        if (e.cause !is EOFException) {
+            fail("reserveFile(emptyFile) should throw IllegalArgumentException(cause=EOFException), got cause=${e.cause}")
+        }
     }
 
-    def "resolvedFile returns absolute and relative files"() {
-        given:
-            def testFile = new File(emptyFile)
-        expect:
-            // absolute
-            sut.getResolvedFile(testFile.getAbsolutePath())
-                    .getCanonicalFile() == testFile.getCanonicalFile()
-            // relative
-            sut.getResolvedFile(testFile.getParentFile().getPath() + "/../testfiles/empty.csv")
-                    .getCanonicalFile() == testFile.getCanonicalFile()
+    @Test
+    fun `resolvedFile returns absolute and relative files`() {
+        val testFile = File(emptyFile)
+        assertEquals(
+            testFile.getCanonicalFile(),
+            sut.getResolvedFile(testFile.absolutePath).getCanonicalFile(),
+            "sut.getResolvedFile(testFile.absolutePath)"
+        )
+        // relative
+        assertEquals(
+            testFile.getCanonicalFile(),
+            sut.getResolvedFile(testFile.getParentFile().path + "/../testfiles/empty.csv")
+                .getCanonicalFile(),
+            "sut.getResolvedFile(testFile.getParentFile().path + \"/../testfiles/empty.csv\")"
+        )
     }
 
-    def "resolvedFile returns relative files with BaseForScript set"() {
-        given:
-            def testFile = new File(emptyFile)
-        when:
-            sut.setBaseForScript(testFile)
-        then:
-            sut.getResolvedFile(testFile.getName())
-                    .getCanonicalFile() == testFile.getCanonicalFile()
+    @Test
+    fun `resolvedFile returns relative files with BaseForScript set`() {
+        val testFile = File(emptyFile)
+        sut.setBaseForScript(testFile)
+        assertEquals(
+            testFile.getCanonicalFile(),
+            sut.getResolvedFile(testFile.getName()).getCanonicalFile()
+        ) {
+            ".getResolvedFile(${testFile.name}).getCanonicalFile()"
+        }
     }
 
-    def "skip bom at start of file and set correct encoding"() {
-        given:
-            sut.reserveFile(bomFile)
-        when:
-            def header = sut.readLine(bomFile)
-        then:
-            header == '"äöü"'
+    @Test
+    fun `skip bom at start of file and set correct encoding`() {
+        sut.reserveFile(bomFile)
+        val header = sut.readLine(bomFile)
+        assertEquals("\"äöü\"", header)
     }
 
-    def "fail to read a line from a directory"() {
-        given:
-            def directory = new File(bomFile).parent
-            sut.reserveFile(directory)
-        when:
+    @Test
+    fun `fail to read a line from a directory`() {
+        val directory = File(bomFile).parent
+        sut.reserveFile(directory)
+        assertThrows<IllegalArgumentException> {
             sut.readLine(directory)
-        then:
-            thrown(IllegalArgumentException)
+        }
     }
-
 }
diff --git a/src/core/src/test/kotlin/org/apache/jmeter/testelement/AbstractTestElementTest.kt b/src/core/src/test/kotlin/org/apache/jmeter/testelement/AbstractTestElementTest.kt
index 10347c2..61e0e26 100644
--- a/src/core/src/test/kotlin/org/apache/jmeter/testelement/AbstractTestElementTest.kt
+++ b/src/core/src/test/kotlin/org/apache/jmeter/testelement/AbstractTestElementTest.kt
@@ -17,7 +17,13 @@
 
 package org.apache.jmeter.testelement
 
+import io.mockk.mockk
+import io.mockk.spyk
+import org.apache.jmeter.testelement.property.CollectionProperty
+import org.apache.jmeter.testelement.property.TestElementProperty
 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.Test
 
 class AbstractTestElementTest {
@@ -50,4 +56,40 @@
                 "Note that comments is added in constructor, however, we remove <<comments>> property before cloning"
         }
     }
+
+    @Test
+    fun `set outer properties as temporary when using a TestElementProperty`() {
+        val sut = spyk<AbstractTestElement>()
+        val outerElement = mockk<TestElement>(relaxed = true)
+        val innerElement = mockk<TestElement>(relaxed = true)
+        val outerProp = TestElementProperty("outerProp", outerElement)
+        val innerProp = TestElementProperty("innerProp", innerElement)
+        outerProp.addProperty(innerProp)
+
+        sut.setTemporary(outerProp)
+
+        assertTrue(sut.isTemporary(outerProp)) {
+            "isTemporary($outerProp)"
+        }
+        assertFalse(sut.isTemporary(innerProp)) {
+            "isTemporary($innerProp)"
+        }
+    }
+
+    @Test
+    fun `set all properties as temporary when using a MultiProperty`() {
+        val sut = spyk<AbstractTestElement>()
+        val outerProp = CollectionProperty()
+        val innerProp = CollectionProperty()
+
+        outerProp.addProperty(innerProp)
+        sut.setTemporary(outerProp)
+
+        assertTrue(sut.isTemporary(outerProp)) {
+            "isTemporary($outerProp)"
+        }
+        assertTrue(sut.isTemporary(innerProp)) {
+            "isTemporary($innerProp)"
+        }
+    }
 }
diff --git a/src/core/src/test/kotlin/org/apache/jmeter/util/keystore/JmeterKeyStoreTest.kt b/src/core/src/test/kotlin/org/apache/jmeter/util/keystore/JmeterKeyStoreTest.kt
index 0ca8fe8..5f906cf 100644
--- a/src/core/src/test/kotlin/org/apache/jmeter/util/keystore/JmeterKeyStoreTest.kt
+++ b/src/core/src/test/kotlin/org/apache/jmeter/util/keystore/JmeterKeyStoreTest.kt
@@ -17,24 +17,29 @@
 
 package org.apache.jmeter.util.keystore
 
+import org.junit.jupiter.api.assertThrows
+import org.junit.jupiter.params.ParameterizedTest
+import org.junit.jupiter.params.provider.MethodSource
 import java.security.KeyStore
 
-import spock.lang.Specification
-import spock.lang.Unroll
-
-@Unroll
-class JmeterKeyStoreSpec extends Specification {
-
-    def "IllegalArgumentException expected when (#startIndex, #endIndex) startIndex < 0, endIndex < -1, or endIndex < startIndex"() {
-        when:
-            JmeterKeyStore.getInstance(KeyStore.defaultType, startIndex, endIndex, "defaultName")
-        then:
-            thrown IllegalArgumentException
-        where:
-            startIndex | endIndex
-            -1         | 0
-            0          | -2 // -1 indicates to return the first alias only
-            1          | 0
+class JmeterKeyStoreTest {
+    data class Case(val startIndex: Int, val endIndex: Int, val message: String? = null)
+    companion object {
+        @JvmStatic
+        fun inputs() = listOf(
+            Case(-1, 0),
+            Case(0, -2, "-1 indicates to return the first alias only"),
+            Case(1, 0),
+        )
     }
 
+    @ParameterizedTest
+    @MethodSource("inputs")
+    fun `throws IllegalArgumentException`(case: Case) {
+        assertThrows<IllegalArgumentException> {
+            JmeterKeyStore.getInstance(
+                KeyStore.getDefaultType(), case.startIndex, case.endIndex, "defaultName"
+            )
+        }
+    }
 }