Skip to content

Commit 4d811cb

Browse files
Sample usage of @TableTest.
1 parent 92d7ae6 commit 4d811cb

File tree

5 files changed

+194
-70
lines changed

5 files changed

+194
-70
lines changed

.claude/skills/migrate-groovy-to-java/SKILL.md

Lines changed: 27 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,12 +13,37 @@ Migrate test Groovy files to Java using JUnit 5
1313

1414
When converting Groovy code to Java code, make sure that:
1515
- The Java code generated is compatible with JDK 8
16-
- When translating Spock tests, favor using `@CsvSource` with `|` delimiters
17-
- When using `@MethodSource`, name the arguments method by appending `Arguments` using camelCase to the test method name (e.g. `testMethodArguments`) and return a `Stream` of arguments using `Stream.of(...)` and `arguments(...)` with static import.
16+
- When translating Spock tests, favor using @TableTest. See detailed instructions in the "TableTest Usage" section of this document.
17+
- When usage of `@TableTest` impossible, use `@MethodSource`, and name the arguments method by appending `Arguments` using camelCase to the test method name (e.g. `testMethodArguments`) and return a `Stream` of arguments using `Stream.of(...)` and `arguments(...)` with static import.
1818
- Ensure parameterized test names are human-readable (i.e. no hashcodes); instead add a description string as the first `Arguments.arguments(...)` value or index the test case
1919
- When converting tuples, create a light dedicated structure instead to keep the typing system
2020
- Instead of checking a state and throwing an exception, use JUnit asserts
2121
- Do not wrap checked exceptions and throw a Runtime exception; prefer adding a throws clause at method declaration
2222
- Do not mark local variables `final`
2323
- Ensure variables are human-readable; avoid single-letter names and pre-define variables that are referenced multiple times
2424
- When translating Spock `Mock(...)` usage, use `libs.bundles.mockito` instead of writing manual recording/stub implementations
25+
26+
TableTest usage
27+
Dependency, if missing add:
28+
- Groovy: testImplementation libs.tabletest
29+
- Kotlin: testImplementation(libs.tabletest)
30+
31+
Import: `import org.tabletest.junit.TableTest;`
32+
33+
JDK 8 rules:
34+
- No text blocks.
35+
- @TableTest must use String[] annotation array syntax: `@TableTest({ "a | b", "1 | 2" })`
36+
37+
Spock `where:`@TableTest:
38+
- First row = header (column names = method parameters).
39+
- Add `scenario` column as first column (display name, not a method parameter).
40+
- Use `|` delimiter; align columns so pipes line up vertically.
41+
- Prefer single quotes for strings with special chars (e.g., `'a|b'`, `'[]'`).
42+
- Blank cell = null (object types); `''` = empty string.
43+
- Collections: `[a, b]` = List/array, `{a, b}` = Set, `[k: v]` = Map.
44+
45+
Mixed eligibility:
46+
- Simple rows ⇒ @TableTest; complex rows (mocks, builders) ⇒ separate `@Test`(s) or small `@MethodSource`.
47+
48+
Do NOT use @TableTest when:
49+
- Majority of rows require complex objects or custom converters.
Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
---
2+
name: use-tabletest
3+
description: Convert JUnit 5 @MethodSource/@CsvSource/@ValueSource parameterized tests to @TableTest (JDK8)
4+
---
5+
Goal: Migrate JUnit 5 parameterized tests using @MethodSource/@CsvSource/@ValueSource to @TableTest with minimal churn and passing tests.
6+
7+
Process (do in this order):
8+
1) Locate targets via Grep (no agent subprocess). Search for: "@ParameterizedTest", "@MethodSource", "@CsvSource", "@ValueSource".
9+
2) Read all matching files up front (parallel is OK).
10+
3) Convert eligible tests to @TableTest.
11+
4) Write each modified file once in full using Write (no incremental per-test edits).
12+
5) Run module tests once and verify "BUILD SUCCESSFUL". If failed, inspect JUnit XML report.
13+
14+
Dependency:
15+
- If missing, add:
16+
- Groovy: testImplementation libs.tabletest
17+
- Kotlin: testImplementation(libs.tabletest)
18+
19+
Import:
20+
- Ensure: import org.tabletest.junit.TableTest;
21+
22+
JDK 8 rules:
23+
- No text blocks.
24+
- @TableTest must use String[] annotation array syntax:
25+
@TableTest({ "a | b", "1 | 2" })
26+
27+
Table formatting rules (mandatory):
28+
- Always include a header row (parameter names).
29+
- Always add a "scenario" column; using common sense for naming; scenario is NOT a method parameter.
30+
- Use '|' as delimiter.
31+
- Align columns with spaces so pipes line up vertically.
32+
- Prefer single quotes for strings requiring quotes (e.g., 'a|b', '[]', '{}', ' ').
33+
34+
Conversions:
35+
A) @CsvSource
36+
- Remove @ParameterizedTest and @CsvSource.
37+
- If delimiter is '|': rows map directly to @TableTest.
38+
- If delimiter is ',' (default): replace ',' with '|' in rows.
39+
40+
B) @ValueSource
41+
- Convert to @TableTest with header from parameter name.
42+
- Each value becomes one row.
43+
- Add "scenario" column using common sense for name.
44+
45+
C) @MethodSource (convert only if values are representable as strings)
46+
- Convert when argument values are primitives, strings, enums, booleans, nulls, and simple collection literals supported by TableTest:
47+
- Array: [a, b, ...]
48+
- List: [a, b, ...]
49+
- Set: {a, b, ...}
50+
- Map: [k: v, ...]
51+
- Blank cell = null (non-primitive).
52+
- '' = empty string.
53+
- For String params that start with '[' or '{', quote to avoid collection parsing (prefer '[]'/'{}').
54+
55+
Scenario handling:
56+
- If MethodSource includes a leading description string OR @ParameterizedTest(name=...) uses {0}, convert that to a scenario column and remove that parameter from method signature.
57+
58+
Cleanup:
59+
- Delete now-unused @MethodSource provider methods and unused imports.
60+
61+
Mixed eligibility:
62+
- If only a few cases need complex construction, split:
63+
- Simple cases ⇒ @TableTest
64+
- Complex cases ⇒ separate @Test(s) (descriptive names) OR keep a small @MethodSource.
65+
66+
Do NOT convert when:
67+
- Most rows require complex builders/mocks.
68+
- Parameters are arrays (String[], int[]) — keep @MethodSource (or refactor to List to convert).
69+
70+
Test command (exact):
71+
./gradlew :path:to:module:test --rerun-tasks 2>&1 | tail -20
72+
- If BUILD FAILED: cat path/to/module/build/test-results/test/TEST-*.xml
73+
74+
Never:
75+
- --info
76+
- extra gradle runs just to “confirm”

