Testing Terraform Code Part Two: Unit and Integration Testing

Testing Terraform Code Part Two: Unit and Integration Testing

13 February 2024

Jonathan Bernaerts

Key takeaways

  • Terratest is great for integration testing and end-to-end testing but requires learning Go.
  • Terraform Test is a new unit testing feature in Terraform itself and uses HCL.
  • Both tools have their shortcomings when it comes to resource deletion and state retrieval.
  • Terraform Test is quickly evolving, so make sure to keep an eye on its documentation.

We always test our applications, but the infrastructure sometimes gets left behind. That’s why we decided to write a two-part series on infrastructure testing with a focus on Terraform.

In the previous part, we discussed three static analysis tools: tfsec, Terrascan and drifctl. In this second part, we’ll look at integration testing with Terratest and unit testing with Terraform Test. Let’s dive in!

Terratest: The Go-to Integration Testing Framework

The first tool we’ll take a look at is Terratest, a testing framework specifically designed for testing Terraform configurations and infrastructure resources. You’ll have to write tests in Go, which gives you a lot of flexibility and extensibility – but you may have to learn it first. Terratest also supports integration with plenty of popular CI/CD tools and refuses cost, so you will only have infrastructure running while running the tests.

When we tried using this tool, we immediately found answers to our questions. For example, if you want to extend your testing to infrastructure levels, you could rewrite your tests in Go. You could create an environment to run your tests, which lets you test your Terraform and application in one single action. Check out this example:

package test 
 
import ( 
	"crypto/tls" 
	"fmt" 
	"testing" 
	"time" 
 
	"github.com/gruntwork-io/terratest/modules/aws" 
	http_helper "github.com/gruntwork-io/terratest/modules/http-helper" 
	"github.com/gruntwork-io/terratest/modules/random" 
	"github.com/gruntwork-io/terratest/modules/terraform" 
) 
 
func TestTerraformHttpExample(t *testing.T) { 
	t.Parallel() 
 
	uniqueID := random.UniqueId() 
 
	instanceName := fmt.Sprintf("terratest-http-example-%s", uniqueID) 
	instanceText := fmt.Sprintf("Hello, %s!", uniqueID) 
	awsRegion := aws.GetRandomStableRegion(t, nil, nil) 
 
	instanceType := aws.GetRecommendedInstanceType(t, awsRegion, []string{"t3.micro", "t3.small"}) 
 
	// Construct the terraform options with default retriable errors to handle the most common retriable errors in 
	// terraform testing. 
	terraformOptions := terraform.WithDefaultRetryableErrors(t, &terraform.Options{ 
		// The path to where our Terraform code is located 
		TerraformDir: "../webexample", 
 
		// Variables to pass to our Terraform code using -var options 
		Vars: map[string]interface{}{ 
			"aws_region":    awsRegion, 
			"instance_name": instanceName, 
			"instance_text": instanceText, 
			"instance_type": instanceType, 
		}, 
	}) 
 
	defer terraform.Destroy(t, terraformOptions) 
 
	terraform.InitAndApply(t, terraformOptions) 
	instanceURL := terraform.Output(t, terraformOptions, "instance_url") 
 
	tlsConfig := tls.Config{} 
 
 
	maxRetries := 30 
	timeBetweenRetries := 5 * time.Second 
 
	http_helper.HttpGetWithRetry(t, instanceURL, &tlsConfig, 200, instanceText, maxRetries, timeBetweenRetries) 
} 

Here’s how it works:

  • In this function, we declare that this test is allowed to run parallel with others and everything else needed for our project. In this case, our AWS region and instance information.
  • Our Terraform options declare where it can find our code and give it the required variables.
  • Next, we declare that the test can delete the infrastructure afterward, before it can be applied.
  • The last part is our test itself. We get the instance URL out of Terraform so we can do an HTTP request on this URL.
  • Once we’ve configured some retries and a delay between them, the infrastructure is fully set up.

When creating a test, it is crucial that it works. When the test crashes, it will not destroy your infrastructure or expose its state file. This means that you will have to delete all your infrastructure manually. That’s quite a shortcoming when you are creating your tests and want to check if they are written correctly.

The solution? When you create your test, make sure to check it against an existing dev environment without deploying and destroying anything. Of course, you’ll have to use the same variables as this environment.

