Skip to main content

Command Palette

Search for a command to run...

Terraform unit testing

How to guide with Google Cloud provider example

Updated
5 min read
Terraform unit testing

Intro

As of Terraform v1.6 and later, there is now native support for writing tests. This guide shows how to start writing unit tests with Terraform.

You might already be familiar with aspects like mocking dependencies when doing unit testing within software development projects and how it differs from integration testing.

With infrastructure testing, the definition of a unit test is the ability to test logic within your code before it is applied. Specifically for Terraform, this means checking the plan that Terraform generates and NOT applying the infrastructure changes.

I'll do a future post on integration tests when the infrastructure has been applied.

TL;DR

GitHub link to the code

https://github.com/eggsy84/terraform_unit_testing_gcp

Scenario

For this guide, rather than a simple main.tf that creates a storage bucket or something like that, let's assume that you have a Terraform project that makes use of local modules. One of those modules you've called networking and creates the VPC setup.

The root main.tf will make use of the networking module and for now, I'll just have the networking module create a VPC and a couple of subnets.

This is the directory structure

├── README.md
├── main.tf
├── modules
│   ├── networking
│   │   ├── main.tf
│   │   ├── outputs.tf
│   │   ├── providers.tf
│   │   └── variables.tf
├── outputs.tf
├── providers.tf
└── variables.tf

The module and usage

The networking module has been created to create a VPC and two subnets within that VPC - one for a production network and one for the development network.

The module takes in two variables and using some string-based logic, names the subnets as per Google naming recommendations.

To test this module you can expect that one of the "units" you might wish to test is that the subnets abide by the naming conventions.

Here's the code of the module

# Example networking module creates a dev and production subnet
# with a specified region. 
# Naming style for networks loosely based on Google Best Practices
# https://cloud.google.com/architecture/best-practices-vpc-design#naming

resource "google_compute_network" "vpc_network" {
  name                      = "${var.company_name}-app"
  description               = "VPC network for the ${var.company_name} app"
  auto_create_subnetworks   = false
}

# Example name for europe-west2 and company name Acme company app
# acme-app-eu-we2-prod-subnet
# acme-app-eu-we2-dev-subnet
resource "google_compute_subnetwork" "dev" {
  name          = "${var.company_name}-app-${substr(var.region, 0, 2)}-${substr(regex("-[a-z]+", var.region), 1, 2)}${substr(var.region, length(var.region) -1, 1)}-dev-subnet"
  ip_cidr_range = "10.128.0.0/20"
  region        = var.region
  network       = google_compute_network.vpc_network.id
}

resource "google_compute_subnetwork" "prod" {
  name          = "${var.company_name}-app-${substr(var.region, 0, 2)}-${substr(regex("-[a-z]+", var.region), 1, 2)}${substr(var.region, length(var.region) -1, 1)}-prod-subnet"
  ip_cidr_range = "10.154.0.0/20"
  region        = var.region
  network       = google_compute_network.vpc_network.id
}

The unit test

Looking at how the subnet names are defined, it feels like some business logic that should be tested and a good case for a unit case. We might want to test out some of that crazy string logic around different regions and company names.

In this guide, I'll take the approach that tests of the networking module are considered unit tests and shall be located within the module directory. In a later post, we'll create integration tests that sit within the root directory.

With that in mind, create a new directory within the networking module called tests and create a new file within that directory, let's call it naming_convention.tftest.hcl

The directory structure now looks similar to this:

.
├── README.md
├── main.tf
├── modules
│   └── networking
│       ├── main.tf
│       ├── outputs.tf
│       ├── provider.tf
│       ├── tests
│       │   └── naming_convention.tftest.hcl
│       └── variables.tf
├── outputs.tf
├── providers.tf
└── variables.tf

Let's create the first test, within the naming_convention.tftest.hcl file

variables {
  company_name  = "acme"
  region        = "europe-west2"
  project_id    = "example"
}

run "valid_naming_convention" {

  command = plan

  assert {
    condition     = google_compute_subnetwork.dev.name == "acme-app-eu-we2-dev-subnet"
    error_message = "Dev subnet name did not match expected naming convention"
  }

  assert {
    condition     = google_compute_subnetwork.prod.name == "acme-app-eu-we2-prod-subnet"
    error_message = "Prod subnet name did not match expected naming convention"
  }
}

📝 NOTE: At the time of writing (2023-10), the Terraform VS Code plugin doesn't observe or support the test code syntax so don't worry if you have lots of red squiggles or a lack of syntax highlighting.

The above code initiates the required variables for the module and then runs a test to assert that the correct name is applied to the subnet.

The global variables can also be overridden by local test variables. For example, we might wish to test an alternative region:

run "valid_dev_naming_convention_alternative_region" {

  command = plan

  variables {
    region        = "asia-east1"
  }

  assert {
    condition     = google_compute_subnetwork.dev.name == "acme-app-as-ea1-dev-subnet"
    error_message = "Dev subnet name did not match expected naming convention"
  }
}

The full test file can be viewed on GitHub

Running the test

The command you'll be using for executing the tests is terraform test. The command has various options which you can find on the documentation. At the time of writing it will, by default, look for a tests directory within the current working directory and execute tests from there. You can specify the -test-directory but it seems to read the terraform config relatively.

With this in mind, to run tests you've created within the networking module we'll navigate to it first, init the module (to ensure the GCP provider can be read) and then run test

cd modules/networking 

terraform init

terraform test

You should then see Terraform read the tests and execute them

tests/naming_convention.tftest.hcl... in progress
  run "valid_dev_naming_convention"... pass
  run "valid_dev_naming_convention_alternative_region"... pass
tests/naming_convention.tftest.hcl... tearing down
tests/naming_convention.tftest.hcl... pass

Success! 2 passed, 0 failed.

Hopefully, all is well, your tests ran and you can see some output.

In a future post, we'll add integration tests that bring together multiple modules and perform an integration test of things.