opentofu-modules

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

OpenTofu Modules & Testing

OpenTofu模块与测试

Write OpenTofu modules and tests for the homelab infrastructure. Modules live in
infrastructure/modules/
, tests in
infrastructure/modules/<name>/tests/
.
为家庭实验室基础设施编写OpenTofu模块及测试。模块存放于
infrastructure/modules/
目录,测试文件存放于
infrastructure/modules/<name>/tests/
目录。

Quick Reference

快速参考

bash
undefined
bash
undefined

Run tests for a module

运行模块测试

task tg:test-<module> # e.g., task tg:test-config
task tg:test-<module> # 示例:task tg:test-config

Format all HCL

格式化所有HCL文件

task tg:fmt
task tg:fmt

Version pinned in .opentofu-version (currently 1.11.2)

版本固定在.opentofu-version文件中(当前为1.11.2)

undefined
undefined

Module Structure

模块结构

Every module MUST have:
infrastructure/modules/<name>/
├── variables.tf    # Input definitions with descriptions and validations
├── main.tf         # Primary resources and locals
├── outputs.tf      # Output definitions
├── versions.tf     # Provider and OpenTofu version constraints
└── tests/          # Test directory
    └── *.tftest.hcl
每个模块必须包含以下内容:
infrastructure/modules/<name>/
├── variables.tf    # 带描述和验证规则的输入定义
├── main.tf         # 核心资源与本地变量
├── outputs.tf      # 输出定义
├── versions.tf     # 提供商与OpenTofu版本约束
└── tests/          # 测试目录
    └── *.tftest.hcl

Test File Structure

测试文件结构

Use
.tftest.hcl
extension. Define top-level
variables
for defaults inherited by all
run
blocks.
hcl
undefined
使用
.tftest.hcl
扩展名。在顶层定义
variables
,为所有
run
块设置默认继承值。
hcl
undefined

Top-level variables set defaults for ALL run blocks

顶层变量为所有run块设置默认值