Pros: 

  • Simplicity: Terratest has native support for Terraform.
  • Flexibility: It lets you write tests in Go.
  • Integration: Terraform supports integration with popular CI/CD tools.
  • Cost-effective: It refuses cost, so you only use infrastructure while running tests.

Cons: 

  • Learning curve: You will have to learn Go if you’re not already familiar with it.
  • Limited support: Terratest only offers support for a few other IaC tools.

Terraform Test: The Built-in Unit Testing Solution

Terraform Test is a relatively new feature in Terraform itself. All you need to do is install Terraform version 15 or higher, and it will be available. It is written in HCL (HashiCorp Configuration Language), the same language as your other infrastructure code, and can live next to your code. Since it’s such a new product, Terraform Test is quickly evolving. For example, it initially had a resource block with the test_assertions type, but now it uses run blocks.

Terraform tests involve files ending with the .tftest.hcl extension and, optionally, helper modules that let you create test-specific resources and data sources separate from the primary configuration.

When you execute the test command, Terraform automatically looks for .tftest.hcl files in both the root directory and the tests directory. The -test-directory flag also lets you specify a different directory for Terraform to search.

If you have a module that relies on another module, you can define another helper module to provide these resources. The simplest test should look like this:

 

run "vpc_tests" { 
    module { 
        source = "./vpc" 
    } 
}

In this context, you can deploy your VPC module without incorporating any variables or validations initially. If variables are necessary, you can easily introduce a variables block and define the required parameters within a map.

 

variables { 
    vpc_name = "website-test" 
  } 

You also have the option of depending on the output of another module.  

variables { 
    vpc_name = "${run.vpc_tests.vpc_name}"

Finally, you can implement checks using the assert block, which mandates two parameters: a condition representing your test, which can be a simple equation or any function you choose, and an error message. Here are two examples:

 

assert { 
    condition     = aws_vpc.vpc.name == "website-test" 
    error_message = "Invalid vpc name" 
  } 
# Check index.html hash matches 
assert { 
   condition     = aws_s3_object.index.etag == filemd5("./www/index.html") 
   error_message = "Invalid eTag for index.html" 
 }

In version 1.7, Terraform added mock providers to its testing feature. These generate simulated data for all the computed attributes during the application process. No actual infrastructure needs to be established, so there is no need to use credentials. Keep in mind that runs and mock providers can only be used in .tftest.hcl files. You can use them simultaneously with .tf files call your modules, but mock and run blocks are not allowed in them.

 

mock_provider "aws" { 
  alias = "fake" 
} 
run "use_mocked_provider" { 
  providers = { 
    aws = aws.fake 
  } 
}

Pros: 

  • Simplicity: Terraform Test is a built-in feature of Terraform.
  • Flexibility: It lets you write tests in HCL, the same language as your other infrastructure code. No need to learn Go!
  • Cost-effective: Mock providers let you test without establishing actual infrastructure.

Cons: 

  • Limited support: Terraform Test is only available in Terraform version 15 or higher.
  • Limited functionality: It may not have all the features and capabilities of other testing frameworks.

Terratest vs. Terraform Test: A Comparison

Terratest and Terraform Test both serve the common goal of testing Infrastructure as Code (Iac) with Terraform, but they differ in their focus and approach.

  • Terratest is tailored for integration testing and uses the Go programming language for flexible test scenarios, while Terraform Test is designed for unit testing using HCL, the language of Terraform itself.
  • Terratest integrates into Go testing frameworks and supports diverse testing scenarios, making it also suitable for end-to-end testing. On the other hand, Terraform Test is simpler and aligns with Terraform’s conventions, making it accessible for users.

Both tools also (currently) have some limitations. During test development, they can provide a less-than-optimal user experience. If your test encounters an issue and crashes, your program comes to a halt, leaving you with no option but to manually delete the resources. Unfortunately, retrieving the state file is not supported either. However, Terraform Test is evolving rapidly, with its newest release introducing the ability to mock infrastructure for your tests, making the state file issue less of a problem.

Speaking of which: if you plan on using Terraform Test, we recommend checking out the official HashiCorp test documentation. Since the testing framework and its documentation are continuously evolving, this article may become less relevant over time.

Ultimately, your choice will depend on which kinds of tests you want to write and run, which programming language you prefer, and how you want to integrate them. Whichever one you choose, make sure to invest in testing, because it will help you make fewer mistakes and improve your code in the future.

Want to stay in touch with all things DevOps? Check out our other blogs!

Related posts
No Comments

Sorry, the comment form is closed at this time.