Repository: gruntwork-io/terratest

Issue:
helm/testing: Support for umbrella charts.
Thanks for this project, do you have any plans to support unit testing of helm charts which import sub-charts?

Discussion:
@yorinasub17: Hi @tariq1890 , can you elaborate on the use cases of the testing? Specifically, I haven't personally worked with umbrella charts yet, so I am not sure what about the current functions prevent testing them. If there are new functions you think would be useful in testing umbrella charts, knowing those would be helpful as well! We certainly would want to support it if there are deficiencies in the current code! @tariq1890: In our org, we have a standard helm template chart that is imported in all the charts that we write and maintain.

Unit tests would we very useful if `terratest` can detect these imported charts and render templates successfully. @dee-kryvenko: Similarly to how `terratest` does `terraform init`, it also has to take care of helm dependencies before running tests.

I was thinking of the following algorithm:

* If `requirements.yaml` exist but not `charts` folder:
  * If no `requirements.lock` - run `helm dependency update`
  * If `requirements.lock` present - run `helm dependency build`@yorinasub17: Thanks for those pointers! Perhaps for the short term, we can treat this the same as how terratest handles `terraform init`? That is, expose a manual function that does this (`helm.UpdateDependencies`?) and call that in your test based on what chart you are testing?@dee-kryvenko: Yeah, that's what I was thinking about. Here's some code I came up with to workaround this for now:

```go
func ensureHelmDependencies(t *testing.T, chartDir string) {
	if !files.FileExists(chartDir) {
		require.FailNow(t, "Chart not found: "+chartDir)
	}

	if !files.FileExists(chartDir + "/requirements.yaml") {
		logger.Logf(t, "Skip chart dependencies - no requirements.yaml")
		return
	}

	if files.FileExists(chartDir + "/charts") {
		logger.Logf(t, "Skip chart dependencies - charts folder already present")
		return
	}

	var buildOrUpdate string
	if files.FileExists(chartDir + "/requirements.lock") {
		buildOrUpdate = "build"
	} else {
		buildOrUpdate = "update"
	}

	if _, err := helm.RunHelmCommandAndGetOutputE(t, &helm.Options{}, "dependency", buildOrUpdate, chartDir); err != nil {
		require.NoError(t, err)
	}
}
```@xhu-cloudera: Is `requirements.yaml` file necessary? I ran `helm dependency update` and it will create a `charts` folder, with dependencies in tgz files. But I don't see a `requirements.yaml` file. Moreover, right now, the code check for existence of individual template files, however, with dependencies in zip files, that is not the case anymore. What do you guys think the best way to approach this?@xhu-cloudera: Is someone working on this? I would like to give it a try.@dee-kryvenko: Not anymore - helm got rid of requirements.yaml since I wrote my comment, and now dependency info is embedded into the Chart.yaml. Lock file is still there though.@xhu-cloudera: Here are some thoughts on conditional checking file existence for template files.
```golang
	for _, templateFile := range templateFiles {
		// validate this is a valid template file
		absTemplateFile := filepath.Join(absChartDir, templateFile)
                // Check file existence only if the template file is from the current chart, not in dependencies
		if !strings.HasPrefix(templateFile, "charts") && !files.FileExists(absTemplateFile) {
			return "", errors.WithStackTrace(TemplateFileNotFoundError{Path: templateFile, ChartDir: absChartDir})
		}

		// Note: we only get the abs template file path to check it actually exists, but the `helm template` command
		// expects the relative path from the chart.
		args = append(args, "--show-only", templateFile)
	}
```@aabouzaid: Is there any update about this issue?

I have an umbrella chart with sub-charts, and Terratest fails when the parent chart uses templates from the sub-charts ... it's not able to resolve the helper function names from the sub-charts (I want to use a helper function from the sub-chart in the umbrella chart).

I will create a PoC issue report soon.@grem11n: One can workaround this issue by managing dependencies separately. For example, executing `helm repo add ...` and `helm dep update ...` as a separate step in CI.

