2024 Recap & Outlook for 2025
2024 Recap & Outlook for 2025 at Infralovers 2024 was a year of innovations and new learning opportunities for us. We continuously expanded our training
Infrastructure as Code (IaC) has revolutionized the way we manage cloud resources, and Terraform has become a leading tool in this space. To ensure the reliability and stability of Terraform configurations, testing is crucial. This blog explores various approaches to testing in Terraform, helping you adopt best practices for safe infrastructure management.
Testing Terraform code is essential for:
Terraform provides several methods for testing infrastructure, ranging from unit tests to acceptance tests. Let's explore each approach.
Unit testing is a fundamental practice in software development, and it applies to Terraform plugins as well. Unit tests are used to verify the correctness of small, isolated units of code, such as helper functions or methods that transform API responses into Terraform-friendly data structures. These tests typically do not require network connections and focus on verifying individual pieces of logic in isolation.
One common use case in Terraform plugins is flattening complex data structures returned by APIs into simpler formats that can be stored in Terraform state. For example, AWS security group rules often come in a nested structure, and a flattener function is needed to transform this data into a format that Terraform can manage. Below is an example of a unit test that verifies a flattening function for AWS security group rules:
1func TestFlattenSecurityGroups(t *testing.T) {
2 cases := []struct {
3 ownerId *string
4 pairs []*ec2.UserIdGroupPair
5 expected []*GroupIdentifier
6 }{
7 // Test cases here...
8 }
9 for _, c := range cases {
10 out := flattenSecurityGroups(c.pairs, c.ownerId)
11 if !reflect.DeepEqual(out, c.expected) {
12 t.Fatalf("Error matching output and expected: %#v vs %#v", out, c.expected)
13 }
14 }
15}
This test case checks that the flattening function correctly transforms different variations of AWS security group rules, ensuring the logic works as expected.
Acceptance tests are a critical part of testing Terraform plugins. These tests are designed to verify the actual behavior of Terraform when interacting with real infrastructure. The goal of acceptance tests is to ensure that Terraform can create, modify, and destroy resources correctly, and that the state file accurately reflects the resource configuration.
Terraform's testing framework supports a variety of test patterns that make it easy to write and run acceptance tests. Acceptance tests can be run on any environment capable of running go test, such as a local workstation command line, or continuous integration runner, such as GitHub Actions.
The most basic acceptance test verifies that a resource can be created and that its attributes are stored correctly in the Terraform state file. This test ensures that Terraform can apply a configuration without errors and that the remote resource matches the state file.
1package example
2// example.Widget represents a concrete Go type that represents an API resource
3func TestAccExampleWidget_basic(t *testing.T) {
4 var widget example.Widget
5 // generate a random name for each widget test run, to avoid
6 // collisions from multiple concurrent tests.
7 // the acctest package includes many helpers such as RandStringFromCharSet
8 // See https://pkg.go.dev/github.com/hashicorp/terraform-plugin-sdk/helper/acctest
9 rName := acctest.RandStringFromCharSet(10, acctest.CharSetAlphaNum)
10 resource.Test(t, resource.TestCase{
11 PreCheck: func() { testAccPreCheck(t) },
12 Providers: testAccProviders,
13 CheckDestroy: testAccCheckExampleResourceDestroy,
14 Steps: []resource.TestStep{
15 {
16 // use a dynamic configuration with the random name from above
17 Config: testAccExampleResource(rName),
18 // compose a basic test, checking both remote and local values
19 ConfigStateChecks: []statecheck.StateCheck{
20 // custom state check - query the API to retrieve the widget object
21 stateCheckExampleResourceExists("example_widget.foo", &widget),
22 // custom state check - verify remote values
23 stateCheckExampleWidgetValues(widget, rName),
24 // built-in state checks - verify local (state) values
25 statecheck.ExpectKnownValue("example_widget.foo", tfjsonpath.New("active"), knownvalue.Bool(true)),
26 statecheck.ExpectKnownValue("example_widget.foo", tfjsonpath.New("name"), knownvalue.StringExact(rName)),
27 },
28 },
29 },
30 })
31}
32// testAccExampleResource returns an configuration for an Example Widget with the provided name
33func testAccExampleResource(name string) string {
34 return fmt.Sprintf(`
35resource "example_widget" "foo" {
36 active = true
37 name = "%s"
38}`, name)
39}
This test verifies that a widget resource is created with the correct attributes and that those attributes are properly stored in both the Terraform state file and the remote API.
Regression tests are essential for ensuring that bug fixes or updates to Terraform plugins do not introduce new issues. When a bug is reported, it should be followed by a regression test that verifies the issue is resolved without breaking existing functionality.
Regression tests are particularly useful for verifying that code changes intended to fix a bug do not unintentionally impact other parts of the codebase. These tests should be named clearly to reflect the issue being fixed and should be added alongside the bug fix to provide a comprehensive verification.
An example for regression testing can be seen here.
Terraform's native testing framework enables the validation of your module configuration by building ephemeral infrastructure. These tests run separately from the regular plan or apply workflows. Instead of modifying your existing infrastructure, tests create temporary resources that Terraform manages only during the testing phase. This approach ensures that your tests don't interfere with your existing state files, offering a safe environment to validate changes.
The syntax for Terraform tests is relatively simple, using .tftest.hcl
files for defining test cases and optional helper modules to manage any test-specific resources. Let’s explore how to implement these tests step by step.
The directory structure for Terraform native tests will look something like this:
1.
2├── LICENSE
3├── README.md
4├── main.tf
5├── outputs.tf
6├── terraform.tf
7├── tests
8│ ├── setup
9│ │ ├── main.tf
10│ └── website.tftest.hcl
11├── variables.tf
12└── www
13 ├── error.html
14 └── index.html
The main.tf
file defines the infrastructure to be tested (e.g. an S3 bucket). The tests
folder contains the test configuration.
Terraform tests are composed of two components:
.tftest.hcl
extension and contain the actual test logic.The tests
directory contains the website.tftest.hcl
test configuration file, which includes two primary blocks:
true
for the test to pass.Here’s an example of the website.tftest.hcl
file:
1run "setup_tests" {
2 module {
3 source = "./tests/setup"
4 }
5}
6run "create_bucket" {
7 command = apply
8 variables {
9 bucket_name = "${run.setup_tests.bucket_prefix}-aws-s3-website-test"
10 }
11 assert {
12 condition = aws_s3_bucket.s3_bucket.bucket == "${run.setup_tests.bucket_prefix}-aws-s3-website-test"
13 error_message = "Invalid bucket name"
14 }
15 assert {
16 condition = aws_s3_object.index.etag == filemd5("./www/index.html")
17 error_message = "Invalid eTag for index.html"
18 }
19 assert {
20 condition = aws_s3_object.error.etag == filemd5("./www/error.html")
21 error_message = "Invalid eTag for error.html"
22 }
23}
In this configuration:
run setup_tests
creates a random bucket name using the helper module.run create_bucket
checks if the bucket name and uploaded files match the expected values.To run the tests, initialize Terraform and install any required providers:
1$ terraform init
Next, run the tests using:
1$ terraform test
If successful, you’ll see an output like:
1Success! 2 passed, 0 failed.
To speed up testing and avoid the overhead of creating real resources, Terraform allows for mocking. Mocking simulates resources and data sources, eliminating the need to provision them.
Example of the real resource we want to mock:
1resource "aws_instance" "backend_api" {
2 ami = data.aws_ami.ubuntu.id
3 instance_type = "t3.micro"
4 tags = {
5 Name = "backend"
6 }
7}
Example of mocking an EC2 instance:
1override_resource {
2 target = aws_instance.backend_api
3}
4run "check_backend_api" {
5 assert {
6 condition = aws_instance.backend_api.tags.Name == "backend"
7 error_message = "Invalid name tag"
8 }
9}
This simulates an EC2 instance instead of creating it, allowing you to test configurations faster.
HCP Terraform integrates closely with module testing, providing several benefits for developers and organizations using Terraform. This allows you to upload your created modules to a private registry, which offers many functionalities:
1$ terraform test -cloud-run=app.terraform.io/ORG/s3-website-tests/aws
Testing with Terraform is an essential process to ensure that your infrastructure code is correct before deploying it to production. By using tests, we can validate our configurations with minimal impact on our existing resources. This article covered the essentials, from setting up tests to publishing them and running tests remotely in HCP Terraform.
As your Terraform configurations evolve, remember to keep tests updated to maintain infrastructure predictability and avoid introducing breaking changes.
You are interested in our courses or you simply have a question that needs answering? You can contact us at anytime! We will do our best to answer all your questions.
Contact us