Running Tests with Helm

Running Tests with Helm

Writing tests is a key task in our day-to-day work to ensure the quality of our code. While we usually run unit tests during the build process, others types of tests, such as acceptance tests, require to have the application previously deployed in the environment.

In a previous post, we reviewed Helm as a tool to deploy our applications into a Kubernetes cluster. Today we will explore a feature that could make our life easier when faced with this last type of testing.

We will use the default ASP.NET Core Web API template to have an application to test. In our case, the project will be called helm-sandbox-app. Add a Dockerfile to the project with the following content:

FROM mcr.microsoft.com/dotnet/aspnet:6.0 AS base
WORKDIR /app
EXPOSE 80

FROM mcr.microsoft.com/dotnet/sdk:6.0 AS build
WORKDIR /src
COPY ["helm-sandbox-app/helm-sandbox-app.csproj", "helm-sandbox-app/"]
RUN dotnet restore "helm-sandbox-app/helm-sandbox-app.csproj"
COPY . .
WORKDIR "/src/helm-sandbox-app"
RUN dotnet build "helm-sandbox-app.csproj" -c Release -o /app/build

FROM build AS publish
RUN dotnet publish "helm-sandbox-app.csproj" -c Release -o /app/publish

FROM base AS final
WORKDIR /app
COPY --from=publish /app/publish .
ENTRYPOINT ["dotnet", "helm-sandbox-app.dll"]

Next, we will create an MSTest Test project called helm-sandbox-tests. Add the following NuGet package to the project:

Modify the default test class with the following code:

[TestClass]
public class WeatherForecastTests
{
    private readonly string _uri;

    [TestMethod]
    public async Task should_return_ok()
    {
        using (var client = new HttpClient())
        {
            client.BaseAddress = new Uri(_uri);

            var response = await client.GetAsync("WeatherForecast");

            Assert.IsNotNull(response);

            Assert.AreEqual(HttpStatusCode.OK, response.StatusCode);
        }
    }

    public WeatherForecastTests()
    {
        var config = new ConfigurationBuilder()
        .AddEnvironmentVariables()
        .Build();

        _uri = config["Uri"];
    }
}

Add a Dockerfile to the project with the following content:

FROM mcr.microsoft.com/dotnet/sdk:6.0 AS build
WORKDIR /src
COPY ["helm-sandbox-tests/helm-sandbox-tests.csproj", "helm-sandbox-tests/"]
RUN dotnet restore "helm-sandbox-tests/helm-sandbox-tests.csproj"
COPY . .
WORKDIR "/src/helm-sandbox-tests"
ENTRYPOINT ["dotnet", "test", "helm-sandbox-tests.csproj","--no-restore"]

