hashicorp / terraform

Terraform enables you to safely and predictably create, change, and improve infrastructure. It is a source-available tool that codifies APIs into declarative configuration files that can be shared amongst team members, treated as code, edited, reviewed, and versioned.
https://www.terraform.io
Other
42.84k stars 9.56k forks source link

Testing/Mocking Submodules #34850

Open OOTS opened 8 months ago

OOTS commented 8 months ago

Terraform Version

Terraform v1.7.2
on linux_amd64

Use Cases

I want to test a terraform module ("main module") that creates/instantiates submodules. In my scenario, the submodule parameters (values passed from the main module as the input variables of the submodule) for the submodules are determined by a more or less complex process.

A dummy example can be found below:

// main.tf
module "submodule" {
  source = "./modules/submodule"
  my_var = 7
}
// modules/submodule/main.tf
terraform {
  required_providers {
    random = {
      source = "hashicorp/random"
      version = "3.5.1"
    }
  }
}

variable "my_var" {
  type = number
}

resource "random_integer" "random_int" {
  min = 0
  max = var.my_var
}

output "submodule_output" {
  value = 42
}

Now, imagine that instead of simply hardcoding the parameters of the submodule in the main module's code and only instantiating the submodule once, I'm actually creating many instances of the submodule using for_each and the parameters of the submodule are determined using a more complex process (e.g. derived by doing some manipulations on the main module's variables and/or derived from some data fetched using a data block).

I'm looking for a way to unit-test the main module as a whole, especially verifying that the logic computing the submodule parameters is working as intended.

Attempted Solutions

With the recently added terraform test command, I can write unit tests for the submodule, to make sure the submodule works correctly internally. Using tests in the main module, I can write assert blocks on the submodule outputs (which might give me some hints about the input variables that were passed to the submodule instance), but in my real-world use-case, the submodule doesn't even have outputs.

I tried writing assert blocks on the submodule parameters in the tests of the main module, but the parameters passed as variables to the submodule do not show up as attributes of the resulting module object, only the submodule outputs do. Example:

// failed attempt to test that the main module passes the right parameter to the submodule
assert {
  condition = module.submodule.my_var == 7
  error_message = "could not validate submodule input variable"
}

Similarly, I tried accessing the resources created by the submodule from the main module, but again, that doesn't appear to be possible/supported. Example:

// failed attempt to test that the resources within the submodule are created using the right values (breaks isolation)
assert {
  condition = module.submodule.random_integer.random_int <= 7
  error_message = "expected the random number to be below 7"
}

I tried if

override_resource {
  target = module.submodule
}

would help somehow, but that just gives me an error that override_resource is for overriding resources, not modules.

Next, I could simply add outputs to my submodule that mirror the submodule input variable values 1-to-1, but that seems like an ugly hack to me.

Finally, I could try write something like

run "create_submodule_instance" {
  module {
    source = "./modules/submodule"
  }

  variables {
    my_var = 7
  }
}

run "verify_submodule_resources" {

  module {
    source = "./test/comparison-module"
  }

  variables = {
    actual_resource = run.create_submodule_instance.random_integer.random_int
  }

  assert {
    condition = var.actual_resource.value <= 7
    error_message = "wrong value"
  }

}

or similar in the tests of my main module. Here, I'd be (ab-)using the fact that one can use resources created in previous run blocks as variable values for later run blocks as a way of accessing the resources created within the submodule. (The test/comparison-module would have to simply output it's input value/object unchanged to make it visible outside.) However, this doesn't actually enable me to test the logic (in my main module) that determines the input parameters for the submodule.

I'm attaching a non-working test file for the main module as a basis for playing around:

// tests/test-submodule.tftest.hcl
run "test_submodule" {

  command = plan

  # override_resource {
  #   target = module.submodule
  # }

  assert {
    condition = module.submodule.my_var == 7
    error_message = "could not validate submodule input variable"
  }

  assert {
    condition = module.submodule.random_integer.random_int <= 7
    error_message = "expected the random number to be below 7"
  }

  assert {
    condition = module.submodule.submodule_output == 42
    error_message = "could not validate submodule output"
  }

}

Proposal

Ideally, I'd like to be able to mock the submodule for my tests (in a similar way that I mock providers using mock_provider or override data using override_data).

For example, I could imagine something like this:

mock_module {

  // which module shall be mocked by this block?
  module {
    source = "./modules/submodule"
  }

  // mocked outputs of the submodule, replacing the actual module outputs
  outputs {
    submodule_output = 21 // only half the truth
  }

  capture_variables = true // when set to true, each input variable of the submodule module will be available as an attribute of the module object (module.submodule in this case)

}

The above would work fine as long as there's only one instance of a given module. However, in my case (since I'm creating many instances of the submodule using for_each), maybe a slightly more advanced version would be required. E.g.:

mock_module {

  // which module shall be mocked by this block?
  module {
    source = "./modules/submodule"
  }

  instances {
    instance {
      variables {
        my_var = 7 // when the submodule is instantiated using the input variable `my_var` set to `7`, ...
      }
      outputs {
        submodule_output = 21 // ... the output `submodule_output` of the mocked module will be `21` instead of the real value
      }
   }

  // ... more instances

  }
}

However, in my case, where the submodule doesn't even have outputs, that wouldn't help me much.

Or, to give the programmer/tester even more flexibility:

mock_module {

  // which module shall be mocked by this block?
  module {
    source = "./modules/submodule"
  }

  // whenever terraform attempts to instantiate the module given above, it will actually instantiate the replacement referenced below instead
  replacement {
    source = "./tests/replacement-submodule"
  }
}

Then, I could add module in tests/replacement-submodule that does whatever I want (e.g. simply mirror the input variables as outputs).

If having a mocked version of a module is not possible, I'd like to be able to make assertions (in the main module) about the resources created in the submodule.

References

No response

crw commented 8 months ago

Thanks for this feature request! If you are viewing this issue and would like to indicate your interest, please use the đź‘Ť reaction on the issue description to upvote this issue. We also welcome additional use case descriptions. Thanks again!

rob-borg commented 5 months ago

In my case I'm using an external submodule.

I need the ability to mock the submodule instead of just override because I don't want the submodule to be executed, just values returned.

This is because I want to implement a unit test (plan) on the main module and I don't want to have to "apply" a large external module just on a unit test because of errors like "xxx will be known only after apply" see below

image

messiahUA commented 1 month ago

I'm facing a similar case where I have my custom module that uses an external third-party module and I want to do just unit tests to validate the logic including known values generated by the submodule. It would be ideal just to refer to the submodule resource the same way as shown in the plan without mocking anything because I want to check the actual value that is generated.