Testing

Graphite subgraphs can be tested with cargo test — no Docker, no PostgreSQL, no graph-node. Tests run natively using an in-process mock store.

Basic Setup

cargo test

Tests live in the same src/lib.rs file alongside your handlers (or in a tests/ directory). The graphite::mock module provides the in-memory store.

The Mock API

mock::reset()

Clears the entire in-memory store. Always call this at the start of each test to prevent state leaking between tests.

#![allow(unused)]
fn main() {
#[test]
fn my_test() {
    mock::reset();
    // ...
}
}

mock::has_entity(type, id)

Returns true if an entity with the given type name and ID exists in the store.

#![allow(unused)]
fn main() {
assert!(mock::has_entity("Transfer", "0xabc-00"));
}

mock::entity_count(type)

Returns the number of entities of a given type.

#![allow(unused)]
fn main() {
assert_eq!(mock::entity_count("Transfer"), 1);
}

mock::assert_entity(type, id)

Returns an assertion builder for inspecting a specific entity's field values.

#![allow(unused)]
fn main() {
mock::assert_entity("Transfer", &id)
    .field_bytes("from", &[0xaa; 20])
    .field_bytes("to", &[0xbb; 20])
    .field_exists("value")
    .field_exists("blockNumber");
}
MethodDescription
.field_exists(name)Asserts the field is set
.field_bytes(name, expected)Asserts a Bytes/Address field equals the given bytes
.field_string(name, expected)Asserts a String field equals the given value
.field_bool(name, expected)Asserts a Boolean field

mock::set_call_result(result)

Mocks the return value of an ethereum.call. See Contract Calls.

mock::set_current_address(address)

Sets the value returned by data_source::address_current(). Useful when testing template handlers:

#![allow(unused)]
fn main() {
mock::set_current_address([0xAA; 20]);
handle_swap_impl(&event, &graphite::EventContext::default());
}

mock::assert_contract_data_source_created(template, address)

Asserts that data_source::create_contract was called with the given template name and address:

#![allow(unused)]
fn main() {
mock::assert_contract_data_source_created("Pair", pair_address);
}

Writing a Test

#![allow(unused)]
fn main() {
#[cfg(test)]
mod tests {
    use super::*;
    use graph_as_runtime::ethereum::{EthereumValue, EventParam, FromRawEvent, RawEthereumEvent};
    use graphite::mock;

    fn mock_transfer() -> RawEthereumEvent {
        RawEthereumEvent {
            tx_hash: [0xab; 32],
            log_index: alloc::vec![0],
            block_number: alloc::vec![1, 0, 0, 0],
            block_timestamp: alloc::vec![100, 0, 0, 0],
            params: alloc::vec![
                EventParam { name: "from".into(),  value: EthereumValue::Address([0xaa; 20]) },
                EventParam { name: "to".into(),    value: EthereumValue::Address([0xbb; 20]) },
                EventParam { name: "value".into(), value: EthereumValue::Uint(alloc::vec![100]) },
            ],
            ..Default::default()
        }
    }

    #[test]
    fn transfer_creates_entity() {
        mock::reset();

        let raw = mock_transfer();
        let event = ERC20TransferEvent::from_raw_event(&raw).unwrap();
        handle_transfer_impl(&event, &graphite::EventContext::default());

        let tx_hex = "ab".repeat(32);
        let id = format!("{}-00", tx_hex);

        assert!(mock::has_entity("Transfer", &id));
        mock::assert_entity("Transfer", &id)
            .field_bytes("from", &[0xaa; 20])
            .field_bytes("to", &[0xbb; 20])
            .field_exists("value");
    }

    #[test]
    fn upsert_does_not_duplicate() {
        mock::reset();

        let raw = mock_transfer();
        let event = ERC20TransferEvent::from_raw_event(&raw).unwrap();

        handle_transfer_impl(&event, &graphite::EventContext::default());
        handle_transfer_impl(&event, &graphite::EventContext::default()); // same id

        assert_eq!(mock::entity_count("Transfer"), 1);
    }
}
}

Constructing Mock Events

RawEthereumEvent has a Default implementation — you only need to set the fields your handler uses.

#![allow(unused)]
fn main() {
RawEthereumEvent {
    tx_hash: [0xab; 32],
    log_index: alloc::vec![0],
    block_number: alloc::vec![1, 0, 0, 0],  // little-endian: block 1
    block_timestamp: alloc::vec![100, 0, 0, 0],
    params: alloc::vec![
        EventParam { name: "from".into(), value: EthereumValue::Address([0xaa; 20]) },
        EventParam { name: "value".into(), value: EthereumValue::Uint(alloc::vec![100]) },
    ],
    ..Default::default()
}
}

EthereumValue Variants

VariantSolidity type
EthereumValue::Address([u8; 20])address
EthereumValue::Uint(Vec<u8>)uint256, uint128, etc. (LE bytes)
EthereumValue::Int(Vec<u8>)int256, int128, etc. (LE signed bytes)
EthereumValue::Bool(bool)bool
EthereumValue::String(String)string
EthereumValue::Bytes(Vec<u8>)bytes
EthereumValue::FixedBytes([u8; N])bytesN
EthereumValue::Array(Vec<EthereumValue>)array types