Skip to content

Commit 9fbeebc

Browse files
author
Liam Cervante
committed
terraform test: add variable definitions to test files
Currently, `terraform test` attempts to work out the type of any external variables by delaying evaluation until each run block executes so it can use the definitions within the run blocks's module. This means that the values of variables can technically change between run blocks which isn't ideal. This commit is the first in a chain which will move the evaluation of variables into the terraform test graph. We need to give the users the option of specifying the type for external variables within the file as these variables are now going to be assessed outside of the context of a run block. To do this, we introduce the optional variable blocks that are added by this commit.
1 parent eb0de9b commit 9fbeebc

File tree

18 files changed

+354
-318
lines changed

18 files changed

+354
-318
lines changed
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
kind: ENHANCEMENTS
2+
body: '`terraform test`: Test authors can now specify definitions for external variables that are referenced within test files directly within the test file itself.'
3+
time: 2025-06-02T15:22:11.453413+02:00
4+
custom:
5+
Issue: "37195"
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
kind: UPGRADE NOTES
2+
body: '`terraform test`: External variables referenced within test files should now be accompanied by a `variable` definition block within the test file. This is optional, but users with complex external variables may see error diagnostics without the additional variable definition.'
3+
time: 2025-06-02T15:20:09.188388+02:00
4+
custom:
5+
Issue: "37195"

internal/backend/local/test.go

Lines changed: 17 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -110,26 +110,30 @@ func (runner *TestSuiteRunner) Test() (moduletest.Status, tfdiags.Diagnostics) {
110110
}
111111

112112
file := suite.Files[name]
113-
evalCtx := graph.NewEvalContext(graph.EvalContextOpts{
114-
CancelCtx: runner.CancelledCtx,
115-
StopCtx: runner.StoppedCtx,
116-
Verbose: runner.Verbose,
117-
Render: runner.View,
118-
})
119-
120-
// TODO(liamcervante): Do the variables in the EvalContextTransformer
121-
// as well as the run blocks.
122113

