How to Handle Contract Errors with expect_revert in GenLayer Tests
Use direct_vm.expect_revert("<message>") as a context manager to assert that contract calls fail with specific error messages in GenLayer Direct-Mode tests.
GenLayer's Direct-Mode test harness provides first-class support for negative testing through the expect_revert helper. When writing tests for the genlayerlabs/genlayer-project-boilerplate repository, you can verify that your contracts correctly enforce business rules by asserting that specific method calls raise expected errors. This approach validates defensive programming patterns directly without deploying to a live network.
How expect_revert Works in Direct-Mode
In GenLayer's Direct-Mode testing framework, contracts signal failures by raising plain Python Exception objects or gl.vm.UserError instances. When a contract method raises an exception, the VM catches it and returns it as a transaction revert. The direct_vm.expect_revert context manager wraps your contract call and verifies both that a revert occurs and that the error message matches the expected string exactly.
According to the source code in tests/direct/test_create_bet.py, the comparison is case-sensitive and must be identical to the message raised by the contract. The helper records the revert, compares the message with the supplied string, and the test passes only if they match. If the method does not revert or the message differs, the test fails immediately.
Testing Common Contract Error Conditions
The contracts/football_bets.py contract demonstrates three defensive error patterns that you can validate using expect_revert. Each guard clause raises a plain Exception with a descriptive message when business rules are violated.
Preventing Duplicate Entries
Contracts often need to enforce uniqueness constraints. In contracts/football_bets.py, the create_bet method raises "Bet already created" when attempting to create a duplicate bet identifier.
The test in tests/direct/test_create_bet.py validates this protection:
def test_create_duplicate_bet_fails(direct_vm, direct_deploy, direct_alice):
contract = direct_deploy("contracts/football_bets.py")
direct_vm.sender = direct_alice
contract.create_bet("2024-06-20", "Spain", "Italy", "1")
# The second creation should revert with "Bet already created"
with direct_vm.expect_revert("Bet already created"):
contract.create_bet("2024-06-20", "Spain", "Italy", "2")
Blocking Invalid State Transitions
State machines require protection against invalid transitions. The resolve_bet method raises "Bet already resolved" when attempting to resolve a bet that has already been finalized.
The corresponding test in tests/direct/test_resolve_bet.py establishes the valid state first, then asserts the failure:
def test_resolve_already_resolved_fails(direct_vm, direct_deploy, direct_alice):
contract = direct_deploy("contracts/football_bets.py")
direct_vm.sender = direct_alice
contract.create_bet("2024-06-20", "Spain", "Italy", "1")
_setup_match_mocks(direct_vm, "1:0", 1)
contract.resolve_bet("2024-06-20_spain_italy") # first resolution succeeds
# Second resolution must revert with "Bet already resolved"
with direct_vm.expect_revert("Bet already resolved"):
contract.resolve_bet("2024-06-20_spain_italy")
Validating External Preconditions
Contracts frequently depend on external data conditions. The resolve_bet method raises "Game not finished" when attempting to resolve a bet for a match that has not yet completed, as implemented in contracts/football_bets.py.
The test verifies this guard by mocking an unfinished game state:
def test_resolve_unfinished_game_fails(direct_vm, direct_deploy, direct_alice):
contract = direct_deploy("contracts/football_bets.py")
direct_vm.sender = direct_alice
contract.create_bet("2024-06-20", "Spain", "Italy", "1")
_setup_match_mocks(direct_vm, "-", -1) # mock a match that is not finished
# Resolve should revert with "Game not finished"
with direct_vm.expect_revert("Game not finished"):
contract.resolve_bet("2024-06-20_spain_italy")
Best Practices for Negative Testing
When implementing error handling with expect_revert in the genlayer-project-boilerplate test suite, follow these guidelines to ensure reliable test coverage:
-
Raise plain exceptions in contract code. GenLayer's linter forbids custom exception hierarchies; a simple
raise Exception("message")works correctly and is captured by the VM. -
Match the exact string in
expect_revert. The comparison is case-sensitive and must be identical to the message raised by the contract, including whitespace and punctuation. -
Scope the VM sender before the call. The revert check uses
gl.message.sender_addressinternally, so you must setdirect_vm.senderto the appropriate test account before entering theexpect_revertblock. -
Isolate negative paths into dedicated test functions. Keep positive-logic tests separate from negative assertions to maintain clarity and ensure that each failure mode is explicitly documented, as demonstrated in
tests/direct/test_create_bet.pyandtests/direct/test_resolve_bet.py.
Summary
- Use
direct_vm.expect_revert("message")as a context manager to assert specific contract failures in GenLayer Direct-Mode tests. - Contract errors must raise plain
Exceptionobjects; custom exception hierarchies are not supported by the linter. - The error message passed to
expect_revertmust match the contract's raised exception string exactly, including case sensitivity. - Always set
direct_vm.senderbefore testing reverts, as the VM relies ongl.message.sender_addressfor transaction context. - Reference implementations are available in
tests/direct/test_create_bet.pyandtests/direct/test_resolve_bet.pywithin thegenlayer-project-boilerplaterepository.
Frequently Asked Questions
What exception type should I raise in my GenLayer contract to trigger expect_revert?
Raise a plain Python Exception with a descriptive string message. According to the GenLayer linter rules used in genlayer-project-boilerplate, custom exception hierarchies are forbidden, so raise Exception("Bet already created") is the correct pattern. The VM catches these standard exceptions and converts them into reverts that expect_revert can detect.
Does the message in expect_revert need to match exactly?
Yes, the comparison requires an exact string match. The expect_revert helper performs a case-sensitive comparison between the string you provide and the exception message raised by the contract. If the message differs by even a single character, the test will fail with a mismatch error.
Why must I configure direct_vm.sender before using expect_revert?
The Direct-Mode VM uses gl.message.sender_address to contextualize the transaction sender when executing contract code. Since expect_revert executes the contract call within the VM to capture the exception, you must explicitly set direct_vm.sender to the appropriate test account (such as direct_alice) before entering the context manager block.
Can I use custom exception classes from Python's standard library?
No. While Python allows importing exceptions like ValueError or RuntimeError, GenLayer's testing infrastructure and linter specifically expect plain Exception instances for contract-level errors. The direct_vm.expect_revert helper is optimized to catch these base exceptions as raised by the contract bytecode execution environment in contracts/football_bets.py.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →