Terraform test - Add support terraform functions in mock objects
Terraform Version
1.7.3
Use Cases
I want to generate test data based on specific data type or string format:
- for a GUID use uuid()
- for an Azure resource id use format("/subscriptions/%s", uuid())
Attempted Solutions
mock_data "azurerm_client_config" {
defaults = {
client_id = uuid()
}
}
override_resource {
target = azurerm_storage_account.example
values = {
id = format("/subscriptions/%s/resourceGroups/example/providers/Microsoft.Storage/storageAccounts/myaccount", uuid())
}
}
Error when trying to use a function:
Error: Function calls not allowed
│
│ on tests/mock_data/data.tfmock.hcl line 9, in mock_data "azurerm_client_config":
│ 9: client_id = uuid()
│
│ Functions may not be called here.
Proposal
No response
References
No response
Thanks for suggesting this, @LaurentLesle!
Your use of uuid in this example draws attention to an important design question: we'd need to decide at what point these expressions are going to get evaluated, and in particular whether they get re-evaluated for each run or just evaluated once up front and used many times.
For most functions that wouldn't matter, but uuid in particular is an "impure" function that returns a different result each time it's called. The same is true for timestamp and bcrypt.
As an input to help make that decision, can you say more about what you intended it to mean to write uuid there? Would it be important to whatever you are testing for the generated UUID to remain fixed across all of the test runs, or would it be okay (or perhaps, even, better!) for it to change for each call like some real-world data sources do?
@apparentlymart - At that stage I would be in favour to keep the current behaviour of the functions as it is with terraform plan/apply instead of changing it for some of them. Maybe at some point new use cases would emerge and would justify some adjustments. I have noted 1.8 includes provider functions, maybe some edge use cases could be handled that way?
My overall goal with this feature request is to ensure I can mock data and resources in order to set correct values (more like a type format, meaning guid or resource ID format) to dependencies required to test the resource I am developing. In this instance I am building an azure verified module for Kusto cluster and need to inject all dependencies to test it accordingly (vnet for private endpoint, storage account for diagnostics profiles....). If I am not overriding the value set by the mock provider I am getting an error saying the attribute is not in the correct format.
I picked-up uuid() on purpose as it changes indeed at every execution and could potentially be interesting for a unit testing perspective. However for the example above I would favour more uuidv5(xx,xx). What is important to me is to assert the mock value generated vs the one being used or returned by the resources and testing the validation rules of the variables. In this example I am mocking an Azure context and just need to ensure the value is a GUID or for an ID making sure it is a resource ID (8 segments for most of the resources).
run "setup_dependencies"... pass
# data.azurerm_client_config.current:
data "azurerm_client_config" "current" {
client_id = "c7ji9i4m". <<<-- Problem I am trying to solve.
id = "9vodmmad"
object_id = "j69fw94x"
subscription_id = "n5s8apth"
tenant_id = "2vvpw2fy"
}
....
Error: Invalid value for variable
│
│ on main.cluster_principal_assignment.tf line 7, in module "kusto_cluster_principal_assignment":
│ 7: principal_id = each.value.principal_id
│ ├────────────────
│ │ var.principal_id is a string
│
│ The value you have set is not a valid GUID.
Thanks for that extra context!
Since this is a situation where functions are not currently allowed, there isn't really an existing precedent to follow: we're going to be implementing something new here either way, but thinking about it as "as close as possible to normal plan/apply" is one plausible way to shape those decisions indeed.
In case it's helpful context to someone else considering this issue in future: an interesting characteristic about these "impure" functions is that during the plan phase they return unknown values, so that we can wait until the apply phase to commit to their final values. If we were to decide to treat this the same as normal plan/apply, that would mean that during the plan phase of each run the value would appear unknown. It would also mean that if a test scenario contained two run blocks that both include both a plan and apply phase, both plans would find an unknown result, and each apply would have a different result.
I've not yet thought very deeply about the consequences of that behavior in the context of mocks and overrides. It's interesting that it would (I think?) represent the first situation where it's actually possible for an overridden value to vary between run blocks in the same test scenario. It might also be in conflict with current implementation decisions, since right now it's immaterial whether the mocks get evaluated just once before any rounds occur or whether they get evaluated separately for each phase/round.