2021-02-18 02:12:20 +01:00
|
|
|
---
|
2021-11-23 00:57:25 +01:00
|
|
|
layout: "language"
|
|
|
|
page_title: "Module Testing Experiment - Configuration Language"
|
2021-02-18 02:12:20 +01:00
|
|
|
---
|
|
|
|
|
|
|
|
# Module Testing Experiment
|
|
|
|
|
|
|
|
This page is about some experimental features available in recent versions of
|
|
|
|
Terraform CLI related to integration testing of shared modules.
|
|
|
|
|
|
|
|
The Terraform team is aiming to use these features to gather feedback as part
|
|
|
|
of ongoing research into different strategies for testing Terraform modules.
|
|
|
|
These features are likely to change significantly in future releases based on
|
|
|
|
feedback.
|
|
|
|
|
|
|
|
## Current Research Goals
|
|
|
|
|
|
|
|
Our initial area of research is into the question of whether it's helpful and
|
|
|
|
productive to write module integration tests in the Terraform language itself,
|
|
|
|
or whether it's better to handle that as a separate concern orchestrated by
|
|
|
|
code written in other languages.
|
|
|
|
|
|
|
|
Some existing efforts have piloted both approaches:
|
|
|
|
|
|
|
|
* [Terratest](https://terratest.gruntwork.io/) and
|
|
|
|
[kitchen-terraform](https://github.com/newcontext-oss/kitchen-terraform)
|
|
|
|
both pioneered the idea of writing tests for Terraform modules with explicit
|
2021-02-25 16:10:42 +01:00
|
|
|
orchestration written in the Go and Ruby programming languages, respectively.
|
2021-02-18 02:12:20 +01:00
|
|
|
|
|
|
|
* The Terraform provider
|
|
|
|
[`apparentlymart/testing`](https://registry.terraform.io/providers/apparentlymart/testing/latest)
|
|
|
|
introduced the idea of writing Terraform module tests in the Terraform
|
|
|
|
language itself, using a special provider that can evaluate assertions
|
|
|
|
and fail `terraform apply` if they don't pass.
|
|
|
|
|
|
|
|
Both of these approaches have both advantages and disadvantages, and so it's
|
|
|
|
likely that both will coexist for different situations, but the community
|
|
|
|
efforts have already explored the external-language testing model quite deeply
|
|
|
|
while the Terraform-integrated testing model has not yet been widely trialled.
|
|
|
|
For that reason, the current iteration of the module testing experiment is
|
|
|
|
aimed at trying to make the Terraform-integrated approach more accessible so
|
|
|
|
that more module authors can hopefully try it and share their experiences.
|
|
|
|
|
|
|
|
## Current Experimental Features
|
|
|
|
|
|
|
|
-> This page describes the incarnation of the experimental features introduced
|
|
|
|
in **Terraform CLI v0.15.0**. If you are using an earlier version of Terraform
|
|
|
|
then you'll need to upgrade to v0.15.0 or later to use the experimental features
|
|
|
|
described here, though you only need to use v0.15.0 or later for running tests;
|
|
|
|
your module itself can remain compatible with earlier Terraform versions, if
|
|
|
|
needed.
|
|
|
|
|
|
|
|
Our current area of interest is in what sorts of tests can and cannot be
|
|
|
|
written using features integrated into the Terraform language itself. As a
|
|
|
|
means to investigate that without invasive, cross-cutting changes to Terraform
|
|
|
|
Core we're using a special built-in Terraform provider as a placeholder for
|
|
|
|
potential new features.
|
|
|
|
|
|
|
|
If this experiment is successful then we expect to run a second round of
|
|
|
|
research and design about exactly what syntax is most ergonomic for writing
|
|
|
|
tests, but for the moment we're interested less in the specific syntax and more
|
|
|
|
in the capabilities of this approach.
|
|
|
|
|
|
|
|
The temporary extensions to Terraform for this experiment consist of the
|
|
|
|
following parts:
|
|
|
|
|
|
|
|
* A temporary experimental provider `terraform.io/builtin/test`, which acts as
|
|
|
|
a placeholder for potential new language features related to test assertions.
|
|
|
|
|
|
|
|
* A `terraform test` command for more conveniently running multiple tests in
|
|
|
|
a single action.
|
|
|
|
|
|
|
|
* An experimental convention of placing test configurations in subdirectories
|
|
|
|
of a `tests` directory within your module, which `terraform test` will then
|
|
|
|
discover and run.
|
|
|
|
|
|
|
|
We would like to invite adventurous module authors to try writing integration
|
|
|
|
tests for their modules using these mechanisms, and ideally also share the
|
|
|
|
tests you write (in a temporary VCS branch, if necessary) so we can see what
|
|
|
|
you were able to test, along with anything you felt unable to test in this way.
|
|
|
|
|
|
|
|
If you're interested in giving this a try, see the following sections for
|
|
|
|
usage details. Because these features are temporary experimental extensions,
|
|
|
|
there's some boilerplate required to activate and make use of it which would
|
|
|
|
likely not be required in a final design.
|
|
|
|
|
|
|
|
### Writing Tests for a Module
|
|
|
|
|
|
|
|
For the purposes of the current experiment, module tests are arranged into
|
|
|
|
_test suites_, each of which is a root Terraform module which includes a
|
|
|
|
`module` block calling the module under test, and ideally also a number of
|
|
|
|
test assertions to verify that the module outputs match expectations.
|
|
|
|
|
|
|
|
In the same directory where you keep your module's `.tf` and/or `.tf.json`
|
|
|
|
source files, create a subdirectory called `tests`. Under that directory,
|
|
|
|
make another directory which will serve as your first test suite, with a
|
|
|
|
directory name that concisely describes what the suite is aiming to test.
|
|
|
|
|
|
|
|
Here's an example directory structure of a typical module directory layout
|
|
|
|
with the addition of a test suite called `defaults`:
|
|
|
|
|
|
|
|
```
|
|
|
|
main.tf
|
|
|
|
outputs.tf
|
|
|
|
providers.tf
|
|
|
|
variables.tf
|
|
|
|
versions.tf
|
|
|
|
tests/
|
|
|
|
defaults/
|
|
|
|
test_defaults.tf
|
|
|
|
```
|
|
|
|
|
|
|
|
The `tests/defaults/test_defaults.tf` file will contain a call to the
|
|
|
|
main module with a suitable set of arguments and hopefully also one or more
|
|
|
|
resources that will, for the sake of the experiment, serve as the temporary
|
|
|
|
syntax for defining test assertions. For example:
|
|
|
|
|
|
|
|
```hcl
|
|
|
|
terraform {
|
|
|
|
required_providers {
|
|
|
|
# Because we're currently using a built-in provider as
|
|
|
|
# a substitute for dedicated Terraform language syntax
|
|
|
|
# for now, test suite modules must always declare a
|
|
|
|
# dependency on this provider. This provider is only
|
|
|
|
# available when running tests, so you shouldn't use it
|
|
|
|
# in non-test modules.
|
|
|
|
test = {
|
|
|
|
source = "terraform.io/builtin/test"
|
|
|
|
}
|
|
|
|
|
|
|
|
# This example also uses the "http" data source to
|
|
|
|
# verify the behavior of the hypothetical running
|
|
|
|
# service, so we should declare that too.
|
|
|
|
http = {
|
|
|
|
source = "hashicorp/http"
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
module "main" {
|
|
|
|
# source is always ../.. for test suite configurations,
|
|
|
|
# because they are placed two subdirectories deep under
|
|
|
|
# the main module directory.
|
|
|
|
source = "../.."
|
|
|
|
|
|
|
|
# This test suite is aiming to test the "defaults" for
|
|
|
|
# this module, so it doesn't set any input variables
|
|
|
|
# and just lets their default values be selected instead.
|
|
|
|
}
|
|
|
|
|
|
|
|
# As with all Terraform modules, we can use local values
|
|
|
|
# to do any necessary post-processing of the results from
|
|
|
|
# the module in preparation for writing test assertions.
|
|
|
|
locals {
|
|
|
|
# This expression also serves as an implicit assertion
|
|
|
|
# that the base URL uses URL syntax; the test suite
|
|
|
|
# will fail if this function fails.
|
|
|
|
api_url_parts = regex(
|
|
|
|
"^(?:(?P<scheme>[^:/?#]+):)?(?://(?P<authority>[^/?#]*))?",
|
|
|
|
module.main.api_url,
|
|
|
|
)
|
|
|
|
}
|
|
|
|
|
|
|
|
# The special test_assertions resource type, which belongs
|
|
|
|
# to the test provider we required above, is a temporary
|
|
|
|
# syntax for writing out explicit test assertions.
|
|
|
|
resource "test_assertions" "api_url" {
|
|
|
|
# "component" serves as a unique identifier for this
|
|
|
|
# particular set of assertions in the test results.
|
|
|
|
component = "api_url"
|
|
|
|
|
|
|
|
# equal and check blocks serve as the test assertions.
|
|
|
|
# the labels on these blocks are unique identifiers for
|
|
|
|
# the assertions, to allow more easily tracking changes
|
|
|
|
# in success between runs.
|
|
|
|
|
|
|
|
equal "scheme" {
|
|
|
|
description = "default scheme is https"
|
|
|
|
got = local.api_url_parts.scheme
|
|
|
|
want = "https"
|
|
|
|
}
|
|
|
|
|
|
|
|
check "port_number" {
|
|
|
|
description = "default port number is 8080"
|
|
|
|
condition = can(regex(":8080$", local.api_url_parts.authority))
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
# We can also use data resources to respond to the
|
|
|
|
# behavior of the real remote system, rather than
|
|
|
|
# just to values within the Terraform configuration.
|
|
|
|
data "http" "api_response" {
|
|
|
|
depends_on = [
|
|
|
|
# make sure the syntax assertions run first, so
|
|
|
|
# we'll be sure to see if it was URL syntax errors
|
|
|
|
# that let to this data resource also failing.
|
|
|
|
test_assertions.api_url,
|
|
|
|
]
|
|
|
|
|
|
|
|
url = module.main.api_url
|
|
|
|
}
|
|
|
|
|
|
|
|
resource "test_assertions" "api_response" {
|
|
|
|
component = "api_response"
|
|
|
|
|
|
|
|
check "valid_json" {
|
|
|
|
description = "base URL responds with valid JSON"
|
|
|
|
condition = can(jsondecode(data.http.api_response.body))
|
|
|
|
}
|
|
|
|
}
|
|
|
|
```
|
|
|
|
|
|
|
|
If you like, you can create additional directories alongside
|
|
|
|
the `default` directory to define additional test suites that
|
|
|
|
pass different variable values into the main module, and
|
|
|
|
then include assertions that verify that the result has changed
|
|
|
|
in the expected way.
|
|
|
|
|
|
|
|
### Running Your Tests
|
|
|
|
|
|
|
|
The `terraform test` command aims to make it easier to exercise all of your
|
|
|
|
defined test suites at once, and see only the output related to any test
|
|
|
|
failures or errors.
|
|
|
|
|
|
|
|
The current experimental incarnation of this command expects to be run from
|
|
|
|
your main module directory. In our example directory structure above,
|
|
|
|
that was the directory containing `main.tf` etc, and _not_ the specific test
|
|
|
|
suite directory containing `test_defaults.tf`.
|
|
|
|
|
|
|
|
Because these test suites are integration tests rather than unit tests, you'll
|
|
|
|
need to set up any credentials files or environment variables needed by the
|
|
|
|
providers your module uses before running `terraform test`. The test command
|
|
|
|
will, for each suite:
|
|
|
|
|
|
|
|
* Install the providers and any external modules the test configuration depends
|
|
|
|
on.
|
|
|
|
* Create an execution plan to create the objects declared in the module.
|
|
|
|
* Apply that execution plan to create the objects in the real remote system.
|
|
|
|
* Collect all of the test results from the apply step, which would also have
|
|
|
|
"created" the `test_assertions` resources.
|
|
|
|
* Destroy all of the objects recorded in the temporary test state, as if running
|
|
|
|
`terraform destroy` against the test configuration.
|
|
|
|
|
|
|
|
```shellsession
|
|
|
|
$ terraform test
|
|
|
|
─── Failed: defaults.api_url.scheme (default scheme is https) ───────────────
|
|
|
|
wrong value
|
|
|
|
got: "http"
|
|
|
|
want: "https"
|
|
|
|
─────────────────────────────────────────────────────────────────────────────
|
|
|
|
```
|
|
|
|
|
|
|
|
In this case, it seems like the module returned an `http` rather than an
|
|
|
|
`https` URL in the default case, and so the `defaults.api_url.scheme`
|
|
|
|
assertion failed, and the `terraform test` command detected and reported it.
|
|
|
|
|
|
|
|
The `test_assertions` resource captures any assertion failures but does not
|
|
|
|
return an error, because that can then potentially allow downstream
|
|
|
|
assertions to also run and thus capture as much context as possible.
|
|
|
|
However, if Terraform encounters any _errors_ while processing the test
|
|
|
|
configuration it will halt processing, which may cause some of the test
|
|
|
|
assertions to be skipped.
|
|
|
|
|
|
|
|
## Known Limitations
|
|
|
|
|
|
|
|
The design above is very much a prototype aimed at gathering more experience
|
|
|
|
with the possibilities of testing inside the Terraform language. We know it's
|
|
|
|
currently somewhat non-ergonomic, and hope to improve on that in later phases
|
|
|
|
of research and design, but the main focus of this iteration is on available
|
|
|
|
functionality and so with that in mind there are some specific possibilities
|
|
|
|
that we know the current prototype doesn't support well:
|
|
|
|
|
|
|
|
* Testing of subsequent updates to an existing deployment of a module.
|
|
|
|
Currently tests written in this way can only exercise the create and destroy
|
|
|
|
behaviors.
|
|
|
|
|
|
|
|
* Assertions about expected errors. For a module that includes variable
|
|
|
|
validation rules and data resources that function as assertion checks,
|
|
|
|
the current prototype doesn't have any way to express that a particular
|
|
|
|
set of inputs is _expected_ to produce an error, and thus report a test
|
|
|
|
failure if it doesn't. We'll hopefully be able to improve on this in a future
|
|
|
|
iteration with the test assertions better integrated into the language.
|
|
|
|
|
|
|
|
* Capturing context about failures. Due to this prototype using a provider as
|
|
|
|
an approximation for new assertion syntax, the `terraform test` command is
|
|
|
|
limited in how much context it's able to gather about failures. A design
|
|
|
|
more integrated into the language could potentially capture the source
|
|
|
|
expressions and input values to give better feedback about what went wrong,
|
|
|
|
similar to what Terraform typically returns from expression evaluation errors
|
|
|
|
in the main language.
|
|
|
|
|
|
|
|
* Unit testing without creating real objects. Although we do hope to spend more
|
|
|
|
time researching possibilities for unit testing against fake test doubles in
|
|
|
|
the future, we've decided to focus on integration testing to start because
|
|
|
|
it feels like the better-defined problem.
|
|
|
|
|
|
|
|
## Sending Feedback
|
|
|
|
|
|
|
|
The sort of feedback we'd most like to see at this stage of the experiment is
|
|
|
|
to see the source code of any tests you've written against real modules using
|
|
|
|
the features described above, along with notes about anything that you
|
|
|
|
attempted to test but were blocked from doing so by limitations of the above
|
|
|
|
features. The most ideal way to share that would be to share a link to a
|
|
|
|
version control branch where you've added such tests, if your module is open
|
|
|
|
source.
|
|
|
|
|
|
|
|
If you've previously written or attempted to write tests in an external
|
|
|
|
language, using a system like Terratest or kitchen-terraform, we'd also be
|
|
|
|
interested to hear about comparative differences between the two: what worked
|
|
|
|
well in each and what didn't work so well.
|
|
|
|
|
|
|
|
Our ultimate goal is to work towards an integration testing methodology which
|
|
|
|
strikes the best compromise between the capabilities of these different
|
|
|
|
approaches, ideally avoiding a hard requirement on any particular external
|
|
|
|
language and fitting well into the Terraform workflow.
|
|
|
|
|
|
|
|
Since this is still early work and likely to lead to unstructured discussion,
|
|
|
|
we'd like to gather feedback primarily via new topics in
|
|
|
|
[the community forum](https://discuss.hashicorp.com/c/terraform-core/27). That
|
|
|
|
way we can have some more freedom to explore different ideas and approaches
|
|
|
|
without the structural requirements we typically impose on GitHub issues.
|
|
|
|
|
|
|
|
Any feedback you'd like to share would be very welcome!
|