Goal
Consolidate duplicate tests into table-driven tests using a structured testdata directory. Remove standalone tests that duplicate table-driven test coverage.
Directory Structure
testdata/table/
├── inventory/
│ ├── simple-ungrouped.yml
│ ├── simple-ungrouped.expected.json
│ ├── hierarchical-groups.yml
│ ├── hierarchical-groups.expected.json
│ └── ...
├── playbooks/
│ ├── simple-play.yml
│ ├── simple-play.expected.json
│ ├── block.yml
│ ├── block.expected.json
│ └── ...
└── tasks/
├── simple-task.yml
├── simple-task.expected.json
├── local-action.yml
├── local-action.expected.json
└── ...
Expected Outcome Format
Each .yml file has a corresponding .expected.json file containing the
expected parsed output. Using JSON because:
- Easy to read and write
- Go has built-in JSON support
- Can represent nested structures
- Easy to diff when tests fail
Inventory Expected Format
{
"hosts": [
{
"name": "host1.example.com",
"vars": {
"ansible_host": "192.168.1.1"
}
}
],
"groups": [
{
"name": "webservers",
"hosts": ["host1.example.com"],
"children": [],
"vars": {}
}
],
"warnings": []
}
Playbook Expected Format
{
"plays": [
{
"name": "Configure servers",
"hosts": "all",
"gather_facts": true,
"become": false,
"tasks": [
{
"name": "Install nginx",
"module": "apt",
"args": {
"name": "nginx",
"state": "present"
},
"is_block": false
}
],
"handlers": [],
"roles": []
}
],
"imports": [],
"warnings": []
}
Tasks Expected Format
{
"tasks": [
{
"name": "Simple task",
"module": "ansible.builtin.debug",
"args": {
"msg": "hello"
},
"is_block": false,
"is_handler": false
}
],
"warnings": []
}
Test Implementation
Generic Table Test Runner
// testutil/table_test.go (or in each plugin's test file)
type TableTest struct {
InputFile string
ExpectedFile string
}
func loadTableTests(dir string) ([]TableTest, error) {
// Find all .yml files, pair with .expected.json
}
func TestInventory_TableDriven(t *testing.T) {
tests, _ := loadTableTests("../../testdata/table/inventory")
for _, tc := range tests {
t.Run(tc.InputFile, func(t *testing.T) {
input, _ := os.ReadFile(tc.InputFile)
expected, _ := os.ReadFile(tc.ExpectedFile)
inv, warnings, err := ParseYAMLInventory(input)
// Compare inv to expected JSON
})
}
}
Comparison Strategy
- Parse expected JSON into same Go struct used for parsing
- Use
reflect.DeepEqualor custom comparison - For better error messages, compare field by field
- Optionally use
go-cmpfor detailed diffs
Migration Steps
Phase 1: Create testdata/table structure
- Create directories:
mkdir -p testdata/table/{inventory,playbooks,tasks}
Phase 2: Migrate inventory tests
Create test files from existing inline tests:
simple-ungrouped.yml+.expected.jsonhierarchical-groups.yml+.expected.jsongroup-vars.yml+.expected.jsonempty-group.yml+.expected.json
Copy relevant files from
testdata/inventory/:ansible-examples.yml+.expected.jsoninherit-group.yml+.expected.json
Update
plugins/inventory/parser_test.go:- Add table-driven test function
- Remove duplicate standalone tests
- Keep edge case tests (invalid YAML, empty request, etc.)
Phase 3: Migrate playbook tests
Create test files:
simple-tasks.yml+.expected.jsonwith-handlers.yml+.expected.jsonwith-block.yml+.expected.jsonwith-roles.yml+.expected.jsonwith-import.yml+.expected.jsonplay-attributes.yml+.expected.jsontask-attributes.yml+.expected.jsonmulti-play.yml+.expected.jsonloop-var-prefix.yml+.expected.json
Copy from
testdata/playbooks/:block.yml+.expected.jsonbecome.yml+.expected.jsoninclude.yml+.expected.jsonpass-loop-var-prefix.yml+.expected.json
Update
plugins/playbook/parser_test.go:- Add table-driven test function
- Remove duplicate standalone tests
- Keep: request parsing, error response, round-trip tests
Phase 4: Migrate task tests
Create test files:
simple-task.yml+.expected.jsonpassing-task.yml+.expected.jsonmain-tasks.yml+.expected.jsonempty-blocks.yml+.expected.jsonlocal-action.yml+.expected.json
Copy from
testdata/playbooks/tasks/:simple_task.yml+.expected.jsonpassing_task.yml+.expected.jsonmain.yml+.expected.json
Update
plugins/playbook/parser_test.go:- Add table-driven task test function
- Remove duplicate standalone task tests
Tests to Keep (Not Table-Driven)
Some tests don't fit the table pattern and should remain standalone:
Inventory
TestParseInventoryRequest- tests FlatBuffer request parsingTestParseInventoryRequest_Empty- edge caseTestBuildInventoryResponse_RoundTrip- FlatBuffer serializationTestBuildInventoryErrorResponse- error response buildingTestParseYAMLInventory_InvalidYAML- error handling
Playbook
TestParsePlaybookRequest- tests FlatBuffer request parsingTestParsePlaybookRequest_Empty- edge caseTestBuildPlaybookResponse_RoundTrip- FlatBuffer serializationTestBuildPlaybookErrorResponse- error response buildingTestParseYAMLPlaybook_InvalidYAML- error handling
Expected JSON Generation
To bootstrap expected files, add a helper that generates them:
// Run once to generate expected files, then verify manually
func TestGenerateExpected(t *testing.T) {
t.Skip("only run manually to generate expected files")
input, _ := os.ReadFile("testdata/table/playbooks/simple.yml")
playbook, warnings, _ := ParseYAMLPlaybook(input)
expected := map[string]any{
"plays": toJSON(playbook.Plays),
"imports": toJSON(playbook.Imports),
"warnings": warnings,
}
out, _ := json.MarshalIndent(expected, "", " ")
os.WriteFile("testdata/table/playbooks/simple.expected.json", out, 0644)
}
File Count Summary
After migration:
| Directory | Files |
|---|---|
| testdata/table/inventory | ~12 (6 yml + 6 json) |
| testdata/table/playbooks | ~20 (10 yml + 10 json) |
| testdata/table/tasks | ~10 (5 yml + 5 json) |
Benefits
- Single source of truth - test data lives in files, not code
- Easy to add tests - just add yml + expected.json
- Easy to review - expected output is explicit
- Reusable - same test data could be used for other purposes
- No duplication - each scenario tested once