The bridge relies on several complementary test layers to make sure Pulumi behavior stays aligned with Terraform while honoring Pulumi semantics. This guide explains when to reach for each layer, how to run it locally, and where to look for examples.
| Use case | Recommended suite | Location | Notes |
|---|---|---|---|
| Unit logic (helpers, conversions) | Go unit tests | Same package (*_test.go) |
Fast feedback without Pulumi/Terraform harnesses. |
| Runtime behavior with Pulumi engine (SDKv2) | Schema + program integration tests | pkg/tests/ |
Runs Pulumi programs via Automation API to exercise full engine↔provider flows; default choice for bridge work. |
| Runtime behavior with Pulumi engine (PF) | PF schema + program tests | pkg/pf/tests/ |
Mirrors SDKv2 harness with PF builders and Pulumi Automation. |
| Terraform parity | Cross-tests | pkg/internal/tests/cross-tests/, pkg/pf/tests/internal/cross-tests/ |
Compares Terraform CLI vs Pulumi for the same provider. |
| Property fuzzing | Rapid-based cross-tests | pkg/internal/tests/cross-tests/rapid_test.go |
Generates many input combos; slower but valuable for tricky schemas. |
| Use case | Suite | Location | Notes |
|---|---|---|---|
| Provider-only runtime behavior (SDKv2) | Provider server tests | pkg/tfbridge/tests/ |
Calls the bridge directly without Pulumi; use only when Automation-backed tests cannot cover the scenario. |
| Repro recorded RPCs | Replay tests | pkg/tfbridge/provider_test.go |
Legacy gRPC recordings; prefer higher-level harnesses whenever possible. |
# Fast lint + unit
make lint
make test RUN_TEST_CMD=./pkg/tfbridge -run TestSomeUnit
# SDKv2 integration (single file)
make test RUN_TEST_CMD='./pkg/tests -run TestFoo'
# PF integration
make test RUN_TEST_CMD='./pkg/pf/tests -run TestBar'
# Cross-tests (SDKv2)
make test RUN_TEST_CMD=./pkg/internal/tests/cross-tests
# Accept golden updates after intentional diff changes
make test_accept
# Accept autogold updates after intentional diff changes
make test RUN_TEST_CMD='./pkg/pf/tests/internal/cross-tests -run TestWritePFHCLProvider -update'make test installs required Pulumi plugins and builds the test provider binary, so the first run can take several minutes.
- Regression Tests - uses schema + program tests and create individual test cases
- Proactive Hardening - uses rapid/property based testing. For when you are trying to figure out edge cases. Only appropriate for the stable core of the system
- Matching TF - use cross-tests. Anytime you are wanting to ensure that Pulumi and Terraform CLI remain aligned you should use cross-tests.
- Regular Tests - use schema + program tests. Simple Go unit tests are not as useful for this codebase. There is test infrastructure in place to make these integration tests pretty fast to run.
Integration tests spin up a minimal Terraform provider schema and drive it through a Pulumi program using
pulumiTest. They are the preferred way to cover
end-to-end scenarios, including multi-step workflows.
pkg/tests/schema_pulumi_test.go shows the basic pattern:
func TestBasic(t *testing.T) {
t.Parallel()
tfResourceMap := map[string]*schema.Resource{
"prov_test": {
Schema: map[string]*schema.Schema{
"test": {Type: schema.TypeString, Optional: true},
},
},
}
tfProvider := &schema.Provider{ResourcesMap: tfResourceMap}
bridgedProvider := pulcheck.BridgedProvider(t, "prov", tfProvider)
program := `
name: test
runtime: yaml
resources:
mainRes:
type: prov:index:Test
properties:
test: "hello"
outputs:
testOut: ${mainRes.test}
`
pt := pulcheck.PulCheck(t, bridgedProvider, program)
upResult := pt.Up(t)
require.Equal(t, "hello", upResult.Outputs["testOut"].Value)
}Key points:
- Build the minimal Terraform schema necessary to exercise the behavior under test.
- Wrap the Terraform provider with
pulcheck.BridgedProviderto obtain a Pulumi provider. - Use a short Pulumi program (YAML works well) and assert on outputs, state transitions, or logs through the
pulumiTestharness.
PF tests follow the same pattern with different helpers (pkg/pf/tests/schema_and_program_test.go):
func TestBasic(t *testing.T) {
t.Parallel()
provBuilder := providerbuilder.NewProvider(providerbuilder.NewProviderArgs{
AllResources: []providerbuilder.Resource{
{
Name: "test",
ResourceSchema: rschema.Schema{
Attributes: map[string]rschema.Attribute{
"s": rschema.StringAttribute{Optional: true},
},
},
},
},
})
prov := bridgedProvider(provBuilder)
program := `
name: test
runtime: yaml
resources:
mainRes:
type: testprovider:index:Test
properties:
s: "hello"
outputs:
testOut: ${mainRes.s}
`
pt, err := pulcheck.PulCheck(t, prov, program)
require.NoError(t, err)
upResult := pt.Up(t)
require.Equal(t, "hello", upResult.Outputs["testOut"].Value)
}PF adds builders to compose provider resources and separate pulcheck helpers, but the assertions and workflow match the
SDKv2 pattern.
When you need to validate the full Pulumi engine ↔ provider interaction, prefer these Pulumi-backed suites (pkg/tests/
for Plugin SDK and pkg/pf/tests/ for Plugin Framework). They surface Automation API behavior, secrets handling, and
multi-step workflows exactly as users experience them and should be the default choice for new coverage.
pkg/tfbridge/tests/ exercises the bridge without running a Pulumi program. Tests construct a provider server directly
via helpers such as newTestProvider or crosstests.MakeConfigure, then call RPCs (or feed gRPC recordings) to inspect
the raw bridge behavior. See pkg/tfbridge/tests/provider_configure_test.go for configure parity coverage and
pkg/tfbridge/tests/provider_test.go for gRPC replay and regression scenarios.
If you are not certain you need a provider-only test, you almost certainly want a Pulumi-backed test in
pkg/tests/instead.
Use this suite when:
- You have a specific reason to bypass Automation API.
- You maintain paired coverage: a Pulumi-backed test in
pkg/tests/plus a provider-only variant inpkg/tfbridge/tests/to isolate whether a regression requires Pulumi in the loop.
Provider-only tests complement, not replace, Pulumi-backed coverage: once behavior is stable, ensure user-visible flows
still pass under pkg/tests/ so Pulumi integration remains exercised end-to-end.
Cross-tests run Terraform CLI and the Pulumi bridge against the same inputs, then compare results. They live under
pkg/internal/tests/cross-tests/ (SDKv2) and pkg/pf/tests/internal/cross-tests/ (PF). Use them when validating parity
or investigating production regressions.
Examples worth consulting:
- Diff parity (
pkg/internal/tests/cross-tests/diff_cross_test.go) - Refresh regressions (
pkg/tests/refresh_cross_test.go) - Create/Update flows (
pkg/tfbridge/tests/provider_test.go) - Provider configuration (
pkg/tfbridge/tests/provider_configure_test.go,pkg/pf/tests/provider_configure_test.go) - PF-specific diffing (
pkg/pf/tests/diff_test.go)
Property-based tests extend cross-tests with randomized inputs using the
Rapid library (pkg/internal/tests/cross-tests/rapid_test.go). These tests
are currently intended to be used as a local tool. They are not yet in a stable place where
we can run these in CI without failures.
Replay tests feed recorded Pulumi Engine ↔ Provider RPC traces back through the bridge (see
pkg/tfbridge/provider_test.go). They are helpful when higher-level harnesses are impractical, but new coverage should
use schema + program or cross-tests instead. Treat replays as stop-gaps and backfill more maintainable coverage later.
Some tests assert against generated output stored alongside fixtures, while others use the autogold package. When behavior changes intentionally:
- Run
make test_accept(setsPULUMI_ACCEPT=1) to update goldens.
- For
autogoldtests, update individual tests using the-updateflag (e.g.make test RUN_TEST_CMD='./pkg/pf/tests/internal/cross-tests -run TestWritePFHCLProvider -update')
- Inspect diffs carefully and justify them in your PR description.
- Coordinate with maintainers when changes could affect downstream providers.
- Set
PULUMI_DEBUG_GRPC=1for verbose Pulumi provider logs. - Enable Terraform tracing with
TF_LOG=DEBUGorTF_LOG=TRACEwhen Terraform behavior is surprising.
- GitHub Actions run
make lintandmake testwith caching. - Coverage reports aggregate into
coverage.txtand feed the configuration incodecov.yml.