123114
currentGlobalVariables := runner.GlobalVariables
124115
if filepath.Dir(file.Name) == runner.TestingDirectory {
125116
// If the file is in the test directory, we'll use the union of the
126117
// global variables and the global test variables.
127118
currentGlobalVariables = testDirectoryGlobalVariables
128119
}
129-
evalCtx.VariableCaches = hcltest.NewVariableCaches(func(vc *hcltest.VariableCaches) {
130-
maps.Copy(vc.GlobalVariables, currentGlobalVariables)
131-
vc.FileVariables = file.Config.Variables
120+
121+
evalCtx := graph.NewEvalContext(graph.EvalContextOpts{
122+
CancelCtx: runner.CancelledCtx,
123+
StopCtx: runner.StoppedCtx,
124+
Verbose: runner.Verbose,
125+
Render: runner.View,
126+
VariableCache: &hcltest.VariableCache{
127+
128+
// TODO(liamcervante): Do the variables in the EvalContextTransformer
129+
// as well as the run blocks.
130+
131+
ExternalVariableValues: currentGlobalVariables,
132+
TestFileVariableDefinitions: file.Config.VariableDefinitions,
133+
TestFileVariableExpressions: file.Config.Variables,
134+
},
132135
})
136+
133137
fileRunner := &TestFileRunner{
134138
Suite: runner,
135139
EvalContext: evalCtx,
@@ -248,7 +252,7 @@ func (runner *TestFileRunner) Test(file *moduletest.File) {
248252
// Build the graph for the file.
249253
b := graph.TestGraphBuilder{
250254
File: file,
251-
GlobalVars: runner.EvalContext.VariableCaches.GlobalVariables,
255+
GlobalVars: runner.EvalContext.VariableCache.ExternalVariableValues,
252256
ContextOpts: runner.Suite.Opts,
253257
}
254258
g, diags := b.Build()

internal/command/test_test.go

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -334,6 +334,11 @@ func TestTest_Runs(t *testing.T) {
334334
expectedOut: []string{"1 passed, 0 failed."},
335335
code: 0,
336336
},
337+
"with-default-variables": {
338+
args: []string{"-var=input_two=universe"},
339+
expectedOut: []string{"1 passed, 0 failed."},
340+
code: 0,
341+
},
337342
}
338343
for name, tc := range tcs {
339344
t.Run(name, func(t *testing.T) {
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
2+
variable "input_one" {
3+
type = string
4+
}
5+
6+
variable "input_two" {
7+
type = string
8+
}
9+
10+
resource "test_resource" "resource" {
11+
value = "${var.input_one} - ${var.input_two}"
12+
}
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
2+
variable "input_one" {
3+
type = string
4+
default = "hello"
5+
}
6+
7+
variable "input_two" {
8+
type = string
9+
default = "world" // we will override this an external value
10+
}
11+
12+
run "test" {
13+
assert {
14+
condition = test_resource.resource.value == "hello - universe"
15+
error_message = "bad concatenation"
16+
}
17+
}

internal/configs/test_file.go

Lines changed: 49 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,14 @@ const (
4545
// A test file is made up of a sequential list of run blocks, each designating
4646
// a command to execute and a series of validations to check after the command.
4747
type TestFile struct {
48+
49+
// VariableDefinitions allows users to specify variables that should be
50+
// provided externally (eg. from the command line or external files).
51+
//
52+
// This conflicts with the Variables block. Variables specified in the
53+
// VariableDefinitions cannot also be specified within the Variables block.
54+
VariableDefinitions map[string]*Variable
55+
4856
// Variables defines a set of global variable definitions that should be set
4957
// for every run block within the test file.
5058
Variables map[string]hcl.Expression
@@ -327,8 +335,9 @@ type TestRunOptions struct {
327335
func loadTestFile(body hcl.Body) (*TestFile, hcl.Diagnostics) {
328336
var diags hcl.Diagnostics
329337
tf := &TestFile{
330-
Providers: make(map[string]*Provider),
331-
Overrides: addrs.MakeMap[addrs.Targetable, *Override](),
338+
VariableDefinitions: make(map[string]*Variable),
339+
Providers: make(map[string]*Provider),
340+
Overrides: addrs.MakeMap[addrs.Targetable, *Override](),
332341
}
333342

334343
// we need to retrieve the file config block first, because the run blocks
@@ -370,6 +379,31 @@ func loadTestFile(body hcl.Body) (*TestFile, hcl.Diagnostics) {
370379
}
371380
runBlockNames[run.Name] = run.DeclRange
372381

382+
case "variable":
383+
variable, variableDiags := decodeVariableBlock(block, false)
384+
diags = append(diags, variableDiags...)
385+
if !variableDiags.HasErrors() {
386+
if existing, exists := tf.VariableDefinitions[variable.Name]; exists {
387+
diags = append(diags, &hcl.Diagnostic{
388+
Severity: hcl.DiagError,
389+
Summary: "Duplicate \"variable\" block names",
390+
Detail: fmt.Sprintf("This test file already has a variable named %s defined at %s.", variable.Name, existing.DeclRange),
391+
Subject: variable.DeclRange.Ptr(),
392+
})
393+
continue
394+
}
395+
tf.VariableDefinitions[variable.Name] = variable
396+
397+
if existing, exists := tf.Variables[variable.Name]; exists {
398+
diags = append(diags, &hcl.Diagnostic{
399+
Severity: hcl.DiagError,
400+
Summary: "Duplicate \"variable\" block names",
401+
Detail: fmt.Sprintf("This test file already has a variable named %s defined at %s.", variable.Name, existing.Range()),
402+
Subject: variable.DeclRange.Ptr(),
403+
})
404+
}
405+
}
406+
373407
case "variables":
374408
if tf.Variables != nil {
375409
diags = append(diags, &hcl.Diagnostic{
@@ -388,6 +422,15 @@ func loadTestFile(body hcl.Body) (*TestFile, hcl.Diagnostics) {
388422
diags = append(diags, varsDiags...)
389423
for _, v := range vars {
390424
tf.Variables[v.Name] = v.Expr
425+
426+
if existing, exists := tf.VariableDefinitions[v.Name]; exists {
427+
diags = append(diags, &hcl.Diagnostic{
428+
Severity: hcl.DiagError,
429+
Summary: "Duplicate \"variable\" block names",
430+
Detail: fmt.Sprintf("This test file already has a variable named %s defined at %s.", v.Name, v.Range),
431+
Subject: existing.DeclRange.Ptr(),
432+
})
433+
}
391434
}
392435
case "provider":
393436
provider, providerDiags := decodeProviderBlock(block, true)
@@ -888,6 +931,10 @@ var testFileSchema = &hcl.BodySchema{
888931
Type: "mock_provider",
889932
LabelNames: []string{"name"},
890933
},
934+
{
935+
Type: "variable",
936+
LabelNames: []string{"name"},
937+
},
891938
{
892939
Type: "variables",
893940
},

internal/moduletest/graph/eval_context.go

Lines changed: 7 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,7 @@ type TestFileState struct {
4242
// within the suite.
4343
// The struct provides concurrency-safe access to the various maps it contains.
4444
type EvalContext struct {
45-
VariableCaches *hcltest.VariableCaches
45+
VariableCache *hcltest.VariableCache
4646

4747
// runOutputs is a mapping from run addresses to cty object values
4848
// representing the collected output values from the module under test.
@@ -84,10 +84,11 @@ type EvalContext struct {
8484
}
8585

8686
type EvalContextOpts struct {
87-
Verbose bool
88-
Render views.Test
89-
CancelCtx context.Context
90-
StopCtx context.Context
87+
Verbose bool
88+
Render views.Test
89+
CancelCtx context.Context
90+
StopCtx context.Context
91+
VariableCache *hcltest.VariableCache
9192
}
9293

9394
// NewEvalContext constructs a new graph evaluation context for use in
@@ -104,7 +105,7 @@ func NewEvalContext(opts EvalContextOpts) *EvalContext {
104105
providersLock: sync.Mutex{},
105106
FileStates: make(map[string]*TestFileState),
106107
stateLock: sync.Mutex{},
107-
VariableCaches: hcltest.NewVariableCaches(),
108+
VariableCache: opts.VariableCache,
108109
cancelContext: cancelCtx,
109110
cancelFunc: cancel,
110111
stopContext: stopCtx,
@@ -326,10 +327,6 @@ func (ec *EvalContext) GetOutputs() map[addrs.Run]cty.Value {
326327
return outputCopy
327328
}
328329

329-
func (ec *EvalContext) GetCache(run *moduletest.Run) *hcltest.VariableCache {
330-
return ec.VariableCaches.GetCache(run.Name, run.ModuleConfig)
331-
}
332-
333330
// ProviderExists returns true if the provider exists for the run inside the context.
334331
func (ec *EvalContext) ProviderExists(run *moduletest.Run, key string) bool {
335332
ec.providersLock.Lock()

internal/moduletest/graph/transform_config.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -88,7 +88,7 @@ func TransformConfigForRun(ctx *EvalContext, run *moduletest.Run, file *modulete
8888
AliasRange: ref.InChild.AliasRange,
8989
Config: &hcltest.ProviderConfig{
9090
Original: testProvider.Config,
91-
VariableCache: ctx.GetCache(run),
91+
VariableCache: ctx.VariableCache,
9292
AvailableRunOutputs: runOutputs,
9393
},
9494
Mock: testProvider.Mock,
@@ -114,7 +114,7 @@ func TransformConfigForRun(ctx *EvalContext, run *moduletest.Run, file *modulete
114114
AliasRange: provider.AliasRange,
115115
Config: &hcltest.ProviderConfig{
116116
Original: provider.Config,
117-
VariableCache: ctx.GetCache(run),
117+
VariableCache: ctx.VariableCache,
118118
AvailableRunOutputs: runOutputs,
119119
},
120120
Mock: provider.Mock,

internal/moduletest/graph/variables.go

Lines changed: 27 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -59,19 +59,8 @@ func (n *NodeTestRun) GetVariables(ctx *EvalContext, includeWarnings bool) (terr
5959
refs, refDiags := langrefs.ReferencesInExpr(addrs.ParseRefFromTestingScope, expr)
6060
for _, ref := range refs {
6161
if addr, ok := ref.Subject.(addrs.InputVariable); ok {
62-
cache := ctx.GetCache(run)
63-
64-
value, valueDiags := cache.GetFileVariable(addr.Name)
65-
diags = diags.Append(valueDiags)
66-
if value != nil {
67-
requiredValues[addr.Name] = value.Value
68-
continue
69-
}
70-
71-
// Otherwise, it might be a global variable.
72-
value, valueDiags = cache.GetGlobalVariable(addr.Name)
73-
diags = diags.Append(valueDiags)
74-
if value != nil {
62+
if value, valueDiags := ctx.VariableCache.GetVariableValue(addr.Name); value != nil {
63+
diags = diags.Append(valueDiags)
7564
requiredValues[addr.Name] = value.Value
7665
continue
7766
}
@@ -118,18 +107,14 @@ func (n *NodeTestRun) GetVariables(ctx *EvalContext, includeWarnings bool) (terr
118107
continue
119108
}
120109

121-
// Otherwise, we'll get it from the cache as a file-level or global
122-
// variable.
123-
cache := ctx.GetCache(run)
124-
125-
value, valueDiags := cache.GetFileVariable(variable)
126-
diags = diags.Append(valueDiags)
127-
if value != nil {
128-
values[variable] = value
110+
if _, exists := run.ModuleConfig.Module.Variables[variable]; exists {
111+
// We'll deal with this later.
129112
continue
130113
}
131114

132-
value, valueDiags = cache.GetGlobalVariable(variable)
115+
// Otherwise, we'll get it from the cache as a file-level or global
116+
// variable.
117+
value, valueDiags := ctx.VariableCache.GetVariableValue(variable)
133118
diags = diags.Append(valueDiags)
134119
if value != nil {
135120
values[variable] = value
@@ -143,11 +128,29 @@ func (n *NodeTestRun) GetVariables(ctx *EvalContext, includeWarnings bool) (terr
143128

144129
for name, variable := range run.ModuleConfig.Module.Variables {
145130
if _, exists := values[name]; exists {
146-
// Then we've provided a variable for this. It's all good.
131+
// Then we've provided a variable for this explicitly. It's all
132+
// good.
147133
continue
148134
}
149135

150-
// Otherwise, we're going to give these variables a value. They'll be
136+
// The user might have provided a value for this externally or at the
137+
// file level, so we can also just pass it through.
138+
139+
if ctx.VariableCache.HasVariableDefinition(variable.Name) {
140+
if value, valueDiags := ctx.VariableCache.GetVariableValue(variable.Name); value != nil {
141+
diags = diags.Append(valueDiags)
142+
values[name] = value
143+
continue
144+
}
145+
} else {
146+
if value, valueDiags := ctx.VariableCache.EvaluateExternalVariable(name, variable); value != nil {
147+
diags = diags.Append(valueDiags)
148+
values[name] = value
149+
continue
150+
}
151+
}
152+
153+
// Finally, we're going to give these variables a value. They'll be
151154
// processed by the Terraform graph and provided a default value later
152155
// if they have one.
153156

0 commit comments

Comments
 (0)