Getting Started with tmpnet Testing
Set up your first Ginkgo test suite with tmpnet for testing Avalanche L1s
This guide shows you how to set up a Ginkgo test suite with tmpnet for testing Avalanche L1s. All testing with tmpnet should use Ginkgo for consistency and best practices.
Prerequisites
Install required packages:
go get github.com/onsi/ginkgo/v2
go get github.com/onsi/gomega
go get github.com/ava-labs/avalanchego/tests/fixture/tmpnet
go get github.com/ava-labs/avalanchego/tests/fixture/e2eBasic Test Suite Structure
1. Create Test Suite File
Create a test file with the standard Ginkgo setup:
package mypackage_test
import (
"context"
"flag"
"os"
"testing"
"time"
"github.com/ava-labs/avalanchego/tests/fixture/e2e"
"github.com/ava-labs/avalanchego/tests/fixture/tmpnet"
"github.com/ava-labs/avalanchego/utils/logging"
"github.com/onsi/ginkgo/v2"
. "github.com/onsi/gomega"
)
var (
network *tmpnet.Network
e2eFlags *e2e.FlagVars
)
// TestMain registers flags and runs tests
func TestMain(m *testing.M) {
e2eFlags = e2e.RegisterFlags()
flag.Parse()
os.Exit(m.Run())
}
// Test entry point
func TestE2E(t *testing.T) {
if os.Getenv("RUN_E2E") == "" {
t.Skip("Environment variable RUN_E2E not set; skipping E2E tests")
}
RegisterFailHandler(ginkgo.Fail)
ginkgo.RunSpecs(t, "My E2E Test Suite")
}Key Components:
TestMain: Registers e2e flags for network reuseRUN_E2Echeck: Gates tests so they only run when explicitly requestedRegisterFailHandler: Integrates Gomega assertions with GinkgoRunSpecs: Ginkgo test entry point
2. Network Lifecycle with BeforeSuite/AfterSuite
Create a network once for all tests:
var _ = ginkgo.BeforeSuite(func() {
// Create network context with timeout
ctx, cancel := context.WithTimeout(
context.Background(),
5*time.Minute,
)
defer cancel()
runtimeCfg, err := e2eFlags.NodeRuntimeConfig() // validates AVALANCHEGO_PATH/AVAGO_PLUGIN_DIR or CLI flags
Expect(err).NotTo(HaveOccurred())
// Create network configuration
network = &tmpnet.Network{
Owner: "my-test-network",
Nodes: tmpnet.NewNodesOrPanic(5),
DefaultRuntimeConfig: *runtimeCfg,
DefaultFlags: tmpnet.FlagsMap{
"log-level": "info",
"network-max-reconnect-delay": "1s",
},
}
// Bootstrap network
err = tmpnet.BootstrapNewNetwork(
ctx,
logging.NoLog{},
network,
e2eFlags.RootNetworkDir(), // empty string uses default ~/.tmpnet/networks
)
Expect(err).NotTo(HaveOccurred())
})
var _ = ginkgo.AfterSuite(func() {
if network != nil {
Expect(network.Stop(context.Background())).To(Succeed())
}
})Important Notes:
BeforeSuiteruns once before all tests in the suiteAfterSuiteensures cleanup even if tests fail- Use generous timeouts for network bootstrap (5+ minutes)
e2eFlagsenables network reuse (explained below)
3. Write Your First Test
var _ = ginkgo.Describe("[Basic Tests]", func() {
ginkgo.It("should have healthy nodes",
ginkgo.Label("smoke"),
func() {
Expect(network).NotTo(BeNil())
Expect(network.Nodes).To(HaveLen(5))
for _, node := range network.Nodes {
Expect(node.IsHealthy()).To(BeTrue())
}
})
ginkgo.It("should have valid URIs",
ginkgo.Label("smoke"),
func() {
for _, node := range network.Nodes {
Expect(node.URI).NotTo(BeEmpty())
}
})
})Running Tests
Basic Execution
# Run E2E tests
RUN_E2E=1 go test -v
# Run with Ginkgo directly
RUN_E2E=1 ginkgo -v
# Run specific test suite
RUN_E2E=1 ginkgo -v ./tests/my-suite/Filter by Label
# Run only smoke tests
RUN_E2E=1 ginkgo --label-filter="smoke" ./...
# Run all except slow tests
RUN_E2E=1 ginkgo --label-filter="!slow" ./...Filter by Name
# Run specific test
RUN_E2E=1 ginkgo --focus="should have healthy nodes" ./...
# Skip specific test
RUN_E2E=1 ginkgo --skip="flaky test" ./...Network Reuse for Faster Iteration
Network bootstrap is slow (2-5 minutes). Reuse networks across test runs:
First Run: Create Network
RUN_E2E=1 ginkgo -v
# Creates network in ~/.tmpnet/networks/[timestamp]The network directory will be printed:
Network created at: /home/user/.tmpnet/networks/20250312-143052.123456Subsequent Runs: Reuse Network
# Reuse existing network (skips bootstrap)
RUN_E2E=1 ginkgo -v -- --reuse-network --network-dir=/home/user/.tmpnet/networks/20250312-143052.123456
# Or export TMPNET_NETWORK_DIR and pass --reuse-network
export TMPNET_NETWORK_DIR=/home/user/.tmpnet/networks/20250312-143052.123456
RUN_E2E=1 ginkgo -v -- --reuse-networkStop Existing Networks
tmpnetctl stop-network --network-dir=/home/user/.tmpnet/networks/20250312-143052.123456Testing with Multiple L1s
Most tests need multiple L1s (chains) for cross-chain scenarios. Here's the standard two-L1 setup:
Complete Example
package mypackage_test
import (
"context"
"flag"
"math/big"
"os"
"testing"
"time"
"github.com/ava-labs/avalanchego/ids"
"github.com/ava-labs/avalanchego/tests/fixture/e2e"
"github.com/ava-labs/avalanchego/tests/fixture/tmpnet"
"github.com/ava-labs/avalanchego/utils/logging"
"github.com/ethereum/go-ethereum/crypto"
"github.com/ethereum/go-ethereum/ethclient"
"github.com/onsi/ginkgo/v2"
. "github.com/onsi/gomega"
)
// L1TestInfo holds information about an L1
type L1TestInfo struct {
SubnetID ids.ID
BlockchainID ids.ID
NodeURIs []string
RPCClient *ethclient.Client
EVMChainID *big.Int
Name string
}
var (
network *tmpnet.Network
e2eFlags *e2e.FlagVars
l1A L1TestInfo
l1B L1TestInfo
)
func TestMain(m *testing.M) {
e2eFlags = e2e.RegisterFlags()
flag.Parse()
os.Exit(m.Run())
}
func TestTwoL1s(t *testing.T) {
if os.Getenv("RUN_E2E") == "" {
t.Skip("RUN_E2E not set")
}
RegisterFailHandler(ginkgo.Fail)
ginkgo.RunSpecs(t, "Two L1s Test Suite")
}
var _ = ginkgo.BeforeSuite(func() {
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Minute)
defer cancel()
runtimeCfg, err := e2eFlags.NodeRuntimeConfig()
Expect(err).NotTo(HaveOccurred())
nodes := tmpnet.NewNodesOrPanic(4)
// Create network with 2 subnets (helpers like utils.NewTmpnetSubnet set genesis/config)
network = &tmpnet.Network{
Owner: "two-l1s-test",
Nodes: nodes,
Subnets: []*tmpnet.Subnet{
{
Name: "L1-A",
ValidatorIDs: tmpnet.NodesToIDs(nodes[:2]),
Chains: []*tmpnet.Chain{{
VMID: constants.EVMID,
Config: `{"log-level": "info"}`,
}},
},
{
Name: "L1-B",
ValidatorIDs: tmpnet.NodesToIDs(nodes[2:]),
Chains: []*tmpnet.Chain{{
VMID: constants.EVMID,
Config: `{"log-level": "info"}`,
}},
},
},
DefaultRuntimeConfig: *runtimeCfg,
}
err = tmpnet.BootstrapNewNetwork(
ctx,
logging.NoLog{},
network,
e2eFlags.RootNetworkDir(),
)
Expect(err).NotTo(HaveOccurred())
// Set up L1 info
l1A = getL1Info(network.Subnets[0], "L1-A")
l1B = getL1Info(network.Subnets[1], "L1-B")
})
var _ = ginkgo.AfterSuite(func() {
if network != nil {
network.Stop(context.Background())
}
})
> For runnable code, supply real genesis bytes/config for each chain (see `tests/contracts/lib/icm-contracts/lib/subnet-evm/tests/utils/tmpnet.go` in icm-services for a working helper).
// Helper to extract L1 info
func getL1Info(subnet *tmpnet.Subnet, name string) L1TestInfo {
chain := subnet.Chains[0]
rpcClient, err := ethclient.Dial(chain.Nodes[0].URI + "/ext/bc/" + chain.ChainID.String() + "/rpc")
Expect(err).NotTo(HaveOccurred())
evmChainID, err := rpcClient.ChainID(context.Background())
Expect(err).NotTo(HaveOccurred())
var nodeURIs []string
for _, node := range chain.Nodes {
nodeURIs = append(nodeURIs, node.URI)
}
return L1TestInfo{
SubnetID: subnet.SubnetID,
BlockchainID: chain.ChainID,
NodeURIs: nodeURIs,
RPCClient: rpcClient,
EVMChainID: evmChainID,
Name: name,
}
}
var _ = ginkgo.Describe("[Two L1 Tests]", func() {
ginkgo.It("should have two L1s configured", func() {
Expect(l1A.Name).To(Equal("L1-A"))
Expect(l1B.Name).To(Equal("L1-B"))
Expect(l1A.EVMChainID.Uint64()).To(Equal(uint64(12345)))
Expect(l1B.EVMChainID.Uint64()).To(Equal(uint64(54321)))
})
})Using Pre-funded Keys
Every tmpnet network has pre-funded keys for transactions:
var _ = ginkgo.Describe("[Funded Keys]", func() {
ginkgo.It("should have pre-funded key", func() {
// Get first pre-funded key
key := network.PreFundedKeys[0]
ecdsaKey := key.ToECDSA()
// Get address
address := crypto.PubkeyToAddress(ecdsaKey.PublicKey)
// Check balance on C-Chain
balance, err := l1A.RPCClient.BalanceAt(
context.Background(),
address,
nil,
)
Expect(err).NotTo(HaveOccurred())
Expect(balance.Uint64()).To(BeNumerically(">", 0))
})
})Best Practices
1. Use BeforeSuite for Expensive Setup
// Good: Share network across tests
var _ = ginkgo.BeforeSuite(func() {
network = createNetwork()
})
// Avoid: Creating network per test (very slow)
var _ = ginkgo.BeforeEach(func() {
network = createNetwork() // Don't do this!
})2. Use Appropriate Timeouts
// Network bootstrap: 5-10 minutes
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Minute)
// Individual operations: 30-60 seconds
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)3. Always Clean Up
var _ = ginkgo.AfterSuite(func() {
if network != nil {
Expect(network.Stop(context.Background())).To(Succeed())
}
})4. Use Labels for Organization
ginkgo.It("test name",
ginkgo.Label("smoke", "fast"),
func() {
// Test code
})5. Use Eventually for Async Operations
// Good: Poll with Eventually
Eventually(func() bool {
return node.IsHealthy()
}, 30*time.Second, 500*time.Millisecond).Should(BeTrue())
// Avoid: Fixed sleep
time.Sleep(10 * time.Second) // Don't do this!Debugging Tests
Enable Verbose Logging
RUN_E2E=1 ginkgo -v -trace ./...Print Network Directory
Add to your BeforeSuite:
var _ = ginkgo.BeforeSuite(func() {
// ... create network ...
ginkgo.GinkgoWriter.Printf("Network directory: %s\n", network.Dir)
})Keep Network After Failure
Manually stop the network if a test fails to inspect logs:
# Run test
RUN_E2E=1 ginkgo -v
# If test fails, network stays running
# Inspect logs in network directory
# Stop when done debugging
tmpnet stop --dir=/path/to/networkCommon Patterns
Get Node URIs
uris := network.GetNodeURIs()
for _, uri := range uris {
ginkgo.GinkgoWriter.Printf("Node: %s\n", uri)
}Check All Nodes Healthy
for _, node := range network.Nodes {
Expect(node.IsHealthy()).To(BeTrue())
}Wait for Node Health
err := node.WaitForHealthy(context.Background())
Expect(err).NotTo(HaveOccurred())Next Steps
L1 Conversion
Convert subnets to L1s with validator managers
Cross-Chain Messaging
Test Teleporter message flows between L1s
Validator Management
Test validator registration and delegation
Warp Messages
Test Warp message construction and verification
Additional Resources
Is this guide helpful?