However, if dependency charts are packaged with the standard Helm mechanism e.g. as a `.tgz` archive, you cannot use `RenderTemplate()` and `RenderTemplateE()` functions. This [validation step](https://github.com/gruntwork-io/terratest/blob/master/modules/helm/template.go#L53) would fail.

Potentially, be removing that validation step Terratest can allow running templating against those packaged sub-charts. An obvious downside for this is that it has to rely on Helm error messages in this case, which are not always clear enough.

Another possibility is to check if a sub-chart's template is provided there. For example, if a `templateFile` has `charts/` prefix, then skip that check.

For now, one can use [`RunHelmCommandAndGetStdOutE()`](https://github.com/gruntwork-io/terratest/blob/a4eeeaba8a5ee26124cbc27e1d02cc8ebf9e9afa/modules/helm/cmd.go#L60) function to render sub-chart's templates, but you have to re-implement all the helpers of the `RenderTemplateE()` function yourself in this case.@aabouzaid: OK, just by installing the dependencies (using `helm dependency update`) before running tests, I was able to [access a dependency from the sub-chart](https://github.com/camunda/camunda-platform-helm/blob/8b5aade10604c443b447b516e01bf8494ff086dc/charts/camunda-platform/templates/ingress.yaml#L23) and [write a test for it](https://github.com/camunda/camunda-platform-helm/blob/8b5aade10604c443b447b516e01bf8494ff086dc/charts/camunda-platform/test/ingress_test.go#L37).

Here is the hierarchy:
> main chart |== has ==> sub-chart |== depends_on ==> external chart

I'm not sure what precisely the goal to achieve in the current issue is. Since there is neither `Definition of Done` nor `Acceptance Criteria`.@james00012: Closing this issue due to 6 years of inactivity. Feel free to reopen if it still needs to be addressed.

Solution patch:
diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md
new file mode 100644
index 00000000..6b100e40
--- /dev/null
+++ b/.github/pull_request_template.md
@@ -0,0 +1,25 @@
+<!-- Prepend '[WIP]' to the title if this PR is still a work-in-progress. Remove it when it is ready for review! -->
+
+## Description
+
+Fixes #000.
+
+<!-- Description of the changes introduced by this PR. -->
+
+## TODOs
+
+Read the [Gruntwork contribution guidelines](https://gruntwork.notion.site/Gruntwork-Coding-Methodology-02fdcd6e4b004e818553684760bf691e).
+
+- [ ] Update the docs.
+- [ ] Run the relevant tests successfully, including pre-commit checks.
+- [ ] Ensure any 3rd party code adheres with our [license policy](https://www.notion.so/gruntwork/Gruntwork-licenses-and-open-source-usage-policy-f7dece1f780341c7b69c1763f22b1378) or delete this line if its not applicable.
+- [ ] Include release notes. If this PR is backward incompatible, include a migration guide.
+
+## Release Notes (draft)
+
+<!-- One-line description of the PR that can be included in the final release notes. -->
+Added / Removed / Updated [X].
+
+### Migration Guide
+
+<!-- Important: If you made any backward incompatible changes, then you must write a migration guide! -->
diff --git a/examples/azure/terraform-azure-frontdoor-example/main.tf b/examples/azure/terraform-azure-frontdoor-example/main.tf
index ed0b2b3b..ffc43650 100644
--- a/examples/azure/terraform-azure-frontdoor-example/main.tf
+++ b/examples/azure/terraform-azure-frontdoor-example/main.tf
@@ -27,9 +27,12 @@ resource "azurerm_resource_group" "rg" {
 # ---------------------------------------------------------------------------------------------------------------------
 
 resource "azurerm_frontdoor" "frontdoor" {
-  name                                         = "terratest-afd-${var.postfix}"
-  resource_group_name                          = azurerm_resource_group.rg.name
-  enforce_backend_pools_certificate_name_check = false
+  name                = "terratest-afd-${var.postfix}"
+  resource_group_name = azurerm_resource_group.rg.name
+
+  backend_pool_settings {
+    enforce_backend_pools_certificate_name_check = false
+  }
 
   routing_rule {
     name               = "terratestRoutingRule1"
diff --git a/examples/helm-dependency-example/.gitignore b/examples/helm-dependency-example/.gitignore
new file mode 100644
index 00000000..eb3b48f0
--- /dev/null
+++ b/examples/helm-dependency-example/.gitignore
@@ -0,0 +1,2 @@
+charts/
+requirements.lock
\ No newline at end of file
diff --git a/examples/helm-dependency-example/Chart.yaml b/examples/helm-dependency-example/Chart.yaml
new file mode 100644
index 00000000..32ed0f64
--- /dev/null
+++ b/examples/helm-dependency-example/Chart.yaml
@@ -0,0 +1,10 @@
+apiVersion: v1
+name: helm-dependency-example
+description: A minimal Helm chart to demonstrate how to use terratest to test helm charts with dependency
+version: 0.0.1
+dependencies:
+  - name: helm-basic-example
+    alias: basic
+    repository: file://../helm-basic-example
+    condition: basic.enabled
+    version: 0.0.1
diff --git a/examples/helm-dependency-example/README.md b/examples/helm-dependency-example/README.md
new file mode 100644
index 00000000..52519be6
--- /dev/null
+++ b/examples/helm-dependency-example/README.md
@@ -0,0 +1,31 @@
+# Helm Dependency Example
+
+This folder contains a minimal helm chart to demonstrate how you can use Terratest to test your helm charts with dependencies.
+
+There are two kinds of tests you can perform on a helm chart:
+
+- Helm Template tests are tests designed to test the logic of the templates. These tests should run `helm template` with
+  various input values and parse the yaml to validate any logic embedded in the templates (e.g by reading them in using
+  client-go). Since templates are not statically typed, the goal of these tests is to promote fast cycle time
+
+The helm chart deploys a single replica `Deployment` resource given the container image spec and a `Service` that
+exposes it. This chart requires the `containerImageRepo` and `containerImageTag` input values.
+
+See the corresponding terratest code for an example of how to test this chart:
+
+- [helm_basic_example_template_test.go](/test/helm_basic_example_template_test.go): the template tests for this chart.
+
+## Running automated tests against this Helm Chart
+
+1. Install and setup [helm](https://docs.helm.sh/using_helm/#installing-helm)
+1. Install [Golang](https://golang.org/) and make sure this code is checked out into your `GOPATH`.
+1. `cd test`
+1. `dep ensure`
+1. `go test -v -tags helm -run TestHelmDependencyExampleTemplate` for the template test
+
+**NOTE**: we have build tags to differentiate kubernetes tests from non-kubernetes tests, and further differentiate helm
+tests. This is done because minikube is heavy and can interfere with docker related tests in terratest. Similarly, helm
+can overload the minikube system and thus interfere with the other kubernetes tests. Specifically, many of the tests
+start to fail with `connection refused` errors from `minikube`. To avoid overloading the system, we run the kubernetes
+tests and helm tests separately from the others. This may not be necessary if you have a sufficiently powerful machine.
+We recommend at least 4 cores and 16GB of RAM if you want to run all the tests together.
diff --git a/examples/helm-dependency-example/templates/_helpers.tpl b/examples/helm-dependency-example/templates/_helpers.tpl
new file mode 100644
index 00000000..61676105
--- /dev/null
+++ b/examples/helm-dependency-example/templates/_helpers.tpl
@@ -0,0 +1,32 @@
+{{/* vim: set filetype=mustache: */}}
+{{/*
+Expand the name of the chart.
+*/}}
+{{- define "helm-dependency-example.name" -}}
+{{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" -}}
+{{- end -}}
+
+{{/*
+Create a default fully qualified app name.
+We truncate at 63 chars because some Kubernetes name fields are limited to this (by the DNS naming spec).
+If release name contains chart name it will be used as a full name.
+*/}}
+{{- define "helm-dependency-example.fullname" -}}
+{{- if .Values.fullnameOverride -}}
+{{- .Values.fullnameOverride | trunc 63 | trimSuffix "-" -}}
+{{- else -}}
+{{- $name := default .Chart.Name .Values.nameOverride -}}
+{{- if contains $name .Release.Name -}}
+{{- .Release.Name | trunc 63 | trimSuffix "-" -}}
+{{- else -}}
+{{- printf "%s-%s" .Release.Name $name | trunc 63 | trimSuffix "-" -}}
+{{- end -}}
+{{- end -}}
+{{- end -}}
+
+{{/*
+Create chart name and version as used by the chart label.
+*/}}
+{{- define "helm-dependency-example.chart" -}}
+{{- printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" -}}
+{{- end -}}
diff --git a/examples/helm-dependency-example/templates/deployment.yaml b/examples/helm-dependency-example/templates/deployment.yaml
new file mode 100644
index 00000000..0237fbbe
--- /dev/null
+++ b/examples/helm-dependency-example/templates/deployment.yaml
@@ -0,0 +1,31 @@
+apiVersion: apps/v1
+kind: Deployment
+metadata:
+  name: {{ include "helm-dependency-example.fullname" . }}
+  namespace: {{ .Release.Namespace }}
+  labels:
+    # These labels are required by helm. You can read more about required labels in the chart best pracices guide:
+    # https://docs.helm.sh/chart_best_practices/#standard-labels
+    helm.sh/chart: {{ include "helm-dependency-example.chart" . }}
+    app.kubernetes.io/name: {{ include "helm-dependency-example.name" . }}
+    app.kubernetes.io/instance: {{ .Release.Name }}
+    app.kubernetes.io/managed-by: {{ .Release.Service }}
+spec:
+  replicas: 1
+  selector:
+    matchLabels:
+      app.kubernetes.io/name: {{ include "helm-dependency-example.name" . }}
+      app.kubernetes.io/instance: {{ .Release.Name }}
+  template:
+    metadata:
+      labels:
+        app.kubernetes.io/name: {{ include "helm-dependency-example.name" . }}
+        app.kubernetes.io/instance: {{ .Release.Name }}
+    spec:
+      containers:
+        - name: app
+          {{- $repo := required "containerImageRepo is required" .Values.containerImageRepo }}
+          {{- $tag := required "containerImageTag is required" .Values.containerImageTag }}
+          image: "{{ $repo }}:{{ $tag }}"
+          ports:
+          - containerPort: 80
diff --git a/examples/helm-dependency-example/templates/service.yaml b/examples/helm-dependency-example/templates/service.yaml
new file mode 100644
index 00000000..abece953
--- /dev/null
+++ b/examples/helm-dependency-example/templates/service.yaml
@@ -0,0 +1,20 @@
+apiVersion: v1
+kind: Service
+metadata:
+  name: {{ include "helm-dependency-example.fullname" . }}
+  labels:
+    # These labels are required by helm. You can read more about required labels in the chart best practices guide:
+    # https://docs.helm.sh/chart_best_practices/#standard-labels
+    helm.sh/chart: {{ include "helm-dependency-example.chart" . }}
+    app.kubernetes.io/name: {{ include "helm-dependency-example.name" . }}
+    app.kubernetes.io/instance: {{ .Release.Name }}
+    app.kubernetes.io/managed-by: {{ .Release.Service }}
+spec:
+  selector:
+    app.kubernetes.io/name: {{ include "helm-dependency-example.name" . }}
+    app.kubernetes.io/instance: {{ .Release.Name }}
+  type: NodePort
+  ports:
+  - protocol: TCP
+    targetPort: 80
+    port: 80
diff --git a/examples/helm-dependency-example/values.yaml b/examples/helm-dependency-example/values.yaml
new file mode 100644
index 00000000..03997f3e
--- /dev/null
+++ b/examples/helm-dependency-example/values.yaml
@@ -0,0 +1,8 @@
+# This chart purposefully does not provide any values, to demonstrate how to test required values.
+# Note that the following two values must be specified if you wish to deploy this chart:
+
+# containerImageRepo is a string that describes the image repository to pull the container image from.
+# containerImageRepo: nginx
+
+# containerImageTag is a string that describes the image tag to use when pulling the container image.
+# containerImageTag: v1.15.4
diff --git a/modules/helm/template.go b/modules/helm/template.go
index 919e2964..2fb5b002 100644
--- a/modules/helm/template.go
+++ b/modules/helm/template.go
@@ -3,6 +3,7 @@ package helm
 import (
 	"encoding/json"
 	"path/filepath"
+	"strings"
 
 	"github.com/ghodss/yaml"
 	"github.com/gruntwork-io/go-commons/errors"
@@ -33,6 +34,11 @@ func RenderTemplateE(t testing.TestingT, options *Options, chartDir string, rele
 		return "", errors.WithStackTrace(ChartNotFoundError{chartDir})
 	}
 
+	// check chart dependencies
+	if _, err := RunHelmCommandAndGetOutputE(t, &Options{}, "dependency", "build", chartDir); err != nil {
+		return "", errors.WithStackTrace(err)
+	}
+
 	// Now construct the args
 	// We first construct the template args
 	args := []string{}
@@ -46,7 +52,7 @@ func RenderTemplateE(t testing.TestingT, options *Options, chartDir string, rele
 	for _, templateFile := range templateFiles {
 		// validate this is a valid template file
 		absTemplateFile := filepath.Join(absChartDir, templateFile)
-		if !files.FileExists(absTemplateFile) {
+		if !strings.HasPrefix(templateFile, "charts") && !files.FileExists(absTemplateFile) {
 			return "", errors.WithStackTrace(TemplateFileNotFoundError{Path: templateFile, ChartDir: absChartDir})
 		}
 
diff --git a/modules/http-helper/http_helper.go b/modules/http-helper/http_helper.go
index 444087a3..7d9e1361 100644
--- a/modules/http-helper/http_helper.go
+++ b/modules/http-helper/http_helper.go
@@ -16,10 +16,31 @@ import (
 	"github.com/gruntwork-io/terratest/modules/testing"
 )
 
+type HttpGetOptions struct {
+	Url       string
+	TlsConfig *tls.Config
+	Timeout   int
+}
+
+type HttpDoOptions struct {
+	Method    string
+	Url       string
+	Body      io.Reader
+	Headers   map[string]string
+	TlsConfig *tls.Config
+	Timeout   int
+}
+
 // HttpGet performs an HTTP GET, with an optional pointer to a custom TLS configuration, on the given URL and
 // return the HTTP status code and body. If there's any error, fail the test.
 func HttpGet(t testing.TestingT, url string, tlsConfig *tls.Config) (int, string) {
-	statusCode, body, err := HttpGetE(t, url, tlsConfig)
+	return HttpGetWithOptions(t, HttpGetOptions{Url: url, TlsConfig: tlsConfig, Timeout: 10})
+}
+
+// HttpGetWithOptions performs an HTTP GET, with an optional pointer to a custom TLS configuration, on the given URL and
+// return the HTTP status code and body. If there's any error, fail the test.
+func HttpGetWithOptions(t testing.TestingT, options HttpGetOptions) (int, string) {
+	statusCode, body, err := HttpGetWithOptionsE(t, options)
 	if err != nil {
 		t.Fatal(err)
 	}
@@ -29,20 +50,26 @@ func HttpGet(t testing.TestingT, url string, tlsConfig *tls.Config) (int, string
 // HttpGetE performs an HTTP GET, with an optional pointer to a custom TLS configuration, on the given URL and
 // return the HTTP status code, body, and any error.
 func HttpGetE(t testing.TestingT, url string, tlsConfig *tls.Config) (int, string, error) {
-	logger.Logf(t, "Making an HTTP GET call to URL %s", url)
+	return HttpGetWithOptionsE(t, HttpGetOptions{Url: url, TlsConfig: tlsConfig, Timeout: 10})
+}
+
+// HttpGetWithOptionsE performs an HTTP GET, with an optional pointer to a custom TLS configuration, on the given URL and
+// return the HTTP status code, body, and any error.
+func HttpGetWithOptionsE(t testing.TestingT, options HttpGetOptions) (int, string, error) {
+	logger.Logf(t, "Making an HTTP GET call to URL %s", options.Url)
 
 	// Set HTTP client transport config
 	tr := http.DefaultTransport.(*http.Transport).Clone()
-	tr.TLSClientConfig = tlsConfig
+	tr.TLSClientConfig = options.TlsConfig
 
 	client := http.Client{
 		// By default, Go does not impose a timeout, so an HTTP connection attempt can hang for a LONG time.
-		Timeout: 10 * time.Second,
+		Timeout: time.Duration(options.Timeout) * time.Second,
 		// Include the previously created transport config
 		Transport: tr,
 	}
 
-	resp, err := client.Get(url)
+	resp, err := client.Get(options.Url)
 	if err != nil {
 		return -1, "", err
 	}
@@ -60,7 +87,14 @@ func HttpGetE(t testing.TestingT, url string, tlsConfig *tls.Config) (int, strin
 // HttpGetWithValidation performs an HTTP GET on the given URL and verify that you get back the expected status code and body. If either
 // doesn't match, fail the test.
 func HttpGetWithValidation(t testing.TestingT, url string, tlsConfig *tls.Config, expectedStatusCode int, expectedBody string) {
-	err := HttpGetWithValidationE(t, url, tlsConfig, expectedStatusCode, expectedBody)
+	options := HttpGetOptions{Url: url, TlsConfig: tlsConfig, Timeout: 10}
+	HttpGetWithValidationWithOptions(t, options, expectedStatusCode, expectedBody)
+}
+
+// HttpGetWithValidationWithOptions performs an HTTP GET on the given URL and verify that you get back the expected status code and body. If either
+// doesn't match, fail the test.
+func HttpGetWithValidationWithOptions(t testing.TestingT, options HttpGetOptions, expectedStatusCode int, expectedBody string) {
+	err := HttpGetWithValidationWithOptionsE(t, options, expectedStatusCode, expectedBody)
 	if err != nil {
 		t.Fatal(err)
 	}
@@ -69,14 +103,26 @@ func HttpGetWithValidation(t testing.TestingT, url string, tlsConfig *tls.Config
 // HttpGetWithValidationE performs an HTTP GET on the given URL and verify that you get back the expected status code and body. If either
 // doesn't match, return an error.
 func HttpGetWithValidationE(t testing.TestingT, url string, tlsConfig *tls.Config, expectedStatusCode int, expectedBody string) error {
-	return HttpGetWithCustomValidationE(t, url, tlsConfig, func(statusCode int, body string) bool {
+	options := HttpGetOptions{Url: url, TlsConfig: tlsConfig, Timeout: 10}
+	return HttpGetWithValidationWithOptionsE(t, options, expectedStatusCode, expectedBody)
+}
+
+// HttpGetWithValidationWithOptionsE performs an HTTP GET on the given URL and verify that you get back the expected status code and body. If either
+// doesn't match, return an error.
+func HttpGetWithValidationWithOptionsE(t testing.TestingT, options HttpGetOptions, expectedStatusCode int, expectedBody string) error {
+	return HttpGetWithCustomValidationWithOptionsE(t, options, func(statusCode int, body string) bool {
 		return statusCode == expectedStatusCode && body == expectedBody
 	})
 }
 
 // HttpGetWithCustomValidation performs an HTTP GET on the given URL and validate the returned status code and body using the given function.
 func HttpGetWithCustomValidation(t testing.TestingT, url string, tlsConfig *tls.Config, validateResponse func(int, string) bool) {
-	err := HttpGetWithCustomValidationE(t, url, tlsConfig, validateResponse)
+	HttpGetWithCustomValidationWithOptions(t, HttpGetOptions{Url: url, TlsConfig: tlsConfig, Timeout: 10}, validateResponse)
+}
+
+// HttpGetWithCustomValidationWithOptions performs an HTTP GET on the given URL and validate the returned status code and body using the given function.
+func HttpGetWithCustomValidationWithOptions(t testing.TestingT, options HttpGetOptions, validateResponse func(int, string) bool) {
+	err := HttpGetWithCustomValidationWithOptionsE(t, options, validateResponse)
 	if err != nil {
 		t.Fatal(err)
 	}
@@ -84,14 +130,19 @@ func HttpGetWithCustomValidation(t testing.TestingT, url string, tlsConfig *tls.
 
 // HttpGetWithCustomValidationE performs an HTTP GET on the given URL and validate the returned status code and body using the given function.
 func HttpGetWithCustomValidationE(t testing.TestingT, url string, tlsConfig *tls.Config, validateResponse func(int, string) bool) error {
-	statusCode, body, err := HttpGetE(t, url, tlsConfig)
+	return HttpGetWithCustomValidationWithOptionsE(t, HttpGetOptions{Url: url, TlsConfig: tlsConfig, Timeout: 10}, validateResponse)
+}
+
+// HttpGetWithCustomValidationWithOptionsE performs an HTTP GET on the given URL and validate the returned status code and body using the given function.
+func HttpGetWithCustomValidationWithOptionsE(t testing.TestingT, options HttpGetOptions, validateResponse func(int, string) bool) error {
+	statusCode, body, err := HttpGetWithOptionsE(t, options)
 
 	if err != nil {
 		return err
 	}
 
 	if !validateResponse(statusCode, body) {
-		return ValidationFunctionFailed{Url: url, Status: statusCode, Body: body}
+		return ValidationFunctionFailed{Url: options.Url, Status: statusCode, Body: body}
 	}
 
 	return nil
@@ -100,7 +151,14 @@ func HttpGetWithCustomValidationE(t testing.TestingT, url string, tlsConfig *tls
 // HttpGetWithRetry repeatedly performs an HTTP GET on the given URL until the given status code and body are returned or until max
 // retries has been exceeded.
 func HttpGetWithRetry(t testing.TestingT, url string, tlsConfig *tls.Config, expectedStatus int, expectedBody string, retries int, sleepBetweenRetries time.Duration) {
-	err := HttpGetWithRetryE(t, url, tlsConfig, expectedStatus, expectedBody, retries, sleepBetweenRetries)
+	options := HttpGetOptions{Url: url, TlsConfig: tlsConfig, Timeout: 10}
+	HttpGetWithRetryWithOptions(t, options, expectedStatus, expectedBody, retries, sleepBetweenRetries)
+}
+
+// HttpGetWithRetryWithOptions repeatedly performs an HTTP GET on the given URL until the given status code and body are returned or until max
+// retries has been exceeded.
+func HttpGetWithRetryWithOptions(t testing.TestingT, options HttpGetOptions, expectedStatus int, expectedBody string, retries int, sleepBetweenRetries time.Duration) {
+	err := HttpGetWithRetryWithOptionsE(t, options, expectedStatus, expectedBody, retries, sleepBetweenRetries)
 	if err != nil {
 		t.Fatal(err)
 	}
@@ -109,8 +167,15 @@ func HttpGetWithRetry(t testing.TestingT, url string, tlsConfig *tls.Config, exp
 // HttpGetWithRetryE repeatedly performs an HTTP GET on the given URL until the given status code and body are returned or until max
 // retries has been exceeded.
 func HttpGetWithRetryE(t testing.TestingT, url string, tlsConfig *tls.Config, expectedStatus int, expectedBody string, retries int, sleepBetweenRetries time.Duration) error {
-	_, err := retry.DoWithRetryE(t, fmt.Sprintf("HTTP GET to URL %s", url), retries, sleepBetweenRetries, func() (string, error) {
-		return "", HttpGetWithValidationE(t, url, tlsConfig, expectedStatus, expectedBody)
+	options := HttpGetOptions{Url: url, TlsConfig: tlsConfig, Timeout: 10}
+	return HttpGetWithRetryWithOptionsE(t, options, expectedStatus, expectedBody, retries, sleepBetweenRetries)
+}
+
+// HttpGetWithRetryWithOptionsE repeatedly performs an HTTP GET on the given URL until the given status code and body are returned or until max
+// retries has been exceeded.
+func HttpGetWithRetryWithOptionsE(t testing.TestingT, options HttpGetOptions, expectedStatus int, expectedBody string, retries int, sleepBetweenRetries time.Duration) error {
+	_, err := retry.DoWithRetryE(t, fmt.Sprintf("HTTP GET to URL %s", options.Url), retries, sleepBetweenRetries, func() (string, error) {
+		return "", HttpGetWithValidationWithOptionsE(t, options, expectedStatus, expectedBody)
 	})
 
 	return err
@@ -119,7 +184,14 @@ func HttpGetWithRetryE(t testing.TestingT, url string, tlsConfig *tls.Config, ex
 // HttpGetWithRetryWithCustomValidation repeatedly performs an HTTP GET on the given URL until the given validation function returns true or max retries
 // has been exceeded.
 func HttpGetWithRetryWithCustomValidation(t testing.TestingT, url string, tlsConfig *tls.Config, retries int, sleepBetweenRetries time.Duration, validateResponse func(int, string) bool) {
-	err := HttpGetWithRetryWithCustomValidationE(t, url, tlsConfig, retries, sleepBetweenRetries, validateResponse)
+	options := HttpGetOptions{Url: url, TlsConfig: tlsConfig, Timeout: 10}
+	HttpGetWithRetryWithCustomValidationWithOptions(t, options, retries, sleepBetweenRetries, validateResponse)
+}
+
+// HttpGetWithRetryWithCustomValidationWithOptions repeatedly performs an HTTP GET on the given URL until the given validation function returns true or max retries
+// has been exceeded.
+func HttpGetWithRetryWithCustomValidationWithOptions(t testing.TestingT, options HttpGetOptions, retries int, sleepBetweenRetries time.Duration, validateResponse func(int, string) bool) {
+	err := HttpGetWithRetryWithCustomValidationWithOptionsE(t, options, retries, sleepBetweenRetries, validateResponse)
 	if err != nil {
 		t.Fatal(err)
 	}
@@ -128,8 +200,15 @@ func HttpGetWithRetryWithCustomValidation(t testing.TestingT, url string, tlsCon
 // HttpGetWithRetryWithCustomValidationE repeatedly performs an HTTP GET on the given URL until the given validation function returns true or max retries
 // has been exceeded.
 func HttpGetWithRetryWithCustomValidationE(t testing.TestingT, url string, tlsConfig *tls.Config, retries int, sleepBetweenRetries time.Duration, validateResponse func(int, string) bool) error {
-	_, err := retry.DoWithRetryE(t, fmt.Sprintf("HTTP GET to URL %s", url), retries, sleepBetweenRetries, func() (string, error) {
-		return "", HttpGetWithCustomValidationE(t, url, tlsConfig, validateResponse)
+	options := HttpGetOptions{Url: url, TlsConfig: tlsConfig, Timeout: 10}
+	return HttpGetWithRetryWithCustomValidationWithOptionsE(t, options, retries, sleepBetweenRetries, validateResponse)
+}
+
+// HttpGetWithRetryWithCustomValidationWithOptionsE repeatedly performs an HTTP GET on the given URL until the given validation function returns true or max retries
+// has been exceeded.
+func HttpGetWithRetryWithCustomValidationWithOptionsE(t testing.TestingT, options HttpGetOptions, retries int, sleepBetweenRetries time.Duration, validateResponse func(int, string) bool) error {
+	_, err := retry.DoWithRetryE(t, fmt.Sprintf("HTTP GET to URL %s", options.Url), retries, sleepBetweenRetries, func() (string, error) {
+		return "", HttpGetWithCustomValidationWithOptionsE(t, options, validateResponse)
 	})
 
 	return err
@@ -141,7 +220,22 @@ func HTTPDo(
 	t testing.TestingT, method string, url string, body io.Reader,
 	headers map[string]string, tlsConfig *tls.Config,
 ) (int, string) {
-	statusCode, respBody, err := HTTPDoE(t, method, url, body, headers, tlsConfig)
+	options := HttpDoOptions{
+		Method:    method,
+		Url:       url,
+		Body:      body,
+		Headers:   headers,
+		TlsConfig: tlsConfig,
+		Timeout:   10}
+	return HTTPDoWithOptions(t, options)
+}
+
+// HTTPDoWithOptions performs the given HTTP method on the given URL and return the HTTP status code and body.
+// If there's any error, fail the test.
+func HTTPDoWithOptions(
+	t testing.TestingT, options HttpDoOptions,
+) (int, string) {
+	statusCode, respBody, err := HTTPDoWithOptionsE(t, options)
 	if err != nil {
 		t.Fatal(err)
 	}
@@ -153,19 +247,33 @@ func HTTPDoE(
 	t testing.TestingT, method string, url string, body io.Reader,
 	headers map[string]string, tlsConfig *tls.Config,
 ) (int, string, error) {
-	logger.Logf(t, "Making an HTTP %s call to URL %s", method, url)
+	options := HttpDoOptions{
+		Method:    method,
+		Url:       url,
+		Body:      body,
+		Headers:   headers,
+		Timeout:   10,
+		TlsConfig: tlsConfig}
+	return HTTPDoWithOptionsE(t, options)
+}
+
+// HTTPDoWithOptionsE performs the given HTTP method on the given URL and return the HTTP status code, body, and any error.
+func HTTPDoWithOptionsE(
+	t testing.TestingT, options HttpDoOptions,
+) (int, string, error) {
+	logger.Logf(t, "Making an HTTP %s call to URL %s", options.Method, options.Url)
 
 	tr := &http.Transport{
-		TLSClientConfig: tlsConfig,
+		TLSClientConfig: options.TlsConfig,
 	}
 
 	client := http.Client{
 		// By default, Go does not impose a timeout, so an HTTP connection attempt can hang for a LONG time.
-		Timeout:   10 * time.Second,
+		Timeout:   time.Duration(options.Timeout) * time.Second,
 		Transport: tr,
 	}
 
-	req := newRequest(method, url, body, headers)
+	req := newRequest(options.Method, options.Url, options.Body, options.Headers)
 	resp, err := client.Do(req)
 	if err != nil {
 		return -1, "", err
@@ -189,8 +297,24 @@ func HTTPDoWithRetry(
 	body []byte, headers map[string]string, expectedStatus int,
 	retries int, sleepBetweenRetries time.Duration, tlsConfig *tls.Config,
 ) string {
-	out, err := HTTPDoWithRetryE(t, method, url, body,
-		headers, expectedStatus, retries, sleepBetweenRetries, tlsConfig)
+	options := HttpDoOptions{
+		Method:    method,
+		Url:       url,
+		Body:      bytes.NewReader(body),
+		Headers:   headers,
+		TlsConfig: tlsConfig,
+		Timeout:   10}
+	return HTTPDoWithRetryWithOptions(t, options, expectedStatus, retries, sleepBetweenRetries)
+}
+
+// HTTPDoWithRetryWithOptions repeatedly performs the given HTTP method on the given URL until the given status code and body are
+// returned or until max retries has been exceeded.
+// The function compares the expected status code against the received one and fails if they don't match.
+func HTTPDoWithRetryWithOptions(
+	t testing.TestingT, options HttpDoOptions, expectedStatus int,
+	retries int, sleepBetweenRetries time.Duration,
+) string {
+	out, err := HTTPDoWithRetryWithOptionsE(t, options, expectedStatus, retries, sleepBetweenRetries)
 	if err != nil {
 		t.Fatal(err)
 	}
@@ -205,17 +329,44 @@ func HTTPDoWithRetryE(
 	body []byte, headers map[string]string, expectedStatus int,
 	retries int, sleepBetweenRetries time.Duration, tlsConfig *tls.Config,
 ) (string, error) {
+	options := HttpDoOptions{
+		Method:    method,
+		Url:       url,
+		Body:      bytes.NewReader(body),
+		Headers:   headers,
+		TlsConfig: tlsConfig,
+		Timeout:   10}
+
+	return HTTPDoWithRetryWithOptionsE(t, options, expectedStatus, retries, sleepBetweenRetries)
+}
+
+// HTTPDoWithRetryWithOptionsE repeatedly performs the given HTTP method on the given URL until the given status code and body are
+// returned or until max retries has been exceeded.
+// The function compares the expected status code against the received one and fails if they don't match.
+func HTTPDoWithRetryWithOptionsE(
+	t testing.TestingT, options HttpDoOptions, expectedStatus int,
+	retries int, sleepBetweenRetries time.Duration,
+) (string, error) {
+	// The request body is closed after a request is complete.
+	// Extract the underlying data and cache it so we can reuse for retried requests
+	data, err := io.ReadAll(options.Body)
+	if err != nil {
+		return "", err
+	}
+
+	options.Body = nil
+
 	out, err := retry.DoWithRetryE(
-		t, fmt.Sprintf("HTTP %s to URL %s", method, url), retries,
+		t, fmt.Sprintf("HTTP %s to URL %s", options.Method, options.Url), retries,
 		sleepBetweenRetries, func() (string, error) {
-			bodyReader := bytes.NewReader(body)
-			statusCode, out, err := HTTPDoE(t, method, url, bodyReader, headers, tlsConfig)
+			options.Body = bytes.NewReader(data)
+			statusCode, out, err := HTTPDoWithOptionsE(t, options)
 			if err != nil {
 				return "", err
 			}
 			logger.Logf(t, "output: %v", out)
 			if statusCode != expectedStatus {
-				return "", ValidationFunctionFailed{Url: url, Status: statusCode}
+				return "", ValidationFunctionFailed{Url: options.Url, Status: statusCode}
 			}
 			return out, nil
 		})
@@ -230,7 +381,24 @@ func HTTPDoWithValidationRetry(
 	body []byte, headers map[string]string, expectedStatus int,
 	expectedBody string, retries int, sleepBetweenRetries time.Duration, tlsConfig *tls.Config,
 ) {
-	err := HTTPDoWithValidationRetryE(t, method, url, body, headers, expectedStatus, expectedBody, retries, sleepBetweenRetries, tlsConfig)
+	options := HttpDoOptions{
+		Method:    method,
+		Url:       url,
+		Body:      bytes.NewReader(body),
+		Headers:   headers,
+		TlsConfig: tlsConfig,
+		Timeout:   10}
+
+	HTTPDoWithValidationRetryWithOptions(t, options, expectedStatus, expectedBody, retries, sleepBetweenRetries)
+}
+
+// HTTPDoWithValidationRetryWithOptions repeatedly performs the given HTTP method on the given URL until the given status code and
+// body are returned or until max retries has been exceeded.
+func HTTPDoWithValidationRetryWithOptions(
+	t testing.TestingT, options HttpDoOptions, expectedStatus int,
+	expectedBody string, retries int, sleepBetweenRetries time.Duration,
+) {
+	err := HTTPDoWithValidationRetryWithOptionsE(t, options, expectedStatus, expectedBody, retries, sleepBetweenRetries)
 	if err != nil {
 		t.Fatal(err)
 	}
@@ -243,10 +411,26 @@ func HTTPDoWithValidationRetryE(
 	body []byte, headers map[string]string, expectedStatus int,
 	expectedBody string, retries int, sleepBetweenRetries time.Duration, tlsConfig *tls.Config,
 ) error {
-	_, err := retry.DoWithRetryE(t, fmt.Sprintf("HTTP %s to URL %s", method, url), retries,
+	options := HttpDoOptions{
+		Method:    method,
+		Url:       url,
+		Body:      bytes.NewReader(body),
+		Headers:   headers,
+		TlsConfig: tlsConfig,
+		Timeout:   10}
+
+	return HTTPDoWithValidationRetryWithOptionsE(t, options, expectedStatus, expectedBody, retries, sleepBetweenRetries)
+}
+
+// HTTPDoWithValidationRetryWithOptionsE repeatedly performs the given HTTP method on the given URL until the given status code and
+// body are returned or until max retries has been exceeded.
+func HTTPDoWithValidationRetryWithOptionsE(
+	t testing.TestingT, options HttpDoOptions, expectedStatus int,
+	expectedBody string, retries int, sleepBetweenRetries time.Duration,
+) error {
+	_, err := retry.DoWithRetryE(t, fmt.Sprintf("HTTP %s to URL %s", options.Method, options.Url), retries,
 		sleepBetweenRetries, func() (string, error) {
-			bodyReader := bytes.NewReader(body)
-			return "", HTTPDoWithValidationE(t, method, url, bodyReader, headers, expectedStatus, expectedBody, tlsConfig)
+			return "", HTTPDoWithValidationWithOptionsE(t, options, expectedStatus, expectedBody)
 		})
 
 	return err
@@ -255,7 +439,21 @@ func HTTPDoWithValidationRetryE(
 // HTTPDoWithValidation performs the given HTTP method on the given URL and verify that you get back the expected status
 // code and body. If either doesn't match, fail the test.
 func HTTPDoWithValidation(t testing.TestingT, method string, url string, body io.Reader, headers map[string]string, expectedStatusCode int, expectedBody string, tlsConfig *tls.Config) {
-	err := HTTPDoWithValidationE(t, method, url, body, headers, expectedStatusCode, expectedBody, tlsConfig)
+	options := HttpDoOptions{
+		Method:    method,
+		Url:       url,
+		Body:      body,
+		Headers:   headers,
+		TlsConfig: tlsConfig,
+		Timeout:   10}
+
+	HTTPDoWithValidationWithOptions(t, options, expectedStatusCode, expectedBody)
+}
+
+// HTTPDoWithValidationWithOptions performs the given HTTP method on the given URL and verify that you get back the expected status
+// code and body. If either doesn't match, fail the test.
+func HTTPDoWithValidationWithOptions(t testing.TestingT, options HttpDoOptions, expectedStatusCode int, expectedBody string) {
+	err := HTTPDoWithValidationWithOptionsE(t, options, expectedStatusCode, expectedBody)
 	if err != nil {
 		t.Fatal(err)
 	}
@@ -264,15 +462,43 @@ func HTTPDoWithValidation(t testing.TestingT, method string, url string, body io
 // HTTPDoWithValidationE performs the given HTTP method on the given URL and verify that you get back the expected status
 // code and body. If either doesn't match, return an error.
 func HTTPDoWithValidationE(t testing.TestingT, method string, url string, body io.Reader, headers map[string]string, expectedStatusCode int, expectedBody string, tlsConfig *tls.Config) error {
-	return HTTPDoWithCustomValidationE(t, method, url, body, headers, func(statusCode int, body string) bool {
+	options := HttpDoOptions{
+		Method:    method,
+		Url:       url,
+		Body:      body,
+		Headers:   headers,
+		TlsConfig: tlsConfig,
+		Timeout:   10}
+
+	return HTTPDoWithValidationWithOptionsE(t, options, expectedStatusCode, expectedBody)
+}
+
+// HTTPDoWithValidationWithOptionsE performs the given HTTP method on the given URL and verify that you get back the expected status
+// code and body. If either doesn't match, return an error.
+func HTTPDoWithValidationWithOptionsE(t testing.TestingT, options HttpDoOptions, expectedStatusCode int, expectedBody string) error {
+	return HTTPDoWithCustomValidationWithOptionsE(t, options, func(statusCode int, body string) bool {
 		return statusCode == expectedStatusCode && body == expectedBody
-	}, tlsConfig)
+	})
 }
 
 // HTTPDoWithCustomValidation performs the given HTTP method on the given URL and validate the returned status code and
 // body using the given function.
 func HTTPDoWithCustomValidation(t testing.TestingT, method string, url string, body io.Reader, headers map[string]string, validateResponse func(int, string) bool, tlsConfig *tls.Config) {
-	err := HTTPDoWithCustomValidationE(t, method, url, body, headers, validateResponse, tlsConfig)
+	options := HttpDoOptions{
+		Method:    method,
+		Url:       url,
+		Body:      body,
+		Headers:   headers,
+		TlsConfig: tlsConfig,
+		Timeout:   10}
+
+	HTTPDoWithCustomValidationWithOptions(t, options, validateResponse)
+}
+
+// HTTPDoWithCustomValidationWithOptions performs the given HTTP method on the given URL and validate the returned status code and
+// body using the given function.
+func HTTPDoWithCustomValidationWithOptions(t testing.TestingT, options HttpDoOptions, validateResponse func(int, string) bool) {
+	err := HTTPDoWithCustomValidationWithOptionsE(t, options, validateResponse)
 	if err != nil {
 		t.Fatal(err)
 	}
@@ -281,14 +507,28 @@ func HTTPDoWithCustomValidation(t testing.TestingT, method string, url string, b
 // HTTPDoWithCustomValidationE performs the given HTTP method on the given URL and validate the returned status code and
 // body using the given function.
 func HTTPDoWithCustomValidationE(t testing.TestingT, method string, url string, body io.Reader, headers map[string]string, validateResponse func(int, string) bool, tlsConfig *tls.Config) error {
-	statusCode, respBody, err := HTTPDoE(t, method, url, body, headers, tlsConfig)
+	options := HttpDoOptions{
+		Method:    method,
+		Url:       url,
+		Body:      body,
+		Headers:   headers,
+		TlsConfig: tlsConfig,
+		Timeout:   10}
+
+	return HTTPDoWithCustomValidationWithOptionsE(t, options, validateResponse)
+}
+
+// HTTPDoWithCustomValidationWithOptionsE performs the given HTTP method on the given URL and validate the returned status code and
+// body using the given function.
+func HTTPDoWithCustomValidationWithOptionsE(t testing.TestingT, options HttpDoOptions, validateResponse func(int, string) bool) error {
+	statusCode, respBody, err := HTTPDoWithOptionsE(t, options)
 
 	if err != nil {
 		return err
 	}
 
 	if !validateResponse(statusCode, respBody) {
-		return ValidationFunctionFailed{Url: url, Status: statusCode, Body: respBody}
+		return ValidationFunctionFailed{Url: options.Url, Status: statusCode, Body: respBody}
 	}
 
 	return nil



Test patch:
diff --git a/test/helm_dependency_example_template_test.go b/test/helm_dependency_example_template_test.go
new file mode 100644
index 00000000..73125579
--- /dev/null
+++ b/test/helm_dependency_example_template_test.go
@@ -0,0 +1,179 @@
+//go:build kubeall || helm
+// +build kubeall helm
+
+// **NOTE**: we have build tags to differentiate kubernetes tests from non-kubernetes tests, and further differentiate helm
+// tests. This is done because minikube is heavy and can interfere with docker related tests in terratest. Similarly, helm
+// can overload the minikube system and thus interfere with the other kubernetes tests. Specifically, many of the tests
+// start to fail with `connection refused` errors from `minikube`. To avoid overloading the system, we run the kubernetes
+// tests and helm tests separately from the others. This may not be necessary if you have a sufficiently powerful machine.
+// We recommend at least 4 cores and 16GB of RAM if you want to run all the tests together.
+
+package test
+
+import (
+	"path/filepath"
+	"strings"
+	"testing"
+
+	"github.com/stretchr/testify/require"
+	appsv1 "k8s.io/api/apps/v1"
+
+	"github.com/gruntwork-io/terratest/modules/helm"
+	"github.com/gruntwork-io/terratest/modules/k8s"
+	"github.com/gruntwork-io/terratest/modules/logger"
+	"github.com/gruntwork-io/terratest/modules/random"
+)
+
+// This file contains examples of how to use terratest to test helm chart template logic by rendering the templates
+// using `helm template`, and then reading in the rendered templates.
+// There are two tests:
+// - TestHelmBasicExampleTemplateRenderedDeployment: An example of how to read in the rendered object and check the
+//   computed values.
+// - TestHelmBasicExampleTemplateRequiredTemplateArgs: An example of how to check that the required args are indeed
+//   required for the template to render.
+
+// An example of how to verify the rendered template object of a Helm Chart given various inputs.
+func TestHelmDependencyExampleTemplateRenderedDeployment(t *testing.T) {
+	t.Parallel()
+
+	// Path to the helm chart we will test
+	helmChartPath, err := filepath.Abs("../examples/helm-dependency-example")
+	releaseName := "helm-dependency"
+	require.NoError(t, err)
+
+	// Since we aren't deploying any resources, there is no need to setup kubectl authentication or helm home.
+
+	// Set up the namespace; confirm that the template renders the expected value for the namespace.
+	namespaceName := "medieval-" + strings.ToLower(random.UniqueId())
+	logger.Logf(t, "Namespace: %s\n", namespaceName)
+
+	// Setup the args. For this test, we will set the following input values:
+	// - containerImageRepo=nginx
+	// - containerImageTag=1.15.8
+	options := &helm.Options{
+		SetValues: map[string]string{
+			"containerImageRepo":       "nginx",
+			"containerImageTag":        "1.15.8",
+			"basic.containerImageRepo": "nginx",
+			"basic.containerImageTag":  "1.15.8",
+		},
+		KubectlOptions: k8s.NewKubectlOptions("", "", namespaceName),
+	}
+
+	testCases := []struct {
+		name         string
+		templateName string
+	}{
+		{
+			"dependent chart",
+			"templates/deployment.yaml",
+		},
+		{
+			"basic chart",
+			"charts/basic/templates/deployment.yaml",
+		},
+	}
+
+	for _, testCase := range testCases {
+		testCase := testCase
+		t.Run(testCase.name, func(subT *testing.T) {
+			// subT.Parallel()
+			// Run RenderTemplate to render the template and capture the output. Note that we use the version without `E`, since
+			// we want to assert that the template renders without any errors.
+			// Additionally, although we know there is only one yaml file in the template, we deliberately path a templateFiles
+			// arg to demonstrate how to select individual templates to render.
+			output := helm.RenderTemplate(t, options, helmChartPath, releaseName, []string{testCase.templateName})
+
+			// Now we use kubernetes/client-go library to render the template output into the Deployment struct. This will
+			// ensure the Deployment resource is rendered correctly.
+			var deployment appsv1.Deployment
+			helm.UnmarshalK8SYaml(t, output, &deployment)
+
+			// Verify the namespace matches the expected supplied namespace.
+			require.Equal(t, namespaceName, deployment.Namespace)
+
+			// Finally, we verify the deployment pod template spec is set to the expected container image value
+			expectedContainerImage := "nginx:1.15.8"
+			deploymentContainers := deployment.Spec.Template.Spec.Containers
+			require.Equal(t, len(deploymentContainers), 1)
+			require.Equal(t, deploymentContainers[0].Image, expectedContainerImage)
+
+		})
+	}
+}
+
+// An example of how to verify required values for a helm chart.
+func TestHelmDependencyExampleTemplateRequiredTemplateArgs(t *testing.T) {
+	t.Parallel()
+
+	// Path to the helm chart we will test
+	helmChartPath, err := filepath.Abs("../examples/helm-dependency-example")
+	releaseName := "helm-dependency"
+	require.NoError(t, err)
+
+	// Since we aren't deploying any resources, there is no need to setup kubectl authentication, helm home, or
+	// namespaces
+
+	// Here, we use a table driven test to iterate through all the required values as subtests. You can learn more about
+	// go subtests here: https://blog.golang.org/subtests
+	// The struct captures the inputs that we will pass to helm template and a human friendly name so we can identify it
+	// in the test output. In this case, each test case will be a complete values input except for one of the required
+	// values missing, to test that neglecting a required value will cause the template rendering to fail.
+	testCases := []struct {
+		name   string
+		values map[string]string
+	}{
+		{
+			"MissingContainerImageRepo in dependent chart",
+			map[string]string{
+				"containerImageTag":        "1.15.8",
+				"basic.containerImageRepo": "nginx",
+				"basic.containerImageTag":  "1.15.8",
+			},
+		},
+		{
+			"MissingContainerImageRepo in basic chart",
+			map[string]string{
+				"basic.containerImageTag": "1.15.8",
+				"containerImageRepo":      "nginx",
+				"containerImageTag":       "1.15.8",
+			},
+		},
+		{
+			"MissingContainerImageTag in dependent chart",
+			map[string]string{
+				"containerImageRepo":       "nginx",
+				"basic.containerImageRepo": "nginx",
+				"basic.containerImageTag":  "1.15.8",
+			},
+		},
+		{
+			"MissingContainerImageTag in basic chart",
+			map[string]string{
+				"basic.containerImageRepo": "nginx",
+				"containerImageRepo":       "nginx",
+				"containerImageTag":        "1.15.8",
+			},
+		},
+	}
+
+	// Now we iterate over each test case and spawn a sub test
+	for _, testCase := range testCases {
+		// Here, we capture the range variable and force it into the scope of this block. If we don't do this, when the
+		// subtest switches contexts (because of t.Parallel), the testCase value will have been updated by the for loop
+		// and will be the next testCase!
+		testCase := testCase
+
+		// The actual sub test spawning. We name the sub test using the human friendly name. Note that we name the sub
+		// test T struct to subT to make it clear which T struct corresponds to which test. However, in most cases you
+		// will not reference the main test T so you can name it the same.
+		t.Run(testCase.name, func(subT *testing.T) {
+			// subT.Parallel()
+
+			// Now we try rendering the template, but verify we get an error
+			options := &helm.Options{SetValues: testCase.values}
+			_, err := helm.RenderTemplateE(t, options, helmChartPath, releaseName, []string{})
+			require.Error(t, err)
+		})
+	}
+}