The pkg/test directory provides a unit testing framework for the wasm-go project, helping plugin developers write and run high-quality unit tests. The framework supports running tests in both Go mode and Wasm mode.
Running test with the compiled wasm binary helps to ensure that the plugin will run when actually compiled to wasm, however stack traces and other debug features will be much worse. It is recommended to run unit tests both with Go and with wasm. Tests will run much faster under Go mode for quicker development cycles, and the wasm mode test can confirm the behavior matches when actually compiled.
host.go- ProvidesTestHostinterface to simulate host(envoy) behaviorredis.go- Provides Redis response building utility functionstest.go- Provides test runners supporting both Go mode and Wasm modeutils.go- Provides utility functions for header testing
Runs tests in both Go mode and Wasm mode simultaneously, ensuring the plugin works correctly in both environments.
func TestMyPlugin(t *testing.T) {
test.RunTest(t, func(t *testing.T) {
// Your test code
})
}Runs tests in both Go mode and Wasm mode with a specified wasm file path. This function allows callers to specify custom wasm file paths for testing.
Runs tests only in Go mode using ABI host call mock interfaces.
Runs tests only in Wasm mode using intelligent wasm file path detection in wazero runtime.
Runs tests only in Wasm mode with a specified wasm file path. This function allows callers to specify custom wasm file paths for testing.
Note: The framework automatically compiles wasm binaries when needed. The framework supports multiple ways to specify the wasm file path:
- Environment Variable: Set
WASM_FILE_PATHenvironment variable - Custom Path: Use
RunWasmTestWithPath()orRunTestWithPath()functions - Auto-compilation: The framework automatically compiles wasm binariy with a fixed filename (
wasm-unit-test.wasm) - Debug-Friendly: Panics are preserved in test environment for better debugging, while still recovered in production
The framework searches for wasm files in the following locations (in order of priority):
main.wasm- Default wasm file name in project rootplugin.wasm- Alternative wasm file name in project root
Example project structure:
my-wasm-plugin/
├── main.go
├── main.wasm
├── go.mod
└── pkg/
Note: The auto-detection only searches in the current working directory. For more complex project structures, use environment variables or explicit path functions.
# Compile wasm binary
GOOS=wasip1 GOARCH=wasm go build -buildmode=c-shared -o main.wasm ./
# Or specify custom path via environment variable
export WASM_FILE_PATH="build/plugin.wasm"
go test ./...Creates a test host instance to simulate the Envoy proxy environment. The config parameter represents the configuration for the wasm plugin.
config := json.RawMessage(`{"key": "value"}`)
host, status := test.NewTestHost(config)
require.Equal(t, types.OnPluginStartStatusOK, status)
defer host.Reset()CallOnHttpRequestHeaders(headers [][2]string) types.Action- Call request header processingCallOnHttpRequestBody(body []byte) types.Action- Call request body processingCallOnHttpStreamingRequestBody(body []byte, endOfStream bool) types.Action- Call streaming request body processing
CallOnHttpResponseHeaders(headers [][2]string) types.Action- Call response header processingCallOnHttpResponseBody(body []byte) types.Action- Call response body processingCallOnHttpStreamingResponseBody(body []byte, endOfStream bool) types.Action- Call streaming response body processing
CallOnHttpCall(headers [][2]string, body []byte)- Simulate HTTP call responseCallOnRedisCall(status int32, response []byte)- Simulate Redis call responseGetHttpCalloutAttributes() []proxytest.HttpCalloutAttribute- Get HTTP callout attributes (outbound http calls made by the plugin)GetRedisCalloutAttributes() []proxytest.RedisCalloutAttribute- Get Redis callout attributes (outbound redis calls made by the plugin)
GetMatchConfig() (any, error)- Get match configuration
SetRouteName(routeName string) error- Set route nameSetClusterName(clusterName string) error- Set cluster nameSetRequestId(requestId string) error- Set request IDGetProperty(path []string) ([]byte, error)- Get property data from the host for a given pathSetProperty(path []string, data []byte) error- Set property data on the host for a given path
GetHttpStreamAction() types.Action- Get HTTP stream actionGetRequestHeaders() [][2]string- Get request headersGetResponseHeaders() [][2]string- Get response headersGetRequestBody() []byte- Get request bodyGetResponseBody() []byte- Get response bodyGetLocalResponse() *proxytest.LocalHttpResponse- Get local response
GetCounterMetric(name string) (uint64, error)- Get the value for the counter metric in the hostGetGaugeMetric(name string) (uint64, error)- Get the value for the gauge metric in the hostGetHistogramMetric(name string) (uint64, error)- Get the value for the histogram metric in the host
GetTraceLogs() []string- Get the trace logs that have been collected in the hostGetDebugLogs() []string- Get the debug logs that have been collected in the hostGetInfoLogs() []string- Get the info logs that have been collected in the hostGetWarnLogs() []string- Get the warn logs that have been collected in the hostGetErrorLogs() []string- Get the error logs that have been collected in the hostGetCriticalLogs() []string- Get the critical logs that have been collected in the host
CompleteHttp()- Complete HTTP requestReset()- Reset test host state
GetTickPeriod() uint32- Get the current tick period in the hostTick()- Execute types.PluginContext.OnTick in the plugin
CreateRedisResp(value interface{}) []byte- Create Redis response for any typeCreateRedisRespArray(values []interface{}) []byte- Create array response for any type
CreateRedisRespString(value string) []byte- Create string responseCreateRedisRespInt(value int) []byte- Create integer responseCreateRedisRespBool(value bool) []byte- Create boolean responseCreateRedisRespFloat(value float64) []byte- Create float responseCreateRedisRespNull() []byte- Create null responseCreateRedisRespError(message string) []byte- Create error response
HasHeader(headers [][2]string, headerName string) bool- Check if headers contain a header with the specified name (case-insensitive)GetHeaderValue(headers [][2]string, headerName string) (string, bool)- Get the value of the specified header, returns value and found status (case-insensitive)HasHeaderWithValue(headers [][2]string, headerName, expectedValue string) bool- Check if headers contain a header with the specified name and value (case-insensitive)
These utility functions are particularly useful for testing HTTP header processing in your wasm plugins. They provide case-insensitive header matching, which is important for HTTP compliance.
func TestMyPlugin(t *testing.T) {
test.RunTest(t, func(t *testing.T) {
// 1. Create test host
config := json.RawMessage(`{"key": "value"}`)
host, status := test.NewTestHost(config)
require.Equal(t, types.OnPluginStartStatusOK, status)
defer host.Reset()
// 2. Set request headers
headers := [][2]string{
{":method", "GET"},
{":path", "/test"},
{":authority", "test.com"},
}
// 3. Call plugin methods
action := host.CallOnHttpRequestHeaders(headers)
require.Equal(t, types.ActionPause, action)
// 4. Verify outbound calls made by the plugin (if any)
// httpCallouts := host.GetHttpCalloutAttributes()
// require.Len(t, httpCallouts, 1)
// assert.Equal(t, "httpbin.org", httpCallouts[0].Upstream)
// assert.Equal(t, "GET", test.GetHeaderValue(httpCallouts[0].Headers, ":method"))
// 5. Simulate external call responses (if needed)
// host.CallOnRedisCall(0, test.CreateRedisRespString("OK"))
// host.CallOnHttpCall([][2]string{{":status", "200"}}, []byte(`{"result": "success"}`))
// 6. Complete request
host.CompleteHttp()
// 7. Verify results
localResponse := host.GetLocalResponse()
require.NotNil(t, localResponse)
assert.Equal(t, uint32(200), localResponse.StatusCode)
})
}func TestStreamingRequest(t *testing.T) {
test.RunTest(t, func(t *testing.T) {
host, status := test.NewTestHost(testConfig)
require.Equal(t, types.OnPluginStartStatusOK, status)
defer host.Reset()
// Set request headers
headers := [][2]string{
{":method", "GET"},
{":path", "/stream"},
{":authority", "test.com"},
}
// Call request header processing
action := host.CallOnHttpRequestHeaders(headers)
require.Equal(t, types.ActionPause, action)
// Simulate streaming response body
action = host.CallOnHttpStreamingRequestBody([]byte("chunk1"), false)
assert.Equal(t, types.ActionContinue, action)
action = host.CallOnHttpStreamingRequestBody([]byte("chunk2"), true)
assert.Equal(t, types.ActionContinue, action)
// Complete request
host.CompleteHttp()
})
}var testConfig = func() json.RawMessage {
data, _ := json.Marshal(map[string]interface{}{
// Global config - applies to all requests when no specific rule matches
"name": "john",
// Rules for specific route matching
"_rules_": []map[string]interface{}{
{
"_match_route_": []string{"foo"}, // route level config
"name": "foo",
},
{
"_match_domain_": []string{"foo.bar.com"}, // domain level config
"name": "foo.bar.com",
}
},
})
return data
}()
func TestParseConfig(t *testing.T) {
test.RunGoTest(t, func(t *testing.T) {
host, status := test.NewTestHost(testConfig)
require.Equal(t, types.OnPluginStartStatusOK, status)
defer host.Reset()
// Get global plugin configuration
config, err := host.GetMatchConfig()
// Get plugin configuration with match route
host.SetRouteName("foo")
config, err = host.GetMatchConfig()
// Get plugin configuration with match cluster
host.SetClusterName("service")
config, err = host.GetMatchConfig()
// Get plugin configuration with match domain
host.SetDomainName("foo.bar.com")
config, err = host.GetMatchConfig()
// Verify configuration content
// ... Your configuration validation logic
})
}Note: GetMatchConfig() can only be used in RunGoTest() mode because they are not the proxy-wasm ABI interface. These functions use Go reflection to expose internal plugin configuration for testing.
- Use
RunTest()to ensure the plugin works in both modes - Use
RunGoTest()for rapid iteration during development - Always use
RunWasmTest()before release to verify compiled behavior - Use
RunTestWithPath()orRunWasmTestWithPath()when you need to specify custom wasm file paths
- Always use
defer host.Reset()to clean up test state - Create new test host instances at the beginning of each test function
- Use
requirefor precondition checks - Use
assertfor result verification - Provide clear error messages
- Use meaningful test data
- Test boundary conditions and error cases
- Simulate real network environments
- Use
GetHttpCalloutAttributes()andGetRedisCalloutAttributes()to verify external service calls - Test order: Verify outbound calls before simulating external responses
- Check that the plugin makes the expected outbound calls with correct parameters
- Verify upstream service names, headers, and request bodies
- Test both successful and failed external call scenarios
- Use environment variable
WASM_FILE_PATHfor consistent configuration across different environments - Leverage intelligent path detection for common project structures
- Use
RunTestWithPath()orRunWasmTestWithPath()for project-specific wasm file locations
- Test Isolation: Each test case should use an independent test host instance
- State Cleanup: Use
defer host.Reset()to ensure test state is properly cleaned up - Error Handling: Tests should verify the plugin's error handling logic
- Performance Considerations: Avoid creating too many objects or performing time-consuming operations in tests
- HTTP Request Lifecycle: If plugin implementing custom
onHttp*methods, follow the proper request lifecycle in test. Do not skip intermediate steps - if you implementonHttpRequestHeader, do not directly callonHttpRequestBody. - Wasm File Path: The framework automatically detects wasm files in common locations. For custom paths, use environment variables or explicit path functions to ensure consistent test execution across different environments.
- proxy-wasm-go-sdk - Underlying SDK
- examples/ - More test examples
- proxy-wasm specification - WebAssembly proxy specification
By using this testing framework, you can ensure your wasm-go plugin works correctly in various environments, improving code quality and reliability.
