Skip to content

Commit 9ab661b

Browse files
The Terraform TeamLiam Cervante
andauthored
Backport of Reinstate the accidentally deleted experimental testing page into v1.5 (#33414)
* backport of commit 15a8099 * backport of commit efbda87 * backport of commit 6363f86 * backport of commit 8209ef7 --------- Co-authored-by: Liam Cervante <liam.cervante@hashicorp.com>
1 parent e66494a commit 9ab661b

File tree

2 files changed

+316
-0
lines changed

2 files changed

+316
-0
lines changed

website/data/language-nav-data.json

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -213,6 +213,11 @@
213213
"path": "modules/develop/refactoring"
214214
}
215215
]
216+
},
217+
{
218+
"title": "Module Testing Experiment",
219+
"path": "modules/testing-experiment",
220+
"hidden": true
216221
}
217222
]
218223
},
Lines changed: 311 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,311 @@
1+
---
2+
page_title: Module Testing Experiment - Configuration Language
3+
description: Part of the ongoing design research for module integration testing.
4+
---
5+
6+
# Module Testing Experiment
7+
8+
This page is about some experimental features available in recent versions of
9+
Terraform CLI related to integration testing of shared modules.
10+
11+
The Terraform team is aiming to use these features to gather feedback as part
12+
of ongoing research into different strategies for testing Terraform modules.
13+
These features are likely to change significantly in future releases based on
14+
feedback.
15+
16+
## Current Research Goals
17+
18+
Our initial area of research is into the question of whether it's helpful and
19+
productive to write module integration tests in the Terraform language itself,
20+
or whether it's better to handle that as a separate concern orchestrated by
21+
code written in other languages.
22+
23+
Some existing efforts have piloted both approaches:
24+
25+
* [Terratest](https://terratest.gruntwork.io/) and
26+
[kitchen-terraform](https://github.com/newcontext-oss/kitchen-terraform)
27+
both pioneered the idea of writing tests for Terraform modules with explicit
28+
orchestration written in the Go and Ruby programming languages, respectively.
29+
30+
* The Terraform provider
31+
[`apparentlymart/testing`](https://registry.terraform.io/providers/apparentlymart/testing/latest)
32+
introduced the idea of writing Terraform module tests in the Terraform
33+
language itself, using a special provider that can evaluate assertions
34+
and fail `terraform apply` if they don't pass.
35+
36+
Both of these approaches have both advantages and disadvantages, and so it's
37+
likely that both will coexist for different situations, but the community
38+
efforts have already explored the external-language testing model quite deeply
39+
while the Terraform-integrated testing model has not yet been widely trialled.
40+
For that reason, the current iteration of the module testing experiment is
41+
aimed at trying to make the Terraform-integrated approach more accessible so
42+
that more module authors can hopefully try it and share their experiences.
43+
44+
## Current Experimental Features
45+
46+
-> This page describes the incarnation of the experimental features introduced
47+
in **Terraform CLI v0.15.0**. If you are using an earlier version of Terraform
48+
then you'll need to upgrade to v0.15.0 or later to use the experimental features
49+
described here, though you only need to use v0.15.0 or later for running tests;
50+
your module itself can remain compatible with earlier Terraform versions, if
51+
needed.
52+
53+
Our current area of interest is in what sorts of tests can and cannot be
54+
written using features integrated into the Terraform language itself. As a
55+
means to investigate that without invasive, cross-cutting changes to Terraform
56+
Core we're using a special built-in Terraform provider as a placeholder for
57+
potential new features.
58+
59+
If this experiment is successful then we expect to run a second round of
60+
research and design about exactly what syntax is most ergonomic for writing
61+
tests, but for the moment we're interested less in the specific syntax and more
62+
in the capabilities of this approach.
63+
64+
The temporary extensions to Terraform for this experiment consist of the
65+
following parts:
66+
67+
* A temporary experimental provider `terraform.io/builtin/test`, which acts as
68+
a placeholder for potential new language features related to test assertions.
69+
70+
* A `terraform test` command for more conveniently running multiple tests in
71+
a single action.
72+
73+
* An experimental convention of placing test configurations in subdirectories
74+
of a `tests` directory within your module, which `terraform test` will then
75+
discover and run.
76+
77+
We would like to invite adventurous module authors to try writing integration
78+
tests for their modules using these mechanisms, and ideally also share the
79+
tests you write (in a temporary VCS branch, if necessary) so we can see what
80+
you were able to test, along with anything you felt unable to test in this way.
81+
82+
If you're interested in giving this a try, see the following sections for
83+
usage details. Because these features are temporary experimental extensions,
84+
there's some boilerplate required to activate and make use of it which would
85+
likely not be required in a final design.
86+
87+
### Writing Tests for a Module
88+
89+
For the purposes of the current experiment, module tests are arranged into
90+
_test suites_, each of which is a root Terraform module which includes a
91+
`module` block calling the module under test, and ideally also a number of
92+
test assertions to verify that the module outputs match expectations.
93+
94+
In the same directory where you keep your module's `.tf` and/or `.tf.json`
95+
source files, create a subdirectory called `tests`. Under that directory,
96+
make another directory which will serve as your first test suite, with a
97+
directory name that concisely describes what the suite is aiming to test.
98+
99+
Here's an example directory structure of a typical module directory layout
100+
with the addition of a test suite called `defaults`:
101+
102+
```
103+
main.tf
104+
outputs.tf
105+
providers.tf
106+
variables.tf
107+
versions.tf
108+
tests/
109+
defaults/
110+
test_defaults.tf
111+
```
112+
113+
The `tests/defaults/test_defaults.tf` file will contain a call to the
114+
main module with a suitable set of arguments and hopefully also one or more
115+
resources that will, for the sake of the experiment, serve as the temporary
116+
syntax for defining test assertions. For example:
117+
118+
```hcl
119+
terraform {
120+
required_providers {
121+
# Because we're currently using a built-in provider as
122+
# a substitute for dedicated Terraform language syntax
123+
# for now, test suite modules must always declare a
124+
# dependency on this provider. This provider is only
125+
# available when running tests, so you shouldn't use it
126+
# in non-test modules.
127+
test = {
128+
source = "terraform.io/builtin/test"
129+
}
130+
# This example also uses the "http" data source to
131+
# verify the behavior of the hypothetical running
132+
# service, so we should declare that too.
133+
http = {
134+
source = "hashicorp/http"
135+
}
136+
}
137+
}
138+
module "main" {
139+
# source is always ../.. for test suite configurations,
140+
# because they are placed two subdirectories deep under
141+
# the main module directory.
142+
source = "../.."
143+
# This test suite is aiming to test the "defaults" for
144+
# this module, so it doesn't set any input variables
145+
# and just lets their default values be selected instead.
146+
}
147+
# As with all Terraform modules, we can use local values
148+
# to do any necessary post-processing of the results from
149+
# the module in preparation for writing test assertions.
150+
locals {
151+
# This expression also serves as an implicit assertion
152+
# that the base URL uses URL syntax; the test suite
153+
# will fail if this function fails.
154+
api_url_parts = regex(
155+
"^(?:(?P<scheme>[^:/?#]+):)?(?://(?P<authority>[^/?#]*))?",
156+
module.main.api_url,
157+
)
158+
}
159+
# The special test_assertions resource type, which belongs
160+
# to the test provider we required above, is a temporary
161+
# syntax for writing out explicit test assertions.
162+
resource "test_assertions" "api_url" {
163+
# "component" serves as a unique identifier for this
164+
# particular set of assertions in the test results.
165+
component = "api_url"
166+
# equal and check blocks serve as the test assertions.
167+
# the labels on these blocks are unique identifiers for
168+
# the assertions, to allow more easily tracking changes
169+
# in success between runs.
170+
equal "scheme" {
171+
description = "default scheme is https"
172+
got = local.api_url_parts.scheme
173+
want = "https"
174+
}
175+
check "port_number" {
176+
description = "default port number is 8080"
177+
condition = can(regex(":8080$", local.api_url_parts.authority))
178+
}
179+
}
180+
# We can also use data resources to respond to the
181+
# behavior of the real remote system, rather than
182+
# just to values within the Terraform configuration.
183+
data "http" "api_response" {
184+
depends_on = [
185+
# make sure the syntax assertions run first, so
186+
# we'll be sure to see if it was URL syntax errors
187+
# that let to this data resource also failing.
188+
test_assertions.api_url,
189+
]
190+
url = module.main.api_url
191+
}
192+
resource "test_assertions" "api_response" {
193+
component = "api_response"
194+
check "valid_json" {
195+
description = "base URL responds with valid JSON"
196+
condition = can(jsondecode(data.http.api_response.body))
197+
}
198+
}
199+
```
200+
201+
If you like, you can create additional directories alongside
202+
the `default` directory to define additional test suites that
203+
pass different variable values into the main module, and
204+
then include assertions that verify that the result has changed
205+
in the expected way.
206+
207+
### Running Your Tests
208+
209+
The `terraform test` command aims to make it easier to exercise all of your
210+
defined test suites at once, and see only the output related to any test
211+
failures or errors.
212+
213+
The current experimental incarnation of this command expects to be run from
214+
your main module directory. In our example directory structure above,
215+
that was the directory containing `main.tf` etc, and _not_ the specific test
216+
suite directory containing `test_defaults.tf`.
217+
218+
Because these test suites are integration tests rather than unit tests, you'll
219+
need to set up any credentials files or environment variables needed by the
220+
providers your module uses before running `terraform test`. The test command
221+
will, for each suite:
222+
223+
* Install the providers and any external modules the test configuration depends
224+
on.
225+
* Create an execution plan to create the objects declared in the module.
226+
* Apply that execution plan to create the objects in the real remote system.
227+
* Collect all of the test results from the apply step, which would also have
228+
"created" the `test_assertions` resources.
229+
* Destroy all of the objects recorded in the temporary test state, as if running
230+
`terraform destroy` against the test configuration.
231+
232+
```shellsession
233+
$ terraform test
234+
─── Failed: defaults.api_url.scheme (default scheme is https) ───────────────
235+
wrong value
236+
got: "http"
237+
want: "https"
238+
─────────────────────────────────────────────────────────────────────────────
239+
```
240+
241+
In this case, it seems like the module returned an `http` rather than an
242+
`https` URL in the default case, and so the `defaults.api_url.scheme`
243+
assertion failed, and the `terraform test` command detected and reported it.
244+
245+
The `test_assertions` resource captures any assertion failures but does not
246+
return an error, because that can then potentially allow downstream
247+
assertions to also run and thus capture as much context as possible.
248+
However, if Terraform encounters any _errors_ while processing the test
249+
configuration it will halt processing, which may cause some of the test
250+
assertions to be skipped.
251+
252+
## Known Limitations
253+
254+
The design above is very much a prototype aimed at gathering more experience
255+
with the possibilities of testing inside the Terraform language. We know it's
256+
currently somewhat non-ergonomic, and hope to improve on that in later phases
257+
of research and design, but the main focus of this iteration is on available
258+
functionality and so with that in mind there are some specific possibilities
259+
that we know the current prototype doesn't support well:
260+
261+
* Testing of subsequent updates to an existing deployment of a module.
262+
Tests written in this way can only exercise the create and destroy
263+
behaviors.
264+
265+
* Assertions about expected errors. For a module that includes variable
266+
validation rules and data resources that function as assertion checks,
267+
the current prototype doesn't have any way to express that a particular
268+
set of inputs is _expected_ to produce an error, and thus report a test
269+
failure if it doesn't. We'll hopefully be able to improve on this in a future
270+
iteration with the test assertions better integrated into the language.
271+
272+
* Capturing context about failures. Due to this prototype using a provider as
273+
an approximation for new assertion syntax, the `terraform test` command is
274+
limited in how much context it's able to gather about failures. A design
275+
more integrated into the language could potentially capture the source
276+
expressions and input values to give better feedback about what went wrong,
277+
similar to what Terraform typically returns from expression evaluation errors
278+
in the main language.
279+
280+
* Unit testing without creating real objects. Although we do hope to spend more
281+
time researching possibilities for unit testing against fake test doubles in
282+
the future, we've decided to focus on integration testing to start because
283+
it feels like the better-defined problem.
284+
285+
## Sending Feedback
286+
287+
The sort of feedback we'd most like to see at this stage of the experiment is
288+
to see the source code of any tests you've written against real modules using
289+
the features described above, along with notes about anything that you
290+
attempted to test but were blocked from doing so by limitations of the above
291+
features. The most ideal way to share that would be to share a link to a
292+
version control branch where you've added such tests, if your module is open
293+
source.
294+
295+
If you've previously written or attempted to write tests in an external
296+
language, using a system like Terratest or kitchen-terraform, we'd also be
297+
interested to hear about comparative differences between the two: what worked
298+
well in each and what didn't work so well.
299+
300+
Our ultimate goal is to work towards an integration testing methodology which
301+
strikes the best compromise between the capabilities of these different
302+
approaches, ideally avoiding a hard requirement on any particular external
303+
language and fitting well into the Terraform workflow.
304+
305+
Since this is still early work and likely to lead to unstructured discussion,
306+
we'd like to gather feedback primarily via new topics in
307+
[the community forum](https://discuss.hashicorp.com/c/terraform-core/27). That
308+
way we can have some more freedom to explore different ideas and approaches
309+
without the structural requirements we typically impose on GitHub issues.
310+
311+
Any feedback you'd like to share would be very welcome!

0 commit comments

Comments
 (0)