This module makes your terratest tests easier with pre-defined stages and other helpful functions.
All functions that init or apply the module allow you to specify an errorFunc
which is called when the terraform command fails.
The error and the output of the terraform command are passed to that function so that you can define if the test should succeed or fail.
To ensure correct function of this module, you need to:
- Put your tests in the directory
test
. - Put variable files for your tests in
test/variables
and name them as the test is named - Put provider configuration into the
test/provider.tf
file
Those functions are the easy start into testing, with more complex scenarios covered below. They are called “run functions” because their name starts with Run
.
All run functions destroy the deployed infrastructure afterwards. Run functions abstract stages for you.
To only run some stages, set a SKIP_$stage
environment variable to skip a certain stage, e.g. SKIP_destroy
. This makes developing and debugging tests much easier.
For examples, see the examples section.
Available run functions are:
RunNoValidate
: Run with default options and no extra validationRunValidate
: Same asRunNoValidate
, but with a validation functionRunOptionsNoValidate
: Run with configured terraform options, no extra validationRunOptionsValidate
: Same asRunOptionsNoValidate
, but with a validation function
In a validation function, you can inspect the deployed infrastructure and have the test fail with assert.Fail
if it does not match what you are expecting.
You can use the DefaultOptions
function for default terraformOptions to be set automatically. Those are:
TerraformDir
, set to the absolute path of the module as the tests are in a subdirectory namedtests
VarFiles
is appended the with the.tfvars
file intest/variables
that has the same as the running test
Using DefaultOptions
ensures that the module directory is copied so that tests can be run in parallel with t.Parallel()
ℹ️ You need stages if you expect e.g. the terraform plan
to fail for a test and want to validate the errors that occur, see the examples below.
Available stages are:
setup
: Saves theterraformOptions
, runsterraform init
andterraform plan
. You can specify anerrorFunc
here.apply
: Runsterraform apply
. You can specify anerrorFunc
here.validate
: If you want to check the state of the deployed infrastructure, you can do so in the validate stage. Pass avalidateFunc
intoStageValidate
.destroy
: Runsterraform destroy
and calls theCleanup
function.
Cleanup
: Removes the test data directory.test-data
and test provider configurationtest-provider.tf
.
func TestDefaults(t *testing.T) {
// If the test needs to run on its own, e.g. because
// of quota limitations, remove this line
t.Parallel()
helpers.RunNoValidate(t)
}
with this, the test will run the module with the variables in test/variables/TestDefaults.tfvars
and expect all operations to be successful.
When you need to define not only constant variables, but also dynamic ones, you can use RunOptionsNoValidate
as follows:
func TestWithVars(t *testing.T) {
// If the test needs to run on its own, e.g. because
// of quota limitations, remove this line
t.Parallel()
helpers.RunOptionsNoValidate(t, &terraform.Options{
Vars: map[string]interface{}{
"name": uuid.New(),
},
})
}
This is needed for e.g. tests with S3 buckets to ensure every bucket is named uniquely.
If a test is expected to fail on apply, defer the destroy
stage and pass an error function to the apply
stage:
func TestS3BucketDefault(t *testing.T) {
t.Parallel()
terraformOptions := defaultOptions(t)
// set everything up for the terraform apply later
terraformOptions.Vars = map[string]interface{}{
"name": uuid.New(),
}
defer helpers.StageDestroy(t, terraformOptions.TerraformDir)
helpers.StageSetup(t, terraformOptions.TerraformDir, terraformOptions)
helpers.StageApply(t, terraformOptions.TerraformDir, func(err error, stdoutStderr string) {
// This error is expected
if err != nil {
if !strings.Contains(stdoutStderr, "Error: Some expected error") {
assert.Fail(t, "We expected an error, but it did not occur. Instead, another error was returned.")
}
}
})
}
For a test that is expected to fail on plan, use the Cleanup
function.
func TestS3BucketBackupOnVersioningOff(t *testing.T) {
t.Parallel()
terraformOptions := defaultOptions(t)
terraformOptions.NoColor = true
terraformOptions.Vars = map[string]interface{}{
"name": uuid.New(),
"backup": "on",
"versioning": "Disabled",
}
defer helpers.Cleanup(t, terraformOptions.TerraformDir)
// The terraform plan is expected to fail as versioning can't be turned off with backups turned on.
helpers.StageSetup(t, terraformOptions.TerraformDir, terraformOptions, func(err error, stdoutStderr string) {
// This error is expected, the precondition failed
if err != nil {
if !strings.Contains(stdoutStderr, "Error: Resource precondition failed") || !strings.Contains(stdoutStderr, "Versioning cannot be disabled when backups are enabled") {
assert.Fail(t, "The precondition checking versioning and backup configuration did not fail, but there is another error.")
}
} else {
// There must be an error in the precondition
assert.Fail(t, "There are no errors, but the precondition checking versioning and backup configuration must fail.")
}
})
}
When cloning this repository, run make init
locally. This sets up pre-commit, which takes care of the go formatting.
Versioning follows the go standard: Semantic versioning with a v
prefix. Tags are currently created and pushed manually.