Test Refactoring Plan: Table-Driven Tests
Not logged in

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:

  1. Easy to read and write
  2. Go has built-in JSON support
  3. Can represent nested structures
  4. 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

  1. Parse expected JSON into same Go struct used for parsing
  2. Use reflect.DeepEqual or custom comparison
  3. For better error messages, compare field by field
  4. Optionally use go-cmp for detailed diffs

Migration Steps

Phase 1: Create testdata/table structure

  1. Create directories:

   mkdir -p testdata/table/{inventory,playbooks,tasks}

Phase 2: Migrate inventory tests

  1. Create test files from existing inline tests:

    • simple-ungrouped.yml + .expected.json
    • hierarchical-groups.yml + .expected.json
    • group-vars.yml + .expected.json
    • empty-group.yml + .expected.json
  2. Copy relevant files from testdata/inventory/:

    • ansible-examples.yml + .expected.json
    • inherit-group.yml + .expected.json
  3. 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

  1. Create test files:

    • simple-tasks.yml + .expected.json
    • with-handlers.yml + .expected.json
    • with-block.yml + .expected.json
    • with-roles.yml + .expected.json
    • with-import.yml + .expected.json
    • play-attributes.yml + .expected.json
    • task-attributes.yml + .expected.json
    • multi-play.yml + .expected.json
    • loop-var-prefix.yml + .expected.json
  2. Copy from testdata/playbooks/:

    • block.yml + .expected.json
    • become.yml + .expected.json
    • include.yml + .expected.json
    • pass-loop-var-prefix.yml + .expected.json
  3. 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

  1. Create test files:

    • simple-task.yml + .expected.json
    • passing-task.yml + .expected.json
    • main-tasks.yml + .expected.json
    • empty-blocks.yml + .expected.json
    • local-action.yml + .expected.json
  2. Copy from testdata/playbooks/tasks/:

    • simple_task.yml + .expected.json
    • passing_task.yml + .expected.json
    • main.yml + .expected.json
  3. 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

Playbook

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

  1. Single source of truth - test data lives in files, not code
  2. Easy to add tests - just add yml + expected.json
  3. Easy to review - expected output is explicit
  4. Reusable - same test data could be used for other purposes
  5. No duplication - each scenario tested once