Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changes/v1.15/ENHANCEMENTS-20260113-130449.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
kind: ENHANCEMENTS
body: "Terraform Test: Allow functions within mock blocks"
time: 2026-01-13T13:04:49.034917+01:00
custom:
Issue: "34672"
16 changes: 15 additions & 1 deletion internal/command/test_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -297,7 +297,6 @@ func TestTest_Runs(t *testing.T) {
},
"mocking-invalid": {
expectedErr: []string{
"Invalid outputs attribute",
"The override_during attribute must be a value of plan or apply.",
},
initCode: 1,
Expand Down Expand Up @@ -418,6 +417,21 @@ func TestTest_Runs(t *testing.T) {
"no-tests": {
code: 0,
},
"simple_pass_function": {
expectedOut: []string{"2 passed, 0 failed."},
code: 0,
expectedResourceCount: 0,
},
"mocking-invalid-outputs": {
expectedErr: []string{
"Invalid outputs attribute",
},
code: 1,
},
"mock-sources-inline": {
expectedOut: []string{"2 passed, 0 failed."},
code: 0,
},
}
for name, tc := range tcs {
t.Run(name, func(t *testing.T) {
Expand Down
27 changes: 27 additions & 0 deletions internal/command/testdata/test/mock-sources-inline/main.tf
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
terraform {
required_providers {
test = {
source = "hashicorp/test"
}
}
}

provider "test" {
alias = "secondary"
}

resource "test_resource" "foo" {
value = "foo"
}

resource "test_resource" "bar" {
provider = test.secondary
value = "bar"
}

output "foo" {
value = test_resource.foo.id
}
output "bar" {
value = test_resource.bar.id
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@

mock_provider "test" {
source = "./testing/base"

mock_resource "test_resource" {
defaults = {
id = "local-mock-id" // should override the file-based mock
}
}
}

mock_provider "test" {
source = "./testing/base"
alias = "secondary"
}

run "test_foo" {
assert {
condition = output.foo == "local-mock-id"
error_message = "invalid value"
}
}


run "test_bar" {
assert {
condition = output.bar == "file-mock-id"
error_message = "invalid value"
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
# If read, this file should cause issues. But, it should be ignored.

mock_resource "test_resource" {}

mock_data "test_resource" {}

override_resource {
target = test_resource.foo
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
mock_resource "test_resource" {
defaults = {
id = "file-mock-id"
}
}
15 changes: 15 additions & 0 deletions internal/command/testdata/test/mocking-invalid-outputs/main.tf
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
terraform {
required_providers {
test = {
source = "hashicorp/test"
}
}
}

variable "instances" {
type = number
}

resource "test_resource" "primary" {
count = var.instances
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
override_resource {
target = test_resource.primary
values = "should be an object" // invalid
}

variables {
instances = 2
}

run "test" {
# We won't even execute this, as the configuration isn't valid.
}
7 changes: 7 additions & 0 deletions internal/command/testdata/test/simple_pass_function/main.tf
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
resource "test_resource" "foo" {
value = "foo"
}

resource "test_resource" "bar" {
value = "bar"
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
mock_provider "test" {
mock_resource "test_resource" {
defaults = {
id = format("f-%s", "foo")
}
}
}

override_resource {
target = test_resource.bar
values = {
id = format("%s-%s", uuid(), "bar")
}
}

run "validate_test_resource_foo" {
assert {
condition = test_resource.foo.id == "f-foo"
error_message = "invalid value"
}
}

run "validate_test_resource_bar" {
assert {
condition = length(test_resource.bar.id) > 10
error_message = "invalid value"
}
}
14 changes: 0 additions & 14 deletions internal/configs/config_build_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,6 @@ import (
"testing"

"github.com/davecgh/go-spew/spew"
"github.com/zclconf/go-cty/cty"

version "github.com/hashicorp/go-version"
"github.com/hashicorp/hcl/v2"
Expand Down Expand Up @@ -394,19 +393,6 @@ func TestBuildConfig_WithMockDataSourcesInline(t *testing.T) {
if cfg == nil {
t.Fatal("got nil config; want non-nil")
}

provider := cfg.Module.Tests["main.tftest.hcl"].Providers["aws"]

// This time we want to check that the mock data defined inline took
// precedence over the mock data defined in the data files.
defaults := provider.MockData.MockResources["aws_s3_bucket"].Defaults
expected := cty.ObjectVal(map[string]cty.Value{
"arn": cty.StringVal("aws:s3:::bucket"),
})

if !defaults.RawEquals(expected) {
t.Errorf("expected: %s\nactual: %s", expected.GoString(), defaults.GoString())
}
}

func TestBuildConfig_WithNestedTestModules(t *testing.T) {
Expand Down
38 changes: 11 additions & 27 deletions internal/configs/mock_provider.go
Original file line number Diff line number Diff line change
Expand Up @@ -181,6 +181,7 @@ type MockResource struct {
Type string

Defaults cty.Value
RawExpr hcl.Expression

// UseForPlan is true if the values should be computed during the planning
// phase.
Expand Down Expand Up @@ -208,6 +209,11 @@ type Override struct {
Target *addrs.Target
Values cty.Value

BlockName string

// The raw expression of the values/outputs block
RawExpr hcl.Expression

// UseForPlan is true if the values should be computed during the planning
// phase.
UseForPlan bool
Expand Down Expand Up @@ -325,14 +331,12 @@ func decodeMockResourceBlock(block *hcl.Block, useForPlanDefault bool) (*MockRes
}

if defaults, exists := content.Attributes["defaults"]; exists {
var defaultDiags hcl.Diagnostics
resource.DefaultsRange = defaults.Range
resource.Defaults, defaultDiags = defaults.Expr.Value(nil)
diags = append(diags, defaultDiags...)
resource.RawExpr = defaults.Expr
} else {
// It's fine if we don't have any defaults, just means we'll generate
// values for everything ourselves.
resource.Defaults = cty.NilVal
resource.RawExpr = hcl.StaticExpr(cty.EmptyObjectVal, hcl.Range{})
}

useForPlan, useForPlanDiags := useForPlan(content, useForPlanDefault)
Expand Down Expand Up @@ -453,6 +457,7 @@ func decodeOverrideBlock(block *hcl.Block, attributeName string, blockName strin
Source: source,
Range: block.DefRange,
TypeRange: block.TypeRange,
BlockName: blockName,
}

if target, exists := content.Attributes["target"]; exists {
Expand All @@ -472,41 +477,20 @@ func decodeOverrideBlock(block *hcl.Block, attributeName string, blockName strin
Subject: override.Range.Ptr(),
})
}

if attribute, exists := content.Attributes[attributeName]; exists {
var valueDiags hcl.Diagnostics
override.ValuesRange = attribute.Range
override.Values, valueDiags = attribute.Expr.Value(nil)
diags = append(diags, valueDiags...)
override.RawExpr = attribute.Expr
} else {
// It's fine if we don't have any values, just means we'll generate
// values for everything ourselves. We set this to an empty object so
// it's equivalent to `values = {}` which makes later processing easier.
override.Values = cty.EmptyObjectVal
override.RawExpr = hcl.StaticExpr(cty.EmptyObjectVal, hcl.Range{})
}

useForPlan, useForPlanDiags := useForPlan(content, useForPlanDefault)
diags = append(diags, useForPlanDiags...)
override.UseForPlan = useForPlan

if !override.Values.Type().IsObjectType() {

var attributePreposition string
switch attributeName {
case "outputs":
attributePreposition = "an"
default:
attributePreposition = "a"
}

diags = diags.Append(&hcl.Diagnostic{
Severity: hcl.DiagError,
Summary: fmt.Sprintf("Invalid %s attribute", attributeName),
Detail: fmt.Sprintf("%s blocks must specify %s %s attribute that is an object.", blockName, attributePreposition, attributeName),
Subject: override.ValuesRange.Ptr(),
})
}

return override, diags
}

Expand Down
6 changes: 0 additions & 6 deletions internal/configs/parser_config_dir_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -280,12 +280,6 @@ func TestParserLoadTestFiles_Invalid(t *testing.T) {
"invalid_data_override_target.tftest.hcl:8,3-24: Invalid override target; You can only target data sources from override_data blocks, not module.child.",
"invalid_data_override_target.tftest.hcl:3,3-31: Invalid override target; You can only target data sources from override_data blocks, not aws_instance.target.",
},
"invalid_mock_data_sources": {
"invalid_mock_data_sources.tftest.hcl:7,13-16: Variables not allowed; Variables may not be used here.",
},
"invalid_mock_resources": {
"invalid_mock_resources.tftest.hcl:7,13-16: Variables not allowed; Variables may not be used here.",
},
"invalid_module_override": {
"invalid_module_override.tftest.hcl:5,1-16: Missing target attribute; override_module blocks must specify a target address.",
"invalid_module_override.tftest.hcl:11,3-9: Unsupported argument; An argument named \"values\" is not expected here.",
Expand Down
4 changes: 2 additions & 2 deletions internal/moduletest/graph/apply.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ import (
// testApply defines how to execute a run block representing an apply command
//
// See also: (n *NodeTestRun).testPlan
func (n *NodeTestRun) testApply(ctx *EvalContext, variables terraform.InputValues, providers map[addrs.RootProviderConfig]providers.Interface, mocks map[addrs.RootProviderConfig]*configs.MockData, waiter *operationWaiter) {
func (n *NodeTestRun) testApply(ctx *EvalContext, variables terraform.InputValues, providers map[addrs.RootProviderConfig]providers.Interface, waiter *operationWaiter) {
file, run := n.File(), n.run
config := run.ModuleConfig
key := n.run.Config.StateKey
Expand All @@ -37,7 +37,7 @@ func (n *NodeTestRun) testApply(ctx *EvalContext, variables terraform.InputValue
tfCtx, _ := terraform.NewContext(n.opts.ContextOpts)

// execute the terraform plan operation
_, plan, planDiags := plan(ctx, tfCtx, file.Config, run.Config, run.ModuleConfig, setVariables, providers, mocks, waiter)
_, plan, planDiags := plan(ctx, tfCtx, file.Config, run.Config, run.ModuleConfig, setVariables, providers, waiter)

// Any error during the planning prevents our apply from
// continuing which is an error.
Expand Down
17 changes: 17 additions & 0 deletions internal/moduletest/graph/eval_context.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ import (
"github.com/hashicorp/terraform/internal/lang"
"github.com/hashicorp/terraform/internal/lang/langrefs"
"github.com/hashicorp/terraform/internal/moduletest"
"github.com/hashicorp/terraform/internal/moduletest/mocking"
teststates "github.com/hashicorp/terraform/internal/moduletest/states"
"github.com/hashicorp/terraform/internal/providers"
"github.com/hashicorp/terraform/internal/states"
Expand Down Expand Up @@ -89,6 +90,9 @@ type EvalContext struct {
// repair is true if the test suite is being run in cleanup repair mode.
// It is only set when in test cleanup mode.
repair bool

overrides map[string]*mocking.Overrides
overrideLock sync.Mutex
}

type EvalContextOpts struct {
Expand Down Expand Up @@ -135,6 +139,7 @@ func NewEvalContext(opts EvalContextOpts) *EvalContext {
mode: opts.Mode,
deferralAllowed: opts.DeferralAllowed,
evalSem: terraform.NewSemaphore(opts.Concurrency),
overrides: make(map[string]*mocking.Overrides),
}
}

Expand Down Expand Up @@ -720,6 +725,18 @@ func (ec *EvalContext) PriorRunsCompleted(runs map[string]*moduletest.Run) bool
return true
}

func (ec *EvalContext) SetOverrides(run *moduletest.Run, overrides *mocking.Overrides) {
ec.overrideLock.Lock()
defer ec.overrideLock.Unlock()
ec.overrides[run.Name] = overrides
}

func (ec *EvalContext) GetOverrides(runName string) *mocking.Overrides {
ec.overrideLock.Lock()
defer ec.overrideLock.Unlock()
return ec.overrides[runName]
}

// evaluationData augments an underlying lang.Data -- presumably resulting
// from a terraform.Context.PlanAndEval or terraform.Context.ApplyAndEval call --
// with results from prior runs that should therefore be available when
Expand Down
22 changes: 19 additions & 3 deletions internal/moduletest/graph/node_provider.go
Original file line number Diff line number Diff line change
Expand Up @@ -78,9 +78,25 @@ func (n *NodeProviderConfigure) Execute(ctx *EvalContext) {
return
}

body, hclDiags := hcldec.Decode(n.Config.Config, spec, hclContext)
n.File.AppendDiagnostics(moreDiags)
if hclDiags.HasErrors() {
// This means we are using a mock provider, which may contain not-yet-evaluated
// mock data, so we will evaluate the data here.
if mock, ok := n.Provider.(*providers.Mock); ok {
for _, res := range mock.Data.MockResources {
values, exprHclDiags := res.RawExpr.Value(hclContext)
moreDiags = moreDiags.Append(exprHclDiags)
res.Defaults = values
}
for _, res := range mock.Data.MockDataSources {
values, exprHclDiags := res.RawExpr.Value(hclContext)
moreDiags = moreDiags.Append(exprHclDiags)
res.Defaults = values
}
}

body, decHclDiags := hcldec.Decode(n.Config.Config, spec, hclContext)
moreDiags = moreDiags.Append(decHclDiags)
if moreDiags.HasErrors() {
n.File.AppendDiagnostics(moreDiags)
ctx.SetProviderStatus(n.Addr, moduletest.Error)
return
}
Expand Down
Loading