'Terratest - How to pass Terraform outputs between functions

I tried to use 'test_structure.SaveTerraformOptions' but it doesn't save resources IDs. For example, I'm running 2 modules - Module 1 creating the networking and the subnets, module 2 needs module's 1 subnets ids. How can I pass this info between them?

When I'm running both functions in the same 'test_structure.RunTestStage' I am able to pass resource id from one function to another, but it doesn't test the first function and doesn't destroy at the end.

I am going to write a helper function that will only load everything and the other functions will do the testing. Maybe this will help.

However, what is the best practice to this?

This is my code:

package test

import (
    "testing"

    //"github.com/gruntwork-io/terratest/modules/random"

    "github.com/gruntwork-io/terratest/modules/azure"
    "github.com/gruntwork-io/terratest/modules/terraform"
    test_structure "github.com/gruntwork-io/terratest/modules/test-structure"
    "github.com/stretchr/testify/assert"
)

// An example of how to test the simple Terraform module in examples/terraform-basic-example using Terratest.

func TestRunAll(t *testing.T) {
    t.Parallel()

    environmentName := "dev"
    projectName := "toha"
    rgName := environmentName + "-" + projectName + "-rg"
    subscriptionID := "96b72f1a-1bdc-4fc7-b971-05e7cea7d850"

    rg_net := test_structure.CopyTerraformFolderToTemp(t, "../", "001_networking")

    test_structure.RunTestStage(t, "test_azure_resource_group", func() {
        terraformOptions := testAzureResourceGroup(t, rgName, subscriptionID)
        test_structure.SaveTerraformOptions(t, rg_net, terraformOptions)
        
        testTerraformAzureFunctionApp(t, rgName, terraformOptions)
        terraform.Destroy(t, terraformOptions)
    })
}

func testAzureResourceGroup(t *testing.T, rgName string, subscriptionID string) *terraform.Options {

    //uniquePostfix := strings.ToLower(random.UniqueId())
    terraformOptions := terraform.WithDefaultRetryableErrors(t, &terraform.Options{

        TerraformDir: "../001_networking",

        Vars: map[string]interface{}{
            "rg_name": rgName,
        },

        VarFiles: []string{"../../environments/dev/vars.tfvars"},

        // Disable colors in Terraform commands so its easier to parse stdout/stderr
        NoColor: true,
    })

    terraform.InitAndApply(t, terraformOptions)
    rg_name_out := terraform.Output(t, terraformOptions, "rg-name")

    assert.Equal(t, rgName, rg_name_out)

    return terraformOptions
}

func testTerraformAzureFunctionApp(t *testing.T, rgName string, netOpts *terraform.Options) {

    azurePortalIPRange := []string{"61.100.129.0/24"}

    subnet_1 := terraform.Output(t, netOpts, "azurerm_subnet_1")
    subnet_2 := terraform.Output(t, netOpts, "azurerm_subnet_2")

    terraformOptions := &terraform.Options{
        TerraformDir: "../002_function_app",

        Vars: map[string]interface{}{
            "subnet_id_1":           subnet_1,
            "subnet_id_2":           subnet_2,
            "rg_name":               rgName,
            "azure_portal_ip_range": azurePortalIPRange,
            "cosmos_db_endpoint":    "test",
            "cosmos_db_password":    "test",
        },

        VarFiles: []string{"../../environments/dev/vars.tfvars"},
    }

    defer terraform.Destroy(t, terraformOptions)
    terraform.InitAndApply(t, terraformOptions)

    resourceGroupName := terraform.Output(t, terraformOptions, "resource_group_name")
    appName := terraform.Output(t, terraformOptions, "function_app_name")

    appId := terraform.Output(t, terraformOptions, "function_app_id")
    appDefaultHostName := terraform.Output(t, terraformOptions, "default_hostname")
    appKind := terraform.Output(t, terraformOptions, "function_app_kind")

    // website::tag::4:: Assert
    assert.True(t, azure.AppExists(t, appName, resourceGroupName, ""))
    site := azure.GetAppService(t, appName, resourceGroupName, "")

    assert.Equal(t, appId, *site.ID)
    assert.Equal(t, appDefaultHostName, *site.DefaultHostName)
    assert.Equal(t, appKind, *site.Kind)

    assert.NotEmpty(t, *site.OutboundIPAddresses)
    assert.Equal(t, "Running", *site.State)
}


Solution 1:[1]

I think the best way you can use the test_structure is separate by stages (https://pkg.go.dev/github.com/gruntwork-io/terratest/modules/test-structure#RunTestStage):

  1. Configure terratest, save the options for your specific configuration to terraform --> SETUP

    test_structure.RunTestStage(t, "SETUP", func() {
       terraformOption := &terraform.Options{
          TerraformDir: "your/path/here",
    }
    test_structure.SaveTerraformOptions(t, directory, terraformOption)
       terraform.InitAndApply(t, terraformOption)
    })
    
  2. Destroy your infra at the end of the testing --> TEARDOWN

    defer test_structure.RunTestStage(t, "TEARDOWN", func() {
        terraformOptions := test_structure.LoadTerraformOptions(t, directory)
           terraform.Destroy(t, terraformOptions)
    })
    
  3. Your test cases --> TESTS

    test_structure.RunTestStage(t, "TESTS", func() {
         terraformOption := test_structure.LoadTerraformOptions(t, "your/path/")
    
         // Your outputs
         resourceGroupName := terraform.Output(t, terraformOptions, "resource_group_name")
         appName := terraform.Output(t, terraformOptions, "function_app_name")
         appId := terraform.Output(t, terraformOptions, "function_app_id")
    
         // your functions
         // it can receive the outputs or anything
         t.Run("Test1", func(t *testing.T) {
           testAzureResourceGroup(t, resourceGroupName, subscriptionID)
         })
    
         t.Run("Test2", func(t *testing.T) {
           testTerraformAzureFunctionApp(t, appId, appName)
         })
    })
    

Microsoft have one example more explain https://docs.microsoft.com/en-us/azure/developer/terraform/best-practices-end-to-end-testing :)

Sources

This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.

Source: Stack Overflow

Solution Source
Solution 1 Monse