components/json/build.gradle.kts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,3 +7,7 @@ apply(from = "$rootDir/gradle/java.gradle")
77
jmh {
88
jmhVersion = libs.versions.jmh.get()
99
}
10+
11+
dependencies {
12+
testImplementation(libs.tabletest)
13+
}

components/json/src/test/java/datadog/json/JsonMapperTest.java

Lines changed: 85 additions & 68 deletions
Original file line numberDiff line numberDiff line change
@@ -10,24 +10,63 @@
1010
import static org.junit.jupiter.params.provider.Arguments.arguments;
1111

1212
import java.io.IOException;
13-
import java.util.ArrayList;
1413
import java.util.Arrays;
15-
import java.util.HashMap;
1614
import java.util.LinkedHashMap;
1715
import java.util.List;
1816
import java.util.Map;
1917
import java.util.stream.Stream;
18+
import org.junit.jupiter.api.Test;
2019
import org.junit.jupiter.params.ParameterizedTest;
2120
import org.junit.jupiter.params.provider.Arguments;
2221
import org.junit.jupiter.params.provider.MethodSource;
23-
import org.junit.jupiter.params.provider.ValueSource;
22+
import org.tabletest.junit.TableTest;
2423

2524
class JsonMapperTest {
25+
@TableTest({
26+
"scenario | input | expected ",
27+
"null input | | '{}' ",
28+
"empty map | [:] | '{}' ",
29+
"single entry | [key1: value1] | '{\"key1\":\"value1\"}' ",
30+
"two entries | [key1: value1, key2: value2] | '{\"key1\":\"value1\",\"key2\":\"value2\"}' "
31+
})
32+
void testMappingToJsonObject(Map<String, Object> input, String expected) throws IOException {
33+
assertMapToJsonRoundTrip(input, expected);
34+
}
35+
36+
@Test
37+
void testMappingToJsonObjectWithComplexMap() throws IOException {
38+
Map<String, Object> input = new LinkedHashMap<>();
39+
input.put("key1", null);
40+
input.put("key2", "bar");
41+
input.put("key3", 3);
42+
input.put("key4", 3456789123L);
43+
input.put("key5", 3.142f);
44+
input.put("key6", Math.PI);
45+
input.put("key7", true);
46+
47+
assertMapToJsonRoundTrip(
48+
input,
49+
"{\"key1\":null,\"key2\":\"bar\",\"key3\":3,\"key4\":3456789123,\"key5\":3.142,\"key6\":3.141592653589793,\"key7\":true}");
50+
}
51+
52+
@Test
53+
void testMappingToJsonObjectWithQuotedEntries() throws IOException {
54+
Map<String, Object> input = new LinkedHashMap<>();
55+
input.put("key1", "va\"lu\"e1");
56+
input.put("ke\"y2", "value2");
57+
58+
assertMapToJsonRoundTrip(input, "{\"key1\":\"va\\\"lu\\\"e1\",\"ke\\\"y2\":\"value2\"}");
59+
}
60+
61+
@Test
62+
void testMappingToJsonObjectWithUnsupportedType() throws IOException {
63+
Map<String, Object> input = new LinkedHashMap<>();
64+
input.put("key1", new UnsupportedType());
2665

27-
@ParameterizedTest(name = "test mapping to JSON object: {0}")
28-
@MethodSource("testMappingToJsonObjectArguments")
29-
void testMappingToJsonObject(
30-
@SuppressWarnings("unused") String testCase, Map<String, Object> input, String expected)
66+
assertMapToJsonRoundTrip(input, "{\"key1\":\"toString\"}");
67+
}
68+
69+
private void assertMapToJsonRoundTrip(Map<String, Object> input, String expected)
3170
throws IOException {
3271
String json = JsonMapper.toJson(input);
3372
assertEquals(expected, json);
@@ -54,62 +93,37 @@ void testMappingToJsonObject(
5493
}
5594
}
5695

57-
static Stream<Arguments> testMappingToJsonObjectArguments() {
58-
Map<String, Object> singleEntry = new LinkedHashMap<>();
59-
singleEntry.put("key1", "value1");
60-
61-
Map<String, Object> twoEntries = new LinkedHashMap<>();
62-
twoEntries.put("key1", "value1");
63-
twoEntries.put("key2", "value2");
64-
65-
Map<String, Object> quotedEntries = new LinkedHashMap<>();
66-
quotedEntries.put("key1", "va\"lu\"e1");
67-
quotedEntries.put("ke\"y2", "value2");
68-
69-
Map<String, Object> complexMap = new LinkedHashMap<>();
70-
complexMap.put("key1", null);
71-
complexMap.put("key2", "bar");
72-
complexMap.put("key3", 3);
73-
complexMap.put("key4", 3456789123L);
74-
complexMap.put("key5", 3.142f);
75-
complexMap.put("key6", Math.PI);
76-
complexMap.put("key7", true);
77-
complexMap.put("key8", new UnsupportedType());
78-
79-
return Stream.of(
80-
arguments("null input", null, "{}"),
81-
arguments("empty map", new HashMap<>(), "{}"),
82-
arguments("single entry", singleEntry, "{\"key1\":\"value1\"}"),
83-
arguments("two entries", twoEntries, "{\"key1\":\"value1\",\"key2\":\"value2\"}"),
84-
arguments(
85-
"quoted entries",
86-
quotedEntries,
87-
"{\"key1\":\"va\\\"lu\\\"e1\",\"ke\\\"y2\":\"value2\"}"),
88-
arguments(
89-
"complex map",
90-
complexMap,
91-
"{\"key1\":null,\"key2\":\"bar\",\"key3\":3,\"key4\":3456789123,\"key5\":3.142,\"key6\":3.141592653589793,\"key7\":true,\"key8\":\"toString\"}"));
92-
}
93-
94-
@ParameterizedTest(name = "test mapping to Map from empty JSON object: {0}")
95-
@MethodSource("testMappingToMapFromEmptyJsonObjectArguments")
96+
@TableTest({
97+
"scenario | json ",
98+
"null | ",
99+
"null string | 'null' ",
100+
"empty string | '' ",
101+
"empty object | '{}' "
102+
})
96103
void testMappingToMapFromEmptyJsonObject(String json) throws IOException {
97104
Map<String, Object> parsed = JsonMapper.fromJsonToMap(json);
98105
assertEquals(emptyMap(), parsed);
99106
}
100107

101-
static Stream<Arguments> testMappingToMapFromEmptyJsonObjectArguments() {
102-
return Stream.of(arguments((Object) null), arguments("null"), arguments(""), arguments("{}"));
103-
}
104-
105-
@ParameterizedTest(name = "test mapping to Map from non-object JSON: {0}")
106-
@ValueSource(strings = {"1", "[1, 2]"})
108+
// temporary disable spotless, will open issue to fix this.
109+
// spotless:off
110+
@TableTest({
111+
"scenario | json ",
112+
"integer | 1 ",
113+
"array | '[1, 2]' "
114+
})
115+
// spotless:on
107116
void testMappingToMapFromNonObjectJson(String json) {
108117
assertThrows(IOException.class, () -> JsonMapper.fromJsonToMap(json));
109118
}
110119

111-
@ParameterizedTest(name = "test mapping iterable to JSON array: {0}")
112-
@MethodSource("testMappingIterableToJsonArrayArguments")
120+
@TableTest({
121+
"scenario | input | expected ",
122+
"null input | | '[]' ",
123+
"empty list | [] | '[]' ",
124+
"single value | [value1] | '[\"value1\"]' ",
125+
"two values | [value1, value2] | '[\"value1\",\"value2\"]' "
126+
})
113127
void testMappingIterableToJsonArray(List<String> input, String expected) throws IOException {
114128
String json = JsonMapper.toJson(input);
115129
assertEquals(expected, json);
@@ -118,13 +132,14 @@ void testMappingIterableToJsonArray(List<String> input, String expected) throws
118132
assertEquals(input != null ? input : emptyList(), parsed);
119133
}
120134

121-
static Stream<Arguments> testMappingIterableToJsonArrayArguments() {
122-
return Stream.of(
123-
arguments(null, "[]"),
124-
arguments(new ArrayList<>(), "[]"),
125-
arguments(Arrays.asList("value1"), "[\"value1\"]"),
126-
arguments(Arrays.asList("value1", "value2"), "[\"value1\",\"value2\"]"),
127-
arguments(Arrays.asList("va\"lu\"e1", "value2"), "[\"va\\\"lu\\\"e1\",\"value2\"]"));
135+
@Test
136+
void testMappingIterableToJsonArrayWithQuotedValue() throws IOException {
137+
List<String> input = Arrays.asList("va\"lu\"e1", "value2");
138+
String json = JsonMapper.toJson(input);
139+
assertEquals("[\"va\\\"lu\\\"e1\",\"value2\"]", json);
140+
141+
List<String> parsed = JsonMapper.fromJsonToList(json);
142+
assertEquals(input, parsed);
128143
}
129144

130145
@ParameterizedTest(name = "test mapping array to JSON array: {0}")
@@ -150,17 +165,19 @@ static Stream<Arguments> testMappingArrayToJsonArrayArguments() {
150165
"[\"va\\\"lu\\\"e1\",\"value2\"]"));
151166
}
152167

