---
title_tag: "Unit Testing Pulumi Programs"
meta_desc: "Guide to unit testing of Pulumi programs: mock-based tests across Node.js, Python, Go, and .NET."
title: Unit testing
h1: Unit testing Pulumi programs
meta_image: /images/docs/meta-images/docs-meta.png
weight: 1
menu:
usingpulumi:
parent: testing
aliases:
- /docs/guides/testing/unit/
---
Pulumi programs are authored in a general-purpose language like TypeScript, Python, Go, C# or Java. The full power of each language is available, including access to tools and libraries for that runtime, including testing frameworks.
When running an update, your Pulumi program talks to the Pulumi CLI to orchestrate the deployment. The idea of _unit tests_ is to cut this communication channel and replace the engine with mocks. The mocks respond to the commands from within the same OS process and return dummy data for each call that your Pulumi program makes.
Because mocks don't execute any real work, unit tests run very fast. Also, they can be made deterministic because tests don't depend on the behavior of any external system.
## Get Started
Let's build a sample test suite. The example uses AWS resources, but the same capabilities and workflow apply to any Pulumi provider. To follow along, complete the [Get Started with AWS](/docs/clouds/aws/get-started/) guide to set up a basic Pulumi program in your language of choice.
Note that unit tests are supported in all [existing Pulumi runtimes](https://www.pulumi.com/docs/languages-sdks/).
## Sample Program
Throughout this guide, we are testing a program that creates a simple AWS EC2-based webserver. We want to develop unit tests to ensure:
- Instances have a Name tag.
- Instances must not use an inline `userData` script—we must use a virtual machine image.
- Instances must not have SSH open to the Internet.
Our starting code is loosely based on the [aws-js-webserver example](https://github.com/pulumi/examples/tree/master/aws-js-webserver):
{{< notes >}}
Choose a language below to adjust the contents of this guide. Your choice is applied throughout the guide.
{{< /notes >}}
{{< chooser language "typescript,python,go,csharp" / >}}
{{% choosable language "typescript" %}}
index.ts:
```typescript
import * as aws from "@pulumi/aws";
export const group = new aws.ec2.SecurityGroup("web-secgrp", {
ingress: [
{ protocol: "tcp", fromPort: 22, toPort: 22, cidrBlocks: ["0.0.0.0/0"] },
{ protocol: "tcp", fromPort: 80, toPort: 80, cidrBlocks: ["0.0.0.0/0"] },
],
});
const userData = `#!/bin/bash echo "Hello, World!" > index.html nohup python -m SimpleHTTPServer 80 &`;
export const server = new aws.ec2.Instance("web-server-www", {
instanceType: "t2.micro",
securityGroups: [ group.name ], // reference the group object above
ami: "ami-c55673a0", // AMI for us-east-2 (Ohio)
userData: userData, // start a simple webserver
});
```
{{% /choosable %}}
{{% choosable language "python" %}}
infra.py:
```python
import pulumi
from pulumi_aws import ec2
group = ec2.SecurityGroup('web-secgrp', ingress=[
{ "protocol": "tcp", "from_port": 22, "to_port": 22, "cidr_blocks": ["0.0.0.0/0"] },
{ "protocol": "tcp", "from_port": 80, "to_port": 80, "cidr_blocks": ["0.0.0.0/0"] },
])
user_data = '#!/bin/bash echo "Hello, World!" > index.html nohup python -m SimpleHTTPServer 80 &'
server = ec2.Instance('web-server-www;',
instance_type="t2.micro",
security_groups=[ group.name ], # reference the group object above
ami="ami-c55673a0", # AMI for us-east-2 (Ohio)
user_data=user_data) # start a simple web server
```
{{% /choosable %}}
{{% choosable language "go" %}}
main.go:
```go
package main
import (
"github.com/pulumi/pulumi-aws/sdk/v4/go/aws/ec2"
"github.com/pulumi/pulumi/sdk/v3/go/pulumi"
)
type infrastructure struct {
group *ec2.SecurityGroup
server *ec2.Instance
}
func createInfrastructure(ctx *pulumi.Context) (*infrastructure, error) {
group, err := ec2.NewSecurityGroup(ctx, "web-secgrp", &ec2.SecurityGroupArgs{
Ingress: ec2.SecurityGroupIngressArray{
ec2.SecurityGroupIngressArgs{
Protocol: pulumi.String("tcp"),
FromPort: pulumi.Int(22),
ToPort: pulumi.Int(22),
CidrBlocks: pulumi.StringArray{pulumi.String("0.0.0.0/0")},
},
ec2.SecurityGroupIngressArgs{
Protocol: pulumi.String("tcp"),
FromPort: pulumi.Int(80),
ToPort: pulumi.Int(80),
CidrBlocks: pulumi.StringArray{pulumi.String("0.0.0.0/0")},
},
},
})
if err != nil {
return nil, err
}
const userData = `#!/bin/bash echo "Hello, World!" > index.html nohup python -m SimpleHTTPServer 80 &`
server, err := ec2.NewInstance(ctx, "web-server-www", &ec2.InstanceArgs{
InstanceType: pulumi.String("t2-micro"),
SecurityGroups: pulumi.StringArray{group.ID()}, // reference the group object above
Ami: pulumi.String("ami-c55673a0"), // AMI for us-east-2 (Ohio)
UserData: pulumi.String(userData), // start a simple web server
})
if err != nil {
return nil, err
}
return &infrastructure{
group: group,
server: server,
}, nil
}
```
{{% /choosable %}}
{{% choosable language "csharp" %}}
WebserverStack.cs:
``` csharp
using Pulumi;
using Pulumi.Aws.Ec2;
using Pulumi.Aws.Ec2.Inputs;
public class WebserverStack : Stack
{
public WebserverStack()
{
var group = new SecurityGroup("web-secgrp", new SecurityGroupArgs
{
Ingress =
{
new SecurityGroupIngressArgs { Protocol = "tcp", FromPort = 22, ToPort = 22, CidrBlocks = { "0.0.0.0/0" } },
new SecurityGroupIngressArgs { Protocol = "tcp", FromPort = 80, ToPort = 80, CidrBlocks = { "0.0.0.0/0" } }
}
});
var userData = "#!/bin/bash echo \"Hello, World!\" > index.html nohup python -m SimpleHTTPServer 80 &";
var server = new Instance("web-server-www", new InstanceArgs
{
InstanceType = "t2.micro",
SecurityGroups = { group.Name }, // reference the group object above
Ami = "ami-c55673a0", // AMI for us-east-2 (Ohio)
UserData = userData // start a simple webserver
});
}
}
```
{{% /choosable %}}
This basic Pulumi program allocates a security group and an instance. Notice, however, that we are violating all three of the rules stated above—let's write some tests!
## Install the unit testing framework
You are free to use your favorite frameworks and libraries for writing unit tests and assertions.
{{% choosable language "typescript" %}}
This guide uses Mocha as the testing framework. [Install Mocha](https://mochajs.org/#installation) to your development environment.
```bash
npm install --global mocha
```
Then, install additional NPM modules to your program:
```bash
npm install mocha @types/mocha ts-node --global --save-dev
```
{{% /choosable %}}
{{% choosable language python %}}
We use the built-in [`unittest`](https://docs.python.org/3/library/unittest.html) framework, so no need to install anything.
{{% /choosable %}}
{{% choosable language go %}}
We use the built-in `go test` command, so no need to install anything.
{{% /choosable %}}
{{% choosable language "csharp" %}}
We use [NUnit](https://nunit.org/) test framework to define and run the tests, [Moq](https://github.com/moq/moq4) for mocks, and [FluentAssertions](https://github.com/fluentassertions/fluentassertions) for assertions.
Install the corresponding NuGet packages to your program:
```bash
dotnet add package NUnit
dotnet add package NUnit3TestAdapter
dotnet add package Moq
dotnet add package FluentAssertions
dotnet add package Microsoft.NET.Test.Sdk
```
{{% /choosable %}}
## Add Mocks
Let's add the following code to mock the external calls to the Pulumi CLI.
{{% choosable language "typescript" %}}
ec2tests.ts:
```ts
import * as pulumi from "@pulumi/pulumi";
pulumi.runtime.setMocks({
newResource: function(args: pulumi.runtime.MockResourceArgs): {id: string, state: any} {
return {
id: args.inputs.name + "_id",
state: args.inputs,
};
},
call: function(args: pulumi.runtime.MockCallArgs) {
return args.inputs;
},
},
"project",
"stack",
false, // Sets the flag `dryRun`, which indicates if pulumi is running in preview mode.
);
```
{{% /choosable %}}
{{% choosable language python %}}
test_ec2.py:
```python
import pulumi
class MyMocks(pulumi.runtime.Mocks):
def new_resource(self, args: pulumi.runtime.MockResourceArgs):
return [args.name + '_id', args.inputs]
def call(self, args: pulumi.runtime.MockCallArgs):
return {}
pulumi.runtime.set_mocks(
MyMocks(),
preview=False, # Sets the flag `dry_run`, which is true at runtime during a preview.
)
```
{{% /choosable %}}
{{% choosable language go %}}
main_test.go:
```go
import (
"github.com/pulumi/pulumi/sdk/v3/go/common/resource"
"github.com/pulumi/pulumi/sdk/v3/go/pulumi"
)
type mocks int
func (mocks) NewResource(args pulumi.MockResourceArgs) (string, resource.PropertyMap, error) {
return args.Name + "_id", args.Inputs, nil
}
func (mocks) Call(args pulumi.MockCallArgs) (resource.PropertyMap, error) {
return args.Args, nil
}
```
{{% /choosable %}}
{{% choosable language "csharp" %}}
Testing.cs:
```csharp
using System.Collections.Immutable;
using System.Threading.Tasks;
using Pulumi;
using Pulumi.Testing;
namespace UnitTesting
{
class Mocks : IMocks
{
public Task<(string? id, object state)> NewResourceAsync(MockResourceArgs args)
{
var outputs = ImmutableDictionary.CreateBuilder();
outputs.AddRange(args.Inputs);
if (args.Type == "aws:ec2/instance:Instance")
{
outputs.Add("publicIp", "203.0.113.12");
outputs.Add("publicDns", "ec2-203-0-113-12.compute-1.amazonaws.com");
}
args.Id ??= $"{args.Name}_id";
return Task.FromResult<(string? id, object state)>((args.Id, (object)outputs));
}
public Task