From tabletest
Use when writing or converting JUnit tests in Java or Kotlin with the TableTest library. Trigger whenever the user wants to test multiple scenarios with the same assertion logic, convert repetitive @Test methods into a table, or start a new @TableTest. Also use when the user asks about TableTest syntax, column design, type converters, or value sets — even if they don't say "TableTest" explicitly.
How this skill is triggered — by the user, by Claude, or both
Slash command
/tabletest:tabletestThe summary Claude sees in its skill listing — used to decide when to auto-load this skill
Use this skill before converting similar JUnit tests or adding a new TableTest.
references/async-and-performance.mdreferences/column-design.mdreferences/common-patterns.mdreferences/consolidating-tests.mdreferences/dependency-setup.mdreferences/example-patterns.mdreferences/incremental-development.mdreferences/large-tables.mdreferences/pair-programming.mdreferences/provided-parameters.mdreferences/table-design-advanced.mdreferences/testing-reveals-bugs.mdreferences/type-converters.mdreferences/value-sets.mdscripts/format-table.shUse this skill before converting similar JUnit tests or adding a new TableTest.
Before writing any TableTest code, verify two things:
Dependencies: Check pom.xml/build.gradle for org.tabletest:tabletest-junit and a JUnit Jupiter version of 5.11 or higher. The TableTest groupId and artifactId are non-obvious and easy to get wrong from memory — if they're missing, read references/dependency-setup.md for the exact coordinates before adding anything.
Test shape: TableTest shines when 2+ test cases share the same setup and assertion logic and their differences can be expressed as data (inputs/outputs). A single-scenario @TableTest is also fine when it's part of a set of focused, single-responsibility tables (e.g., one table per syntactic feature of a parser) — the benefit is structural consistency and easy row addition later.
Stick with standard @Test methods when:
@TableTest("""
Scenario | a | b | Sum?
positive numbers | 1 | 2 | 3
with zero | 0 | 5 | 5
negative number | -3 | 7 | 4
""")
void shouldAddNumbers(int a, int b, int sum) {
assertEquals(sum, Calculator.add(a, b));
}
Notes:
\t, \n, etc. Kotlin raw strings do NOT — they remain literal. For special characters in Kotlin, use actual characters or regular strings.Keep a scenario column as the leftmost column. Do not map it to a parameter unless you annotate the parameter with @Scenario (needed for referencing scenario description in test method, or when using inject parameters). Suffix expectation columns with ? to signal intent (e.g., Expected?, Valid?).
Rules:
Use blank cells for null (reference types). Use '' for empty strings. Use ' ' for blank strings.
| Value contains or starts with | Action |
|---|---|
| (pipe) | Quote with "..." |
" or ' | Quote with the other quote style |
Starts with [ | Quote to avoid list syntax |
Starts with { | Quote to avoid set syntax |
@TableTest("""
Value | Description
simple | No quotes needed
"contains | pipe" | Quotes required for special chars
'' | Empty string
| Blank cell = null
"[1,2,3]" | Quote to avoid list syntax
"{a,b}" | Quote to avoid set syntax
""")
void testValues(String value, String description) { ... }
Strategy: Apply minimal quoting. Start without quotes; if a test fails with a parsing error, add quotes only around the problematic value. Over-quoting obscures the data.
Quote inside collection values, not the whole collection. For a collection element containing a special character, quote only that element: [path: 'C:\\Users'], not '[path: C:\\Users]'. The quotes wrap the problematic element, not the entire collection.
Newlines in values: To include a newline character inside a table value, write \\n in the table (keeps the row on one line), then process it manually in the test method: value.replace("\\n", "\n"). Do not use a literal newline — it would split the row across lines. Note: Java text blocks process \n into a real newline before TableTest sees it, so use double-backslash \\n to preserve it as text for manual processing.
Lists use [], sets use {}, and maps use [] with key: value entries.
// List (empty list uses [])
@TableTest("""
Numbers | Sum?
[] | 0
[1] | 1
[1, 2, 3] | 6
""")
void testSum(List<Integer> numbers, int sum) { ... }
// Set (empty set uses {})
@TableTest("""
Values | Size?
{} | 0
{1, 2, 3} | 3
{1, 1, 2, 2} | 2
""")
void testSetSize(Set<Integer> values, int size) { ... }
// Map (empty map uses [:])
@TableTest("""
Scores | Highest?
[:] | 0
[Alice: 95, Bob: 87] | 95
[x: 1, y: 2, z: 3] | 3
""")
void testHighestScore(Map<String, Integer> scores, int highest) { ... }
Common mistake: Using [] for a Set<> parameter. Lists use []; sets use {}. If a parameter is typed Set<T> but the table uses [], JUnit will report a conversion failure. Double-check the brackets match the parameter type.
Note: Empty collections are explicit: [] for empty list, {} for empty set, [:] for empty map.
JUnit converts many standard types automatically: primitives, String, Path, File, URI, URL, UUID, LocalDate, LocalTime, LocalDateTime, enums, and more. Prefer direct parameter types that JUnit can convert.
Built-in conversion also applies to collection elements: [com/example] → List<Path>, [Bob: 1980-03-04] → Map<String, LocalDate>, {https://claude.ai} → Set<URL>.
Date format limitation: Built-in LocalDate/LocalDateTime conversion only handles ISO 8601 format (yyyy-MM-dd, e.g. 2024-01-15). Non-standard formats — slash dates (15/01/2024), short years (24-01-15), locale-specific patterns — will fail at runtime. If any column contains non-ISO date strings, read references/type-converters.md before finalising the table and write a @TypeConverter method to handle the parsing.
@TableTest("""
Scenario | Class Name | Resolved Path?
With package | com.example.Foo | com/example/Foo
Nested class | Outer$Inner | Outer/Inner
""")
void converts_class_names(String className, Path expectedPath) {
assertThat(resolver.resolve(className)).isEqualTo(expectedPath);
}
Model observable inputs and outputs. Avoid internal flags or setup-only columns unless they are part of the public contract.
@TableTest("""
Scenario | Build Dir | JUnit Property | Configured Dir | Resolved Dir?
Configured input wins | build | report/junit | tabletest | tabletest
JUnit property takes effect | target | report/junit | | report/junit
Fallback when none set | build | | | build/junit-jupiter
""")
void resolvesInputDirectory(String buildDir, String junitProperty, String configuredDir, String resolvedDir) {
// setup derived from inputs, assert resolvedDir
}
When an operation produces multiple observable outputs, include them all as expectation columns in one table. Each row should give the complete picture of what happens for a given scenario. Don't split outputs of the same behavioral concern across separate test methods.
// Good — all outputs of priority resolution in one table
@TableTest("""
Scenario | Input Dir | JUnit Dir | Resolved Path? | Source? | Searched Locations?
Configured input wins | my-config | report/junit | my-config | CONFIGURED | [my-config]
JUnit property wins | | report/junit | report/junit | JUNIT_PROPERTY | [report/junit, build/junit-jupiter]
Fallback wins | | | build/junit-jupiter | FALLBACK | [build/junit-jupiter]
""")
void resolvesWithPriority(String inputDir, String junitDir,
String resolvedPath, ResolutionSource source, List<String> searchLocations) { ... }
// Bad — same outputs split across separate tests
void resolvesPath(...) // tests Resolved Path? only
void reportsSource(...) // tests Source? only
void reportsSearchedLocations(...)// tests Searched Locations? only
Splitting forces the reader to cross-reference multiple tables to understand one behavior. If the outputs all come from the same operation and concern, they belong together.
Separate tests are appropriate when testing a different concern of the same operation (e.g., path normalization vs. priority resolution) or a different method entirely.
The type of logic under test determines what each row should represent:
If rows feel out of place — parsing variations in a decision table, or decision branches in a parsing table — this signals the code under test may be mixing responsibilities. Consider whether the method should be split before adding more test rows.
End expectation columns with ? suffix to signal which columns are outputs being verified versus inputs being provided.
Examples: Valid?, Formatted?, Result?, Throws?, Expected?
Common mistake — ? as prefix instead of suffix:
?Source ← WRONG
Source? ← CORRECT
Describe the condition being tested, not the expected outcome. Good scenario names answer "under what circumstances?" rather than "what happens?".
| Good | Bad |
|---|---|
Negative input | Returns error |
Empty list | Sum is zero |
User without licence | Cannot rent |
Divisible by 4 but not 100 | Is leap year |
Scenario names appear in test failure messages, so clarity helps diagnose failures quickly.
Column values should be concrete, meaningful data — not abstract flags or codes. Expectation column values should be traceable to input column values.
Good — directory names as inputs, resolved dir traceable to an input column:
@TableTest("""
Scenario | Configured Dir | JUnit Dir | Fallback State | Resolved Dir? | Source?
Configured wins | my-config | report/junit | yaml | my-config | CONFIGURED
JUnit property wins | | report/junit | yaml | report/junit | JUNIT_PROPERTY
Fallback wins | | | yaml | target/junit | FALLBACK
""")
Bad — abstract flags, expectation values not traceable to inputs:
@TableTest("""
Scenario | Has Config | Override State | Fallback State | Resolved?
Configured wins | true | yaml | yaml | configured
Override wins | false | yaml | yaml | override
Fallback wins | false | | yaml | fallback
""")
In the bad example, configured, override, and fallback in Resolved? are names hardcoded in the test body, not visible in the table. The reader cannot understand the table without reading the test code.
When a value is derived from an input column (e.g., fallback path = Build Dir + "/junit-jupiter"), include the source column so readers can trace the derivation:
@TableTest("""
Scenario | Build Dir | Build State | Resolved Dir?
Maven fallback | target | yaml | target/junit-jupiter
Gradle fallback | build | yaml | build/junit-jupiter
""")
Here target/junit-jupiter is visibly derived from Build Dir = target.
Column names should use domain or feature terminology that readers understand without knowing the implementation. Avoid parameter names, variable names, or internal API terms.
| Good (Domain) | Bad (Implementation) |
|---|---|
JUnit Dir | Override |
Build Output | junitOutputDirOverride |
Search Locations? | Candidates? |
When one input takes precedence regardless of other inputs, use value sets to express this declaratively instead of listing every combination. Each {...} column generates a test per value.
@TableTest("""
Scenario | Priority | Fallback State | Resolved?
Priority wins regardless | main | {yaml, empty, missing} | main
""")
This single row generates 3 tests, all asserting main wins regardless of fallback state. See references/value-sets.md for full syntax.
Use blank cells for null, '' for empty strings, and ' ' for blank strings.
@TableTest("""
Scenario | Input | Resolved?
Normal input | hello | HELLO
Null input | |
Empty input | '' |
Blank input | ' ' |
""")
void resolves_values(String input, String resolved) {
assertThat(transform(input)).isEqualTo(resolved);
}
Note: These are syntax examples, not test design patterns. Null/empty/blank variants of an input should typically be additional rows in the test that covers the feature, not in a separate test method. For example, if a resolver ignores blank JUnit dir values, add those as rows in the main resolution test rather than creating a separate "handles blank values" test.
When writing TableTests with a pair, the most important habit is showing a mockup before writing any code — a 30-second table sketch prevents 5 minutes of rework. The full collaborative cadence is in references/pair-programming.md; read it when pairing or when the user wants a structured walkthrough.
Resist the urge to start coding immediately. The time spent understanding the code under test pays off in a cleaner table structure:
| Scenario | orgId | featureId | version | Feature Toggles | Query Count? | Result?
| Specific match | O | F | V | [O:F:V: true] | 1 | true
| Wild version | O | F | V | [O:F:*: true] | 2 | true
?).@Scenario.references/incremental-development.md for full refinement workflow)references/incremental-development.md for progressive enhancement)After tests pass, improve names and structure — see references/pair-programming.md for the full refinement workflow. The short version: names emerge from understanding, so don't expect perfect column or scenario names on the first implementation. Replace implementation terms with domain language once the table is working.
After writing, verify:
@Test for single casesif/switch statements, no parsing or conversion logic; the method only arranges, acts, and asserts@TypeConverter) or JUnit converters handle type conversion, keeping the test method free of parsing code[], {}, [:])? suffix (not prefix)Primary OK, Secondary ERROR)QueryCounter, not Helper); only extract to separate file when reused across test classesREAD these references when the condition applies. DO NOT proceed without reading:
| Reference | When to use |
|---|---|
references/dependency-setup.md | Project lacks TableTest dependency |
references/value-sets.md | Multiple example inputs map to same expectation |
references/type-converters.md | Custom types need parsing logic; non-ISO date formats appear in the table (dd/MM/yyyy, yy-MM-dd, etc.); or any column value won't convert automatically |
references/column-design.md | Deciding whether to split, combine, or use maps for columns; cross-table consistency |
references/common-patterns.md | Consolidating identity+status, positional fields, timing, async testing |
references/large-tables.md | Need comments, grouping, or external table files |
references/example-patterns.md | Need inspiration for table design (business rules, boundaries, exceptions) |
references/async-and-performance.md | Testing async/non-blocking behavior or tracking execution order |
references/provided-parameters.md | Using @TempDir or other injected parameters |
references/table-design-advanced.md | Table has rows that don't fit; mixed concerns suspected; scenario names unclear |
references/incremental-development.md | Building a complex table iteratively; learning from test failures |
references/consolidating-tests.md | Removing @Test methods covered by table |
references/testing-reveals-bugs.md | Test design feels wrong; suspecting implementation bug |
references/pair-programming.md | Pairing with a colleague; need structured collaborative cadence |
npx claudepluginhub scienta/claude-plugins --plugin tabletestGuides JUnit parameterized tests with value sources like CsvSource, MethodSource and test factories for efficient multi-case testing in Java. Use for test parameterization issues.
Provides a checklist for writing and reviewing tests: naming tests/files, designing data/fixtures/mocks, choosing assertions. Use for unit/integration/E2E tests.
Provides test design patterns, coverage strategies (80-100% targets), types (unit/integration/E2E), organization, and best practices for comprehensive test suites. Use for new suites, coverage improvement, or test design.