153-
@ParameterizedTest(name = "test mapping to List from empty JSON object: {0}")
154-
@MethodSource("testMappingToListFromEmptyJsonObjectArguments")
168+
@TableTest({
169+
"scenario | json ",
170+
"null | ",
171+
"null string | 'null' ",
172+
"empty string | '' ",
173+
"empty array | '[]' "
174+
})
155175
void testMappingToListFromEmptyJsonObject(String json) throws IOException {
156176
List<String> parsed = JsonMapper.fromJsonToList(json);
157177
assertEquals(emptyList(), parsed);
158178
}
159179

160-
static Stream<Arguments> testMappingToListFromEmptyJsonObjectArguments() {
161-
return Stream.of(arguments((Object) null), arguments("null"), arguments(""), arguments("[]"));
162-
}
163-
180+
// Using `@MethodSource` as special chars not supported by `@TableTest` (yet?).
164181
@ParameterizedTest(name = "test mapping to JSON string: {0}")
165182
@MethodSource("testMappingToJsonStringArguments")
166183
void testMappingToJsonString(String input, String expected) {
@@ -170,7 +187,7 @@ void testMappingToJsonString(String input, String expected) {
170187

171188
static Stream<Arguments> testMappingToJsonStringArguments() {
172189
return Stream.of(
173-
arguments((Object) null, ""),
190+
arguments(null, ""),
174191
arguments("", ""),
175192
arguments(String.valueOf((char) 4096), "\"\\u1000\""),
176193
arguments(String.valueOf((char) 256), "\"\\u0100\""),

gradle/libs.versions.toml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,7 @@ junit5 = "5.14.1"
7070
junit-platform = "1.14.1"
7171
mockito = "4.4.0"
7272
spock = "2.4-groovy-3.0"
73+
tabletest = "1.2.0"
7374
testcontainers = "1.21.4"
7475

7576
[libraries]
@@ -161,6 +162,7 @@ objenesis = { module = "org.objenesis:objenesis", version = "3.3" } # Used by Sp
161162
spock-core = { module = "org.spockframework:spock-core", version.ref = "spock" }
162163
spock-junit4 = { module = "org.spockframework:spock-junit4", version.ref = "spock" }
163164
spock-spring = { module = "org.spockframework:spock-spring", version.ref = "spock" }
165+
tabletest = { module = "org.tabletest:tabletest-junit", version.ref = "tabletest" }
164166
testcontainers = { module = "org.testcontainers:testcontainers", version.ref = "testcontainers" }
165167
testcontainers-localstack = { module = "org.testcontainers:localstack", version.ref = "testcontainers" }
166168

0 commit comments

Comments
 (0)