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. |

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.



