Infrastructure as code solved the problem of reproducibility. It didn’t solve the problem of correctness. You can define your infrastructure in Terraform, commit it, review it, and deploy it: and still discover in production that a DNS record points to the wrong origin, a secret wasn’t injected into the right environment, or a branch protection rule is missing from a repository you created three months ago.
Terraform’s testing framework, introduced in version 1.6, is supposed to close that gap. In practice, it’s immature. The rough edges are tolerable if you’re writing tests by hand for a single project. They become untenable when you’re generating infrastructure for multiple projects from templates: because every consumer inherits the same rough edges, and fixing them means fixing the generator, not patching individual test files.
LATHE is a project scaffolding CLI. It reads a YAML config and renders layered templates to produce an entire project structure: CI workflows, Terraform modules, app scaffolding, build scripts. The Terraform test files are one output among many. But the interesting property is that LATHE generates both the infrastructure and the tests that verify it from the same source of truth. They can’t drift. If the template changes, the tests change with it.
Layered Template Composition
LATHE’s templates are organised in layers: base, then template type, then tech stack variant. Each layer overwrites files from the layer below. Adding a new variant, a Godot project, a Unity project, a different backend framework, means adding a directory under the appropriate type. No changes to the CLI, the renderer, or any existing template.
The renderer is deliberately minimal: three mechanisms, substitution, conditionals, and iteration, in under 150 lines, isolated behind a single render() function. A strict “no nesting” rule on conditionals keeps templates readable and pushes complex logic into directory-level composition where it belongs. If a template needs nested conditionals, the structure is wrong.
# lathe.yml; single source of truth for a project
project:
name: aftertouch
displayName: Aftertouch
org: thepromisedclan
template: full
backend: typescript-hono
hosting: render
database: supabase
domains:
primary: aftertouch.app
clients:
- slug: web
framework: javascript-react
platform: web
From this config, LATHE produces Terraform modules for Cloudflare Pages, DNS records, GitHub repository settings, branch protection rules, Sentry project configuration, and the corresponding test files that verify all of it. One input. Complete output.
Four Issues at the Edges
The test generation approach was validated during real consumer rollouts: AFTERTOUCH and CRACKPOINT were the first projects scaffolded by LATHE. Running terraform test against the generated files for the first time surfaced four genuine gaps in Terraform’s testing ecosystem. None were user errors. All required workarounds in the generator.
Hyphenated Resource Names
Terraform accepts hyphens in resource names in .tf files. The test framework doesn’t handle them in assertion conditions. A for_each resource named github_actions_secret.my-project["KEY"] breaks the test parser; the hyphen is interpreted as subtraction.
The fix is architectural, not syntactic. LATHE’s config schema validates that project.name contains no hyphens. Underscores for identifier slugs, enforced at the input layer. The problem is eliminated by construction; no generated resource will ever have a name the test parser can’t handle, because the config that drives generation won’t allow it.
One Plan Per Run Block
Each run block in a .tftest.hcl file executes a full terraform plan. Multiple assertions within a single run block share that plan. But Terraform re-plans from scratch for every run block. Up to nine test files across global and environment workspaces, with multiple run blocks each, means minutes, not seconds, for a full test pass.
There’s no “plan once, assert many” mode. The structural reason is that Terraform treats each run block as an isolated plan/apply step for state management. The workaround is batching: LATHE groups related assertions into as few run blocks as possible. All Sentry assertions in one block. All branch protection checks in another. Minimise the number of plans. Maximise assertions per plan.
It’s the best available approach, but it’s a workaround for a missing feature, not a solution.
The Version Gap
mock_provider, which mocks all API calls for a provider, arrived in Terraform 1.7. override_data, which stubs a specific data source’s return value, arrived in 1.8. They solve different problems:
Global workspace tests use mock_provider because they need to mock Cloudflare, GitHub, and Sentry API calls entirely. No real API traffic during testing.
Environment workspace tests use override_data because they need to stub data.terraform_remote_state.global, the environment workspace reads outputs from the global workspace, which doesn’t exist during testing. You can’t mock the entire Terraform provider for this; you need to stub one specific data source.
The version gap means a consumer on Terraform 1.7 can run global tests but gets parse errors on environment tests. LATHE’s generated files don’t declare a minimum Terraform version, so the failure is silent until you run the tests. The resolution was documenting the ≥1.8 requirement; straightforward, but the kind of friction that shouldn’t exist in a generator that’s supposed to produce working output.
Validation Block Compliance
Terraform variables can have validation blocks with arbitrary conditions: regex("^[0-9a-f]{32}$", var.cloudflare_account_id), length(var.cf_origin_verify_secret) >= 32. The stub values in generated test files must satisfy these validations, or terraform test fails at the plan stage before any assertions run.
The hard part: validation rules are arbitrary HCL expressions. You can’t introspect them programmatically from outside Terraform. LATHE handles the known cases: actual config values for account IDs that are already valid hex, zero-padded strings for crypto secrets, test-* prefixes for length checks. But it’s fragile. A consumer adding a custom validation rule to a variable they’ve extended will break the generated stubs with no obvious error message pointing to the cause.
There’s no general solution short of parsing HCL validation expressions, which Terraform itself doesn’t expose as a programmatic API. This is the one issue where the workaround is genuinely unsatisfying.
The Thesis
Each of these issues is individually minor. A developer hitting one of them in a single project would spend an hour finding the fix, apply it, and move on. The reason they matter is scale. LATHE generates infrastructure for every new project. Every consumer inherits whatever the templates produce. A workaround applied once in a template file propagates to every project scaffolded after it, which means getting the template right matters more than getting any individual project right.
This is the argument for generating infrastructure and tests from the same source of truth. When the test files are hand-written, they drift from the infrastructure they’re testing. When they’re co-generated, a fix to the template is a fix to every current and future consumer. The pain points surface early because every project hits them, not just one team discovering them six months in.
Treating infrastructure as testable code requires tooling that the Terraform ecosystem doesn’t yet provide in full. LATHE fills the gap pragmatically; not by solving Terraform’s problems, but by generating around them consistently and automatically.
What I’d Do Differently
The renderer’s “no nesting” rule on conditionals is a good constraint but it pushes complexity into the directory structure. Some template variants have nearly identical files differing by a single block; a Cloudflare Pages project with or without a web analytics token, for instance. Extracting that into a partial or mixin system would reduce duplication without sacrificing the readability that the no-nesting rule protects.
The test generation doesn’t currently verify its own output. LATHE generates .tftest.hcl files, but there’s no step that runs terraform test as part of LATHE’s own CI to confirm the generated tests actually pass. Adding snapshot tests that render a template with known config values and run the resulting Terraform tests would catch regressions in the generator itself; testing the tests, which sounds redundant until you’ve shipped a broken test template to three projects simultaneously.
Outcome
LATHE scaffolds complete project structures (infrastructure, CI, app scaffolding, tests) from a single YAML config. AFTERTOUCH, CRACKPOINT, and every future project under The Promised Clan start from the same foundation, with the same conventions, the same CI pipeline, the same Terraform test coverage, and AI code review via Tribunal on every PR. Fixes to the templates propagate forward. Lessons from one consumer improve the generator for all of them.
The four issues are real gaps in a young testing framework. Documenting them matters less than generating around them: automatically, consistently, so no one else has to discover them the hard way.
Four issues. Four fixes. Every future project inherits all of them.