Testing

Poktroll uses a three-tier testing strategy: unit tests for isolated module logic, integration tests with an in-memory chain for cross-module interactions, and E2E tests using Gherkin BDD scenarios on a live LocalNet. All tests must pass before a PR can merge.

Test Organization

plaintext
poktroll/
├── x/{module}/keeper/*_test.go    # Unit tests (per-module)
├── tests/integration/              # Integration tests (cross-module)
├── e2e/tests/                      # E2E tests (Gherkin + LocalNet)
│   ├── *.feature                   # Gherkin feature files
│   └── steps/                      # Step implementations
└── testutil/                       # Shared test utilities
    ├── sample/                     # Test data generation
    ├── keeper/                     # Test keeper constructors
    ├── network/                    # Test network setup
    └── mocks/                      # Generated mocks

Running Tests

bash
# Everything
make test_all

# By tier
make test_unit                          # Unit tests only
make test_integration                   # Integration tests
make test_e2e                           # E2E (requires LocalNet running)

# Module-specific
go test ./x/proof/...                   # All proof module tests
go test ./x/proof/keeper/... -run TestMsgServer_CreateClaim  # Specific test

# With coverage
go test ./x/proof/... -cover -coverprofile=coverage.out
go tool cover -html=coverage.out        # Opens browser with coverage map

Unit Tests

Unit tests live alongside the code they test in x/{module}/keeper/*_test.go. They use table-driven patterns with mocked dependencies — the keeper under test is real, but its dependencies (bank keeper, other module keepers) are mocked.

Table-Driven Pattern

This is the standard pattern for all message handler tests:

go
package keeper_test

func TestMsgServer_CreateClaim(t *testing.T) {
    tests := []struct {
        name        string
        setup       func(k *keeper.Keeper, ctx sdk.Context)
        msg         *types.MsgCreateClaim
        expectedErr error
        validate    func(t *testing.T, k *keeper.Keeper, ctx sdk.Context)
    }{
        {
            name: "success - valid claim",
            setup: func(k *keeper.Keeper, ctx sdk.Context) {
                // Pre-populate state needed for this test case
            },
            msg: &types.MsgCreateClaim{
                SupplierOperatorAddress: sample.AccAddress(),
                SessionHeader:          validSessionHeader,
                RootHash:               validRootHash,
            },
            expectedErr: nil,
            validate: func(t *testing.T, k *keeper.Keeper, ctx sdk.Context) {
                // Verify the claim was stored correctly
            },
        },
        {
            name: "failure - invalid supplier address",
            msg: &types.MsgCreateClaim{
                SupplierOperatorAddress: "invalid",
            },
            expectedErr: types.ErrInvalidSupplier,
        },
    }

    for _, tt := range tests {
        t.Run(tt.name, func(t *testing.T) {
            k, ctx := keepertest.ProofKeeper(t)
            msgServer := keeper.NewMsgServerImpl(k)

            if tt.setup != nil {
                tt.setup(k, ctx)
            }

            _, err := msgServer.CreateClaim(ctx, tt.msg)

            if tt.expectedErr != nil {
                require.ErrorIs(t, err, tt.expectedErr)
            } else {
                require.NoError(t, err)
                if tt.validate != nil {
                    tt.validate(t, k, ctx)
                }
            }
        })
    }
}

Every test case defines its own setup, input, expected error, and post-condition validation. This makes tests self-contained and easy to extend — adding a new test case is just adding another struct to the slice.

Test Utilities

Sample data generation (testutil/sample/):

go
import "github.com/pokt-network/poktroll/testutil/sample"

appAddr := sample.AccAddress()
stake := sample.Coin("upokt", 1000000)
sessionHeader := sample.SessionHeader(appAddr, serviceId, blockHeight)

Test keeper constructors (testutil/keeper/):

go
import keepertest "github.com/pokt-network/poktroll/testutil/keeper"

// Creates a real keeper with mocked dependencies
k, ctx := keepertest.ProofKeeper(t)

Custom mocks (when you need specific mock behavior):

go
ctrl := gomock.NewController(t)
bankMock := mocks.NewMockBankKeeper(ctrl)

bankMock.EXPECT().
    SendCoinsFromAccountToModule(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).
    Return(nil)

k, ctx := keepertest.ProofKeeperWithMocks(t, bankMock)

Context Manipulation

Tests can control block height, time, and other context fields:

go
ctx = ctx.WithBlockHeight(100)
ctx = ctx.WithBlockTime(time.Now())

Testing Events

Verify that state-changing operations emit the correct events:

go
_, err := msgServer.CreateClaim(ctx, validMsg)
require.NoError(t, err)

events := ctx.EventManager().Events()
require.Len(t, events, 1)
require.Equal(t, types.EventTypeClaimCreated, events[0].Type)

Testing Queries

go
// Setup: store a claim
k.SetClaim(ctx, claim)

// Query
resp, err := k.Claim(ctx, &types.QueryGetClaimRequest{
    SessionId:              sessionHeader.SessionId,
    SupplierOperatorAddress: supplierAddr,
})

require.NoError(t, err)
require.Equal(t, claim, resp.Claim)

Integration Tests

Integration tests use an in-memory test network to test cross-module interactions with real keeper wiring. They’re slower than unit tests but catch interaction bugs that mocks miss.

Suite Pattern

Integration tests use testify/suite for setup and teardown:

go
package proof_test

import (
    "testing"
    "github.com/stretchr/testify/suite"
    "github.com/pokt-network/poktroll/tests/integration"
)

type ClaimIntegrationSuite struct {
    integration.BaseSuite
}

func TestClaimIntegration(t *testing.T) {
    suite.Run(t, new(ClaimIntegrationSuite))
}

func (s *ClaimIntegrationSuite) TestClaimSubmission() {
    // 1. Stake an application
    s.StakeApplication(s.AppAddr, "1000000upokt", "eth")

    // 2. Stake a supplier
    s.StakeSupplier(s.SupplierAddr, "1000000upokt", "eth")

    // 3. Get session
    session := s.GetSession(s.AppAddr, "eth", s.CurrentHeight())

    // 4. Submit claim
    s.SubmitClaim(s.SupplierAddr, session, rootHash)

    // 5. Verify
    claim := s.GetClaim(session.SessionId, s.SupplierAddr)
    s.Require().NotNil(claim)
}

Test Network

The BaseSuite creates an in-memory test network with real module wiring:

go
func (s *BaseSuite) SetupSuite() {
    s.Network = testnetwork.New(s.T(), testnetwork.DefaultConfig())
    s.Network.WaitForNextBlock()
}

func (s *BaseSuite) TearDownSuite() {
    s.Network.Cleanup()
}
Info

Integration tests are ideal for testing the full claim/proof/settlement lifecycle, cross-module parameter interactions, and delegation flows that span multiple modules.

When to Use Integration vs Unit Tests

ScenarioUnit TestIntegration Test
Single message handler logic✅
Query response shape✅
Error codes for invalid input✅
Claim → Proof → Settlement flow✅
Session hydration with real stakers✅
Parameter change affecting multiple modules✅

E2E Tests (Gherkin BDD)

E2E tests run against a live LocalNet using Gherkin (Behavior-Driven Development) syntax. They validate complete user flows from the perspective of external actors.

Feature Files

Feature files describe scenarios in human-readable language:

gherkin
# e2e/tests/proof.feature
Feature: Proof Submission

  Scenario: Supplier submits valid proof
    Given an application "app1" staked for service "eth"
    And a supplier "supplier1" staked for service "eth"
    And a valid session exists for "app1" and "supplier1"
    When "supplier1" services 100 relays
    And the claim window opens
    And "supplier1" submits a claim
    And the proof window opens
    And "supplier1" submits a proof
    Then the proof should be accepted
    And "supplier1" should receive rewards

  Scenario: Supplier submits proof outside window
    Given an application "app1" staked for service "eth"
    And a supplier "supplier1" staked for service "eth"
    And a valid session exists for "app1" and "supplier1"
    When "supplier1" submits a proof before the window opens
    Then the proof should be rejected with "proof window not open"

Step Implementations

Each Gherkin step maps to a Go function:

go
// e2e/tests/steps/proof_steps.go
func (s *Suite) supplierSubmitsProof(supplierName string) error {
    supplier := s.GetActor(supplierName)
    session := s.CurrentSession

    proof := s.BuildProof(supplier, session)

    resp, err := s.BroadcastTx(supplier, &prooftypes.MsgSubmitProof{
        SupplierOperatorAddress: supplier.Address,
        SessionHeader:          session.Header,
        Proof:                  proof,
    })

    s.LastTxResponse = resp
    s.LastError = err
    return nil
}

Running E2E Tests

E2E tests require a running LocalNet:

bash
# Start LocalNet first
make localnet_up

# Wait for it to stabilize, then run E2E tests
make test_e2e
Warning

E2E tests are slow (they wait for real block production) and can be flaky due to timing sensitivity. If a test intermittently fails, check whether it’s a timing issue with claim/proof windows before debugging the logic.

Writing New E2E Tests

  1. Create or extend a .feature file in e2e/tests/
  2. Run the test — unimplemented steps will show as “pending”
  3. Implement step definitions in e2e/tests/steps/
  4. Use the suite’s helper methods for staking, querying, and broadcasting transactions

Mocking Guidelines

When to Mock

  • External service dependencies
  • Bank keeper for token operations (unit tests only)
  • Other module keepers (unit tests only)
  • Time-sensitive operations

When NOT to Mock

  • The module under test (test the real code)
  • Simple data structures
  • Pure functions

Generating Mocks

bash
mockgen -source=x/proof/types/expected_keepers.go \
        -destination=testutil/mocks/proof_mocks.go \
        -package=mocks

Debugging Relay Flows

When a relay-related test fails, follow this trace before examining code:

markdown
**Observed:** [exact symptom]

**Tracing relay flow:**
- Session: [session_id, app, service, height range]
- Suppliers in session: [from session hydration]
- Relay path: [gateway → relayminer → backend]
- Claim submission: [height, root hash, compute units]
- Proof submission: [height, closest merkle proof]
- Settlement: [expected mint/burn amounts]

**Possible root causes:**
1. [specific hypothesis]
2. [specific hypothesis]

**Investigating:** [which hypothesis first and why]
Danger

Do not propose code fixes without completing the relay flow trace. Symptom-patching without understanding the root cause leads to fragile tests and hidden bugs.

CI Integration

All tests run on every PR via GitHub Actions:

  1. make go_lint — code quality
  2. buf lint + buf breaking — proto validation
  3. make test_unit — unit tests
  4. make test_integration — integration suite
  5. make test_e2e — E2E with fresh LocalNet

Failed tests block PR merge. There are no exceptions.