Time to generate our local Docker images (one for your API and the other to test the API). At the solution level, run (don't forget the dot at the end):

docker build -t raulnq/helm-sandbox-app:1.0 -f .\helm-sandbox-app\Dockerfile .
docker build -t raulnq/helm-sandbox-tests:1.0 -f .\helm-sandbox-tests\Dockerfile .

Let's create our helm package:

mkdir helm
cd helm
helm create helm-sandbox-app

Keep only these files in your Helm package (rename the default test-connection.yaml to test.yaml):

helm\helm-sandbox-app
|--test
|   `-- test.yaml
|-- templates
|   |-- _helpers.tpl
|   |-- deployment.yaml
|   `-- service.yaml
|-- .helmignore
|-- Chart.yaml
`-- values.yaml

Modify the deployment.yaml file as follows:

apiVersion: apps/v1
kind: Deployment
metadata:
  name: {{ include "helm-sandbox-app.fullname" . }}
  labels:
    {{- include "helm-sandbox-app.labels" . | nindent 4 }}
spec:
  replicas: {{ .Values.replicaCount }}
  selector:
    matchLabels:
      {{- include "helm-sandbox-app.selectorLabels" . | nindent 6 }}
  template:
    metadata:
      labels:
        {{- include "helm-sandbox-app.selectorLabels" . | nindent 8 }}
    spec:
      containers:
        - name: {{ .Chart.Name }}
          image: "{{ .Values.image.repository }}:{{ .Values.image.tag | default .Chart.AppVersion }}"
          imagePullPolicy: {{ .Values.image.pullPolicy }}
          ports:
            - name: http
              containerPort: 80
              protocol: TCP

The service.yaml file:

apiVersion: v1
kind: Service
metadata:
  name: {{ include "helm-sandbox-app.fullname" . }}
  labels:
    {{- include "helm-sandbox-app.labels" . | nindent 4 }}
spec:
  type: {{ .Values.service.type }}
  ports:
    - port: {{ .Values.service.port }}
      targetPort: http
      protocol: TCP
      name: http
  selector:
    {{- include "helm-sandbox-app.selectorLabels" . | nindent 4 }}

The test.yaml file:

apiVersion: v1
kind: Pod
metadata:
  name: "{{ include "helm-sandbox-app.fullname" . }}-test"
  labels:
    {{- include "helm-sandbox-app.labels" . | nindent 4 }}
  annotations:
    "helm.sh/hook": test-success
    "helm.sh/hook-delete-policy": before-hook-creation
spec:
  containers:
    - name: {{ .Chart.Name }}
      image: "{{ .Values.tests.repository }}:{{ .Values.tests.tag | default .Chart.AppVersion }}"
      imagePullPolicy: {{ .Values.tests.pullPolicy }}
      env:
      - name: Uri
        value: "http://{{ include "helm-sandbox-app.fullname" . }}:{{ .Values.service.port }}"
  restartPolicy: Never

And the values.yaml file:

replicaCount: 1

image:
  repository: raulnq/helm-sandbox-app
  pullPolicy: IfNotPresent
  tag: "1.0"

tests:
  repository: raulnq/helm-sandbox-tests
  pullPolicy: IfNotPresent
  tag: "1.0"

nameOverride: ""
fullnameOverride: ""

service:
  type: ClusterIP
  port: 80

Run the following command to install your chart in your local Kubernetes cluster:

helm upgrade release-helm-sandbox-app helm-sandbox-app --install --debug

And finally, the helm test command:

helm test release-helm-sandbox-app

If everything goes fine, we will see an output like this:

Pod release-helm-sandbox-app-test pending
Pod release-helm-sandbox-app-test pending
Pod release-helm-sandbox-app-test pending
Pod release-helm-sandbox-app-test running
Pod release-helm-sandbox-app-test succeeded
NAME: release-helm-sandbox-app
LAST DEPLOYED: Sat Jun 18 10:57:44 2022
NAMESPACE: default
STATUS: deployed
REVISION: 1
TEST SUITE:     release-helm-sandbox-app-test
Last Started:   Sat Jun 18 22:59:42 2022
Last Completed: Sat Jun 18 22:59:47 2022
Phase:          Succeeded

We can check the logs of our pod with:

kubectl logs release-helm-sandbox-app-test

Having as a result:

helm-sandbox-tests -> /src/helm-sandbox-tests/bin/Debug/net6.0/helm-sandbox-tests.dll
Test run for /src/helm-sandbox-tests/bin/Debug/net6.0/helm-sandbox-tests.dll (.NETCoreApp,Version=v6.0)
Microsoft (R) Test Execution Command Line Tool Version 17.2.0 (x64)
Copyright (c) Microsoft Corporation.  All rights reserved.

Starting test execution, please wait...
A total of 1 test files matched the specified pattern.

Passed!  - Failed:     0, Passed:     1, Skipped:     0, Total:     1, Duration: 115 ms - /src/helm-sandbox-tests/bin/Debug/net6.0/helm-sandbox-tests.dll (net6.0)

Here you can find the official documentation about the helm test command. The code of this post can be found here.