LAB 08 – Testing and code quality¶
Story: After last week's outage (a storage account named with an underscore broke the pipeline at 2 a.m.), GreenGrid wants mistakes caught on the laptop and in pull requests – in seconds, without touching Azure.
| Duration | ~45 min |
| You create | tests/greengrid.tftest.hcl, tests/mocks/azurerm/, checks.tf, .tflint.hcl |
| Solution | solutions/LAB08/ (or ./bin/catchup 8) |
| Layer | Tool | Catches |
|---|---|---|
| formatting | terraform fmt -check |
style |
| syntax & types | terraform validate |
typos, wrong types, missing arguments |
| linting | tflint + azurerm ruleset | invalid SKUs/sizes, deprecated syntax, unused declarations |
| unit tests | terraform test + mock providers |
naming, tagging, logic, validations – no Azure |
| integration tests | terraform test with real providers |
the real thing (slow, costs money) |
| runtime checks | check blocks |
assertions after every plan/apply |
| policy | Azure Policy, Terraform Policy, OPA/Conftest, Checkov | security and compliance |
Step 1 – Mock data¶
terraform test can replace a provider with a mock. The azurerm provider still validates IDs, so the mocks need realistic ones. Copy the prepared file:
mkdir -p tests/mocks/azurerm
cp ../../solutions/LAB08/tests/mocks/azurerm/azurerm.tfmock.hcl tests/mocks/azurerm/
grep -A3 'mock_resource "azurerm_storage_account"' tests/mocks/azurerm/azurerm.tfmock.hcl
Step 2 – Unit tests¶
tests/greengrid.tftest.hcl:
mock_provider "azurerm" {
source = "./tests/mocks/azurerm"
}
mock_provider "random" {}
# The import block (legacy.tf) cannot import from a mocked provider - give the resource fixed values instead
override_resource {
target = azurerm_resource_group.legacy
values = {
id = "/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/rg-jk-legacy"
}
}
# Deterministic names: pin the random suffix
override_resource {
target = random_string.suffix
values = {
result = "a1b2"
}
}
variables {
prefix = "jk"
environment = "dev"
}
run "names_follow_the_standard" {
command = apply
assert {
condition = azurerm_resource_group.main.name == "rg-jk-greengrid-dev"
error_message = "Resource group name does not follow rg-<prefix>-greengrid-<env>."
}
assert {
condition = output.storage_account_name == "stjkggdeva1b2"
error_message = "Storage account name must be st<prefix>gg<env><4 random chars>."
}
}
run "every_resource_is_tagged" {
command = plan
assert {
condition = alltrue([for k in ["project", "environment", "owner", "managed_by"] : contains(keys(output.tags), k)])
error_message = "Standard tags are missing."
}
}
run "rejects_an_invalid_prefix" {
command = plan
variables {
prefix = "Bad_Prefix"
}
expect_failures = [var.prefix]
}
run "rejects_an_unknown_environment" {
command = plan
variables {
environment = "production"
}
expect_failures = [var.environment]
}
✅ Checkpoint: Success! 4 passed, 0 failed. – in seconds, offline.
command = plan tests logic before anything exists; command = apply with mocks also gives you computed values (IDs, endpoints). expect_failures turns "this must be rejected" into a test.
Break it on purpose: change the storage name in locals.tf to "st-${var.prefix}..." and run the tests again – then revert.
Step 3 – A check block¶
checks.tf:
# Checks run after every plan and apply - they warn, they do not block.
check "storage_uses_https" {
assert {
condition = startswith(module.storage.primary_blob_endpoint, "https://")
error_message = "The blob endpoint must use HTTPS."
}
}
| Blocks the run? | Typical use | |
|---|---|---|
variable validation |
yes | wrong input |
precondition / postcondition |
yes | assumptions about a resource |
check |
no – warning | health of the whole system (can use its own data source, e.g. an HTTP probe) |
Step 4 – tflint¶
.tflint.hcl:
plugin "terraform" {
enabled = true
preset = "recommended"
}
plugin "azurerm" {
enabled = true
version = "0.33.0"
source = "github.com/terraform-linters/tflint-ruleset-azurerm"
}
Introduce mistakes and lint again: a VM size Standard_D2s_v99, an unused variable, a module without a pinned version.
tflint knows Azure: invalid SKUs, sizes and regions are found without an API call –
terraform validatecannot do that.
Step 5 – One command for the pipeline¶
This exact line runs in every pull request in LAB11.
Bonus¶
terraform test -filter=tests/greengrid.tftest.hcl -verbose– see the planned values per run.- Write a test for prod:
environment = "prod"must giveZRSreplication. (Hint: expose the replication type as an output, or test the storage module directly with a test inmodules/storage/tests/.) - Run a security scanner:
checkov -d .ortrivy config ..
Troubleshooting¶
| Symptom | Likely cause |
|---|---|
parsing "...": the number of segments didn't match |
a mock ID is not a real ARM ID – add a mock_resource with a proper id |
Cannot import resources from mock providers |
an import block in the config – add override_resource for its target |
| test asks for Azure credentials | mock_provider missing for a provider (azurerm, random, azapi…) |
tflint: plugin not found |
run tflint --init once |
Files for this lab: Think-Cube/terraform-azure-labs/labs/LAB08-testing-quality