Test Fixtures

Generated Artifacts Layer — JSON test vectors from executable specification


Definition

Test fixtures are structured test vectors generated directly from the executable specification. They define input states, operations, and expected outputs in a format that any implementation can consume to verify conformance.

Role in the Framework

Test fixtures are the enforcement mechanism for the executable specification. They translate “the spec says this” into “prove your implementation does this.”

Executable Specification
    ↓
Test Fixtures ← generated from spec execution
    ↓
├── Client A: runs fixtures → pass/fail
├── Client B: runs fixtures → pass/fail
└── Client C: runs fixtures → pass/fail

Why Generated, Not Written

Test fixtures are generated from the executable specification, not hand-written:

Generated FixturesHand-Written Tests
Guaranteed to match specMay diverge from spec
Comprehensive coverageMay miss edge cases
Single source of truthMultiple sources that may conflict
Automatically updatedRequire manual maintenance

When the specification changes, fixtures regenerate automatically—ensuring tests always reflect current requirements.

Fixture Structure

Ethereum’s execution-spec-tests use a standardized structure:

{
  "test_name": {
    "description": "Validates SPEC-4.2.3: Transaction nonce verification",
    "pre": {
      "0x1234...": {
        "balance": "0x100000",
        "nonce": "0x5",
        "code": "0x",
        "storage": {}
      }
    },
    "transaction": {
      "sender": "0x1234...",
      "to": "0x5678...",
      "nonce": "0x5",
      "value": "0x1000",
      "gasLimit": "0x5208",
      "gasPrice": "0x1"
    },
    "post": {
      "0x1234...": {
        "balance": "0xfed78",
        "nonce": "0x6"
      },
      "0x5678...": {
        "balance": "0x1000"
      }
    },
    "postStateRoot": "0xabcd..."
  }
}

Generation Process

Fixtures are generated by running the executable specification:

def generate_state_transition_fixtures() -> List[Fixture]:
    """
    Generate state transition test fixtures from executable spec.
    """
    fixtures = []
    
    for scenario in load_test_scenarios():
        # Set up initial state per scenario
        pre_state = scenario.create_pre_state()
        
        # Execute using THE SPECIFICATION
        post_state = execute_block(pre_state, scenario.block)
        
        # Capture the fixture
        fixture = Fixture(
            name=scenario.name,
            spec_reference=scenario.spec_section,
            pre=serialize_state(pre_state),
            block=serialize_block(scenario.block),
            post=serialize_state(post_state),
            post_state_root=compute_state_root(post_state)
        )
        fixtures.append(fixture)
    
    return fixtures
 
def generate_vm_fixtures() -> List[Fixture]:
    """
    Generate EVM opcode test fixtures.
    """
    fixtures = []
    
    for opcode in ALL_OPCODES:
        for test_case in opcode_test_cases(opcode):
            # Set up EVM state
            evm = create_evm(
                code=test_case.code,
                stack=test_case.initial_stack,
                memory=test_case.initial_memory,
                gas=test_case.initial_gas
            )
            
            # Execute using THE SPECIFICATION
            execute_opcode(evm, opcode)
            
            fixture = Fixture(
                name=f"{opcode.name}_{test_case.name}",
                spec_reference=f"EVM-{opcode.value}",
                pre=serialize_evm_state(test_case),
                post={
                    "stack": evm.stack,
                    "memory": evm.memory.hex(),
                    "gas_remaining": evm.gas,
                    "pc": evm.pc
                }
            )
            fixtures.append(fixture)
    
    return fixtures

Fixture Categories

State Transition Tests

Verify block processing:

  • Pre-state + Block → Post-state
  • State root verification
  • Receipt verification

Transaction Tests

Verify transaction handling:

  • Valid transaction acceptance
  • Invalid transaction rejection (various reasons)
  • Gas calculation

VM Tests

Verify EVM operations:

  • Each opcode’s behavior
  • Stack operations
  • Memory operations
  • Gas consumption

Difficulty/Consensus Tests

Verify consensus rules:

  • Block header validation
  • Fork choice rules
  • Finality conditions

Fixture Format Details

{
  "_meta": {
    "generated_by": "execution-spec-tests",
    "spec_version": "cancun",
    "generated_at": "2024-03-15T10:30:00Z"
  },
  
  "state_transition_001": {
    "spec_reference": "FUNC-4.2.3",
    "description": "Simple value transfer",
    
    "env": {
      "currentNumber": "0x1",
      "currentTimestamp": "0x1234",
      "currentGasLimit": "0x1000000"
    },
    
    "pre": {
      "0xsender": {
        "balance": "0x10000",
        "nonce": "0x0",
        "code": "0x",
        "storage": {}
      }
    },
    
    "transaction": {
      "type": "0x2",
      "chainId": "0x1",
      "nonce": "0x0",
      "to": "0xrecipient",
      "value": "0x1000",
      "maxFeePerGas": "0x10",
      "maxPriorityFeePerGas": "0x1",
      "gasLimit": "0x5208",
      "data": "0x"
    },
    
    "expect": {
      "result": "valid",
      "post": {
        "0xsender": {
          "balance": "0xed98",
          "nonce": "0x1"
        },
        "0xrecipient": {
          "balance": "0x1000"
        }
      },
      "gasUsed": "0x5208",
      "stateRoot": "0x..."
    }
  }
}

Client Integration

Clients consume fixtures to verify conformance:

// Geth test runner
func TestStateTransitions(t *testing.T) {
    fixtures := LoadFixtures("state_transition_tests.json")
    
    for _, fixture := range fixtures {
        t.Run(fixture.Name, func(t *testing.T) {
            // Build pre-state
            state := BuildState(fixture.Pre)
            
            // Apply transaction
            result, err := ApplyTransaction(state, fixture.Transaction)
            
            // Verify against fixture
            if fixture.Expect.Result == "valid" {
                require.NoError(t, err)
                require.Equal(t, fixture.Expect.StateRoot, state.Root())
                require.Equal(t, fixture.Expect.GasUsed, result.GasUsed)
            } else {
                require.Error(t, err)
            }
        })
    }
}

Downstream Dependencies

DownstreamRelationship
Client ImplementationsMust pass all fixtures
Conformance TestsExecute fixtures against clients
Compliance MatrixTrack fixture pass rates
Audit TrailsRecord fixture test results

Fixture Maintenance

When specifications change:

1. Specification updated (prose or executable)
           ↓
2. Fixture generation re-run
           ↓
3. New/changed fixtures identified
           ↓
4. Clients updated to pass new fixtures
           ↓
5. Old fixtures deprecated/removed as appropriate

Quality Criteria

  • Generated: From executable spec, not hand-written
  • Comprehensive: Cover all specified behaviors
  • Isolated: Each fixture tests one thing
  • Reproducible: Same inputs always give same fixtures
  • Documented: Include spec references and descriptions
  • Versioned: Track which spec version generated them

Best Practices

  • Never edit generated fixtures manually
  • Include spec references in fixture metadata
  • Generate fixtures for positive AND negative cases
  • Version fixtures with specification version
  • Provide fixture validation tools for clients
  • Document fixture format comprehensively