Test a Module Contract

ShellBeginner
Practice Now

Introduction

A configuration can be formatted and structurally valid while labeling a report environment incorrectly. Your team needs executable checks for a small storage module's inputs, outputs, and planned tags before it trusts a delivery plan.

You will write a module contract, test two supported environments and an unsupported input, and make a genuine business defect stop the job. You should understand module variables/outputs and the formatting/validation gates from the previous lab. Existing reference storage must remain unchanged.

Certification Relevance

Certification Exam task Practice
DevOps Engineer – Professional (DOP-C02) Task 1.2 Foundational practice integrating executable module assertions into an automated quality gate.

Module inputs are checked by plan-mode assertions before a delivery plan, while reference storage is preserved.

Define the Input Contract

In this step, you will define which environment inputs the storage module accepts. Input validation rejects an unsupported request before creating a resource plan.

Enter the supplied module and load the prepared tools.

cd /home/labex/project/report-module
source environment.sh

Read its interface and resource implementation.

cat variables.tf main.tf outputs.tf pipeline.json

environment selects preview or review; bucket_prefix distinguishes a caller's storage names. The bucket name combines both inputs, and the Environment tag and output must identify the requested environment. The supplied main.tf, outputs.tf, provider.tf, and run-job.py are preservation items: keep their final contents unchanged. Your work belongs in variables.tf, tests/, and pipeline.json.

Add a validation block to environment. contains checks membership in the supported list; a false condition produces the provided error message. The quoted here-document writes literal HCL.

cat > variables.tf <<'EOF'
variable "environment" {
  type    = string
  default = "preview"
  validation {
    condition     = contains(["preview", "review"], var.environment)
    error_message = "Choose preview or review."
  }
}
variable "bucket_prefix" {
  type    = string
  default = "delivery-contract"
}
EOF

Format your change.

terraform fmt

Try an unsupported request. -var supplies a value for this invocation. This command intentionally returns native exit 1; it must report Choose preview or review., rather than propose a production bucket.

terraform plan -input=false -var=environment=production

Validation defines the accepted interface. In the next step, tests will exercise both accepted values and this expected rejection.

Write Assertions and Observe a Real Failure

In this step, you will use native terraform test to check the module's planned behavior. Formatting and validation can accept a hard-coded but incorrect output; an assertion compares the computed result with the requested input.

Terraform reads .tftest.hcl files under tests/. Each run is an independent test scenario. Explicit command = plan evaluates a plan without applying it. Keep test files under tests/ in .tftest.hcl form. In this lab every run must use plan mode and the supplied provider/module; alternate modules, provider mocks, and overrides are outside the task. Keep bucket_prefix inherited from the suite input: do not assign it in file-level or run-level test variables, so a caller can select another legitimate prefix. These tests check known plan values, not actual creation, runtime permissions, or a deployed application.

Create the test directory. mkdir -p creates it if necessary and accepts an existing directory.

mkdir -p tests

Write assertions for the default and alternate environment. var refers to the run's inputs, resource references inspect planned values, and output refers to module outputs. expect_failures requires the named variable validation to fail; it does not turn arbitrary provider or network errors into successful tests.

cat > tests/contract.tftest.hcl <<'EOF'
run "preview_contract" {
  command = plan
  assert {
    condition     = output.environment == var.environment
    error_message = "The output must identify the requested environment."
  }
  assert {
    condition     = aws_s3_bucket.reports.bucket == "${var.bucket_prefix}-${var.environment}"
    error_message = "The bucket name must use the requested prefix and environment."
  }
  assert {
    condition     = aws_s3_bucket.reports.tags.Environment == var.environment
    error_message = "The planned tag must identify the requested environment."
  }
}
run "review_contract" {
  command = plan
  variables { environment = "review" }
  assert {
    condition     = output.environment == var.environment
    error_message = "The alternate environment must not use the default label."
  }
  assert {
    condition     = aws_s3_bucket.reports.bucket == "${var.bucket_prefix}-${var.environment}"
    error_message = "The alternate bucket must use its requested inputs."
  }
  assert {
    condition     = aws_s3_bucket.reports.tags.Environment == var.environment
    error_message = "The alternate environment tag must match its input."
  }
}
run "unsupported_environment" {
  command         = plan
  variables { environment = "production" }
  expect_failures = [var.environment]
}
EOF

Format the tests and module files recursively.

terraform fmt -recursive

Run the test suite.

terraform test

Expect three passing runs and a success summary. No test bucket is deployed. Input and output values come from an actual Terraform plan using the configured provider.

Prove that your assertions catch a plausible defect. Temporarily replace only the environment output with a fixed default label; retain the bucket-name output.

cat > outputs.tf <<'EOF'
output "environment" {
  value = "preview"
}
output "bucket_name" {
  value = aws_s3_bucket.reports.bucket
}
EOF

Run the suite again. This command intentionally returns native exit 1. The alternate-environment assertion should fail, showing that valid HCL can still violate the module contract.

terraform test

Restore the supplied output implementation from Git. This removes your temporary defect while retaining the validation and tests you wrote.

git restore -- outputs.tf

Run the corrected implementation through the same assertions.

terraform test

All runs should pass again. Tests must accept legitimate inputs rather than merely recognize one default output.

Make Tests Stop the Delivery Job

In this step, you will place module tests between the static gates and planning. The supplied executor is a preparation item; edit the stage configuration rather than changing run-job.py.

Add test after fmt/validate and before plan.

cat > pipeline.json <<'EOF'
{"stages":["fmt","validate","test","plan"]}
EOF

Run the complete job.

python3 run-job.py

All four stages should pass. The final plan can return native 2 because this module has not been applied; the runner treats that as a successful preview. A failed test must instead stop the job before its plan stage. No apply stage exists.

Run the suite with another legitimate bucket prefix. This -var changes the suite's input without editing the module or its tests.

terraform test -var=bucket_prefix=audit-contract

The same three runs should pass. For this exercise, keep bucket_prefix inherited from the caller in every run: do not assign it in test-file or run-level variables blocks. Such assignments override CLI test inputs and would stop this command from exercising its requested prefix. Environment overrides remain necessary to exercise both supported environments. Assertions built from the requested inputs continue to work when the caller chooses a different valid name.

Read the existing reference report directly from storage. These prepared buckets belong to another environment; do not destroy or modify them.

aws s3   cp s3://delivery-reference-reports/report.txt -

It should still say reference report v1. Check the neighboring data too.

aws s3   cp s3://delivery-reference-neighbor/keep.txt -

Expect preserve contract neighbor. Your tests created no storage resources, so there is no test stack to destroy. Keep the input contract, tests, and pipeline configuration for the final check; the temporary output defect has already been removed.

Summary

You defined a supported input contract, used native plan-mode Terraform tests to compare planned outputs and tags with inputs, and observed a real assertion failure. You then made tests part of the delivery quality gate while preserving existing data and business state.

Plan-mode tests do not establish deployed behavior. Later delivery steps combine these checks with a reviewed concrete plan and actual controlled application. See the official Terraform test reference and test configuration documentation for the mechanisms practiced here.