Methods for Testing Terraform


Bicycle

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.

Why Test Terraform Code?

Testing Terraform code is essential for:

  • Preventing Misconfigurations: Catch errors before they affect production.
  • Ensuring Consistency: Verify infrastructure behaves as expected across environments.
  • Facilitating Collaboration: Improve code quality in teams through automated checks.
  • Compliance and Security: Ensure infrastructure meets compliance standards.

Types of Testing in Terraform

Terraform provides several methods for testing infrastructure, ranging from unit tests to acceptance tests. Let's explore each approach.

1. Unit Testing for Terraform Plugins

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.

Example: Flattening AWS Security Group Rules

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.

2. Acceptance Testing for Terraform Plugins

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.

Example: Basic Test to Verify Attributes

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.

3. Regression Testing for Terraform Plugins

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.

4. Terraform Native Testing Framework

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.

Example: AWS hosted website

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:

  • Test files: These files end with the .tftest.hcl extension and contain the actual test logic.
  • Helper modules: These are optional and help create test-specific resources or data sources.

The tests directory contains the website.tftest.hcl test configuration file, which includes two primary blocks:

  • Run Blocks: Each run block applies a specific Terraform command (such as apply or plan) and performs assertions on the resulting state.
  • Assert Blocks: Within a run block, assert blocks check conditions. These conditions must evaluate to 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.

Running the Tests

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.

Mocking Resources for Testing

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.

Using the tests in HCP Terraform

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:

  • When a module uses branch-based publishing and contains tests, HCP Terraform automatically runs these tests:
    • On every push to the configured branch
    • For any pull requests against that branch
  • Developers can trigger remote tests in HCP Terraform using the Terraform CLI locally, without committing changes to source control
1$ terraform test -cloud-run=app.terraform.io/ORG/s3-website-tests/aws
  • HCP Terraform allows you to configure environment variables for tests, including sensitive data like cloud provider credentials
  • This configuration is done through the HCP Terraform interface, enhancing security by keeping credentials out of source control

Benefits of using HCP Terraform

  • Continuous Validation: Ensures that module changes don't introduce breaking changes
  • Secure Testing Environment:
    • Uses configured environment variables for tests, including sensitive data
    • Eliminates the need to store cloud credentials on local machines
  • Version Control Integration: Automatically runs tests on pushes and pull requests, catching issues early in the development process
  • Centralized Test History: HCP Terraform provides a history of all test runs, allowing easy tracking and review of test results over time
  • Detailed Test Reporting:
    • Shows the status of each test step
    • Provides detailed output for failed tests to aid in troubleshooting
  • Remote Testing Capability: Allows running tests in HCP Terraform's environment from the local CLI, ensuring consistent test environments
  • Enhanced Collaboration: Team members can view and analyze test results directly in HCP Terraform, facilitating easier collaboration and issue resolution

Conclusion

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.

Go Back explore our courses

We are here for you

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