variables { name = "test-cluster" features = ["gateway-api", "longhorn"]
networking = { id = 1 internal_tld = "internal.test.local" # ... other required fields }

Default machine - inherited unless overridden

machines = { node1 = { cluster = "test-cluster" type = "controlplane" install = { selector = "disk.model = *" } interfaces = [{ id = "eth0" hardwareAddr = "aa:bb:cc:dd:ee:01" addresses = [{ ip = "192.168.10.101" }] }] } } }
run "descriptive_test_name" { command = plan # Use plan mode - no real resources created
variables { features = ["prometheus"] # Only override what differs }
assert { condition = output.some_value == "expected" error_message = "Descriptive failure message" } }
undefined
variables { name = "test-cluster" features = ["gateway-api", "longhorn"]
networking = { id = 1 internal_tld = "internal.test.local" # ... 其他必填字段 }

默认机器配置 - 除非被覆盖否则会被继承

machines = { node1 = { cluster = "test-cluster" type = "controlplane" install = { selector = "disk.model = *" } interfaces = [{ id = "eth0" hardwareAddr = "aa:bb:cc:dd:ee:01" addresses = [{ ip = "192.168.10.101" }] }] } } }
run "descriptive_test_name" { command = plan # 使用plan模式 - 不会创建真实资源
variables { features = ["prometheus"] # 仅覆盖需要修改的内容 }
assert { condition = output.some_value == "expected" error_message = "描述性失败信息" } }
undefined

Key Patterns

核心模式

Use
command = plan

使用
command = plan

Always use plan mode for tests. This validates configuration without creating resources.
测试始终使用plan模式。这样可以在不创建资源的情况下验证配置。

Variable Inheritance

变量继承

Only include variables in
run
blocks when they differ from defaults. Minimizes duplication.
hcl
undefined
仅在
run
块中包含与默认值不同的变量,减少重复代码。
hcl
undefined

CORRECT: Override only what changes

正确写法:仅覆盖需要修改的内容

run "feature_enabled" { command = plan variables { features = ["prometheus"] } assert { ... } }
run "feature_enabled" { command = plan variables { features = ["prometheus"] } assert { ... } }

AVOID: Repeating all variables

避免:重复所有变量

run "feature_enabled" { command = plan variables { name = "test-cluster" # Unnecessary - inherited features = ["prometheus"] machines = { ... } # Unnecessary - inherited } }
undefined
run "feature_enabled" { command = plan variables { name = "test-cluster" # 不必要 - 已继承 features = ["prometheus"] machines = { ... } # 不必要 - 已继承 } }
undefined

Assert Against Outputs

针对输出进行断言

Reference module outputs in assertions, not internal resources.
hcl
assert {
  condition     = length(output.machines) == 2
  error_message = "Expected 2 machines"
}

assert {
  condition     = output.talos.kubernetes_version == "1.32.0"
  error_message = "Version mismatch"
}
在断言中引用模块输出,而非内部资源。
hcl
assert {
  condition     = length(output.machines) == 2
  error_message = "预期2台机器"
}

assert {
  condition     = output.talos.kubernetes_version == "1.32.0"
  error_message = "版本不匹配"
}

Test Feature Flags

测试功能开关

Test both enabled and disabled states:
hcl
run "feature_enabled" {
  command = plan
  variables { features = ["longhorn"] }

  assert {
    condition = alltrue([
      for m in output.talos.talos_machines :
      contains(m.install.extensions, "iscsi-tools")
    ])
    error_message = "Extension should be added when feature enabled"
  }
}

run "feature_disabled" {
  command = plan
  variables { features = [] }

  assert {
    condition = alltrue([
      for m in output.talos.talos_machines :
      !contains(m.install.extensions, "iscsi-tools")
    ])
    error_message = "Extension should not be present without feature"
  }
}
测试功能启用和禁用两种状态:
hcl
run "feature_enabled" {
  command = plan
  variables { features = ["longhorn"] }

  assert {
    condition = alltrue([
      for m in output.talos.talos_machines :
      contains(m.install.extensions, "iscsi-tools")
    ])
    error_message = "启用功能时应添加扩展"
  }
}

run "feature_disabled" {
  command = plan
  variables { features = [] }

  assert {
    condition = alltrue([
      for m in output.talos.talos_machines :
      !contains(m.install.extensions, "iscsi-tools")
    ])
    error_message = "未启用功能时不应存在扩展"
  }
}

Test Validations

测试验证规则

Use
expect_failures
to verify variable validation rules:
hcl
run "invalid_version_rejected" {
  command = plan
  variables {
    versions = {
      talos = "1.9.0"  # Missing v prefix - should fail
      # ...
    }
  }
  expect_failures = [var.versions]
}
使用
expect_failures
验证变量验证规则:
hcl
run "invalid_version_rejected" {
  command = plan
  variables {
    versions = {
      talos = "1.9.0"  # 缺少v前缀 - 应验证失败
      # ...
    }
  }
  expect_failures = [var.versions]
}

Common Assertions

常见断言示例

hcl
undefined
hcl
undefined

Check length

检查长度

condition = length(output.items) == 3
condition = length(output.items) == 3

Check key exists

检查键是否存在

condition = contains(keys(output.map), "expected_key")
condition = contains(keys(output.map), "expected_key")

Check value in list

检查值是否在列表中

condition = contains(output.list, "expected_value")
condition = contains(output.list, "expected_value")

Check string contains

检查字符串是否包含子串

condition = strcontains(output.config, "expected_substring")
condition = strcontains(output.config, "expected_substring")

Check all items match

检查所有项是否匹配

condition = alltrue([for item in output.list : item.enabled == true])
condition = alltrue([for item in output.list : item.enabled == true])

Check any item matches

检查是否存在匹配项

condition = anytrue([for item in output.list : item.name == "target"])
condition = anytrue([for item in output.list : item.name == "target"])

Nested check with labels/annotations

嵌套检查标签/注解

condition = anytrue([ for label in output.machines["node1"].labels : label.key == "expected-label" && label.value == "expected-value" ])
undefined
condition = anytrue([ for label in output.machines["node1"].labels : label.key == "expected-label" && label.value == "expected-value" ])
undefined

Test Organization

测试组织方式

Organize tests by concern:
  • plan.tftest.hcl
    - Basic structure and output validation
  • validation.tftest.hcl
    - Input validation rules
  • feature_<name>.tftest.hcl
    - Feature flag behavior
  • edge_cases.tftest.hcl
    - Boundary conditions
按关注点组织测试:
  • plan.tftest.hcl
    - 基础结构与输出验证
  • validation.tftest.hcl
    - 输入验证规则
  • feature_<name>.tftest.hcl
    - 功能开关行为
  • edge_cases.tftest.hcl
    - 边界条件

Detailed Reference

详细参考

For OpenTofu testing syntax, mock providers, and advanced patterns, see: references/opentofu-testing.md
关于OpenTofu测试语法、模拟提供商及高级模式,请参考: references/opentofu-testing.md