Call for Research Proposals — up to $50,000. Deadline June 1, 2026.Apply now
Guides

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/e2e

Basic Test Suite Structure

1. Create Test Suite File

Create a test file with the standard Ginkgo setup:

my_test.go
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 reuse
  • RUN_E2E check: Gates tests so they only run when explicitly requested
  • RegisterFailHandler: Integrates Gomega assertions with Ginkgo
  • RunSpecs: 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:

  • BeforeSuite runs once before all tests in the suite
  • AfterSuite ensures cleanup even if tests fail
  • Use generous timeouts for network bootstrap (5+ minutes)
  • e2eFlags enables 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.123456

Subsequent 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-network

Stop Existing Networks

tmpnetctl stop-network --network-dir=/home/user/.tmpnet/networks/20250312-143052.123456

Testing with Multiple L1s

Most tests need multiple L1s (chains) for cross-chain scenarios. Here's the standard two-L1 setup:

Complete Example

two_l1s_test.go
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 ./...

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/network

Common 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

Additional Resources

Is this guide helpful?