Smart Contract Testing | Algorand Developer Portal

Smart Contract Testing

This guide provides an overview of how to test smart contracts using the Algorand Python SDK (algopy). We will cover the basics of testing ARC4Contract and Contract classes, focusing on abimethod and baremethod decorators.

The code snippets showcasing the contract testing capabilities are using [pytest](https://docs.pytest.org/en/latest/) as the test framework. However, note that the `algorand-python-testing` package can be used with any other test framework that supports Python. `pytest` is used for demonstration purposes in this documentation.
import algopy

import algopy_testing

from algopy_testing import algopy_testing_context

# Create the context manager for snippets below

ctx_manager = algopy_testing_context()

# Enter the context

context = ctx_manager.__enter__()

The following video includes a practical tutorial of how to do tests in Algorand Smart Contracts:

Smart Contract Testing - Python

algopy.ARC4Contract

Subclasses of algopy.ARC4Contract are required to be instantiated with an active test context. As part of instantiation, the test context will automatically create a matching algopy.Application object instance.

Within the class implementation, methods decorated with algopy.arc4.abimethod and algopy.arc4.baremethod will automatically assemble an algopy.gtxn.ApplicationCallTransaction transaction to emulate the AVM application call. This behavior can be overridden by setting the transaction group manually as part of test setup, this is done via implicit invocation of algopy_testing.context.any_application() value generator (refer to APIs for more details).

class SimpleVotingContract(algopy.ARC4Contract):

def __init__(self) -> None:

self.topic = algopy.GlobalState(algopy.Bytes(b"default_topic"), key="topic", description="Voting topic")

self.votes = algopy.GlobalState(

algopy.UInt64(0),

key="votes",

description="Votes for the option",

)

self.voted = algopy.LocalState(algopy.UInt64, key="voted", description="Tracks if an account has voted")

@algopy.arc4.abimethod(create="require")

def create(self, initial_topic: algopy.Bytes) -> None:

self.topic.value = initial_topic

self.votes.value = algopy.UInt64(0)

@algopy.arc4.abimethod

def vote(self) -> algopy.UInt64:

assert self.voted[algopy.Txn.sender] == algopy.UInt64(0), "Account has already voted"

self.votes.value += algopy.UInt64(1)

self.voted[algopy.Txn.sender] = algopy.UInt64(1)

return self.votes.value

@algopy.arc4.abimethod(readonly=True)

def get_votes(self) -> algopy.UInt64:

return self.votes.value

@algopy.arc4.abimethod

def change_topic(self, new_topic: algopy.Bytes) -> None:

assert algopy.Txn.sender == algopy.Txn.application_id.creator, "Only creator can change topic"

self.topic.value = new_topic

self.votes.value = algopy.UInt64(0)

# Reset user's vote (this is simplified per single user for the sake of example)

self.voted[algopy.Txn.sender] = algopy.UInt64(0)

# Arrange

initial_topic = algopy.Bytes(b"initial_topic")
contract = SimpleVotingContract()
contract.voted[context.default_sender] = algopy.UInt64(0)

# Act - Create the contract

contract.create(initial_topic)

# Assert - Check initial state

assert contract.topic.value == initial_topic
assert contract.votes.value == algopy.UInt64(0)

# Act - Vote

# The method `.vote()` is decorated with `algopy.arc4.abimethod`, which means it will assemble a transaction to emulate the AVM application call

result = contract.vote()

# Assert - you can access the corresponding auto generated application call transaction via test context

assert len(context.txn.last_group.txns) == 1

# Assert - Note how local and global state are accessed via regular python instance attributes

assert result == algopy.UInt64(1)
assert contract.votes.value == algopy.UInt64(1)
assert contract.voted[context.default_sender] == algopy.UInt64(1)

# Act - Change topic

new_topic = algopy.Bytes(b"new_topic")
contract.change_topic(new_topic)

# Assert - Check topic changed and votes reset

assert contract.topic.value == new_topic
assert contract.votes.value == algopy.UInt64(0)
assert contract.voted[context.default_sender] == algopy.UInt64(0)

# Act - Get votes (should be 0 after reset)

votes = contract.get_votes()

# Assert - Check votes

assert votes == algopy.UInt64(0)

For more examples of tests using algopy.ARC4Contract, see the examples section.

algopy.Contract

Subclasses of algopy.Contract are required to be instantiated with an active test context. As part of instantiation, the test context will automatically create a matching algopy.Application object instance. This behavior is identical to algopy.ARC4Contract class instances.

Unlike algopy.ARC4Contract, algopy.Contract requires manual setup of the transaction context and explicit method calls. Alternatively, you can use active_txn_overrides to specify application arguments and foreign arrays without needing to create a full transaction group if your aim is to patch a specific active transaction related metadata.

Here’s an updated example demonstrating how to test a Contract class:

import algopy

import pytest

from algopy_testing import AlgopyTestContext, algopy_testing_context

class CounterContract(algopy.Contract):

def __init__(self):

self.counter = algopy.UInt64(0)

@algopy.subroutine

def increment(self):

self.counter += algopy.UInt64(1)

return algopy.UInt64(1)

@algopy.arc4.baremethod

def approval_program(self):

return self.increment()

@algopy.arc4.baremethod

def clear_state_program(self):

return algopy.UInt64(1)

@pytest.fixture()

def context():

with algopy_testing_context() as ctx:

yield ctx

def test_counter_contract(context: AlgopyTestContext):

# Instantiate contract

contract = CounterContract()

# Set up the transaction context using active_txn_overrides

with context.txn.create_group(

active_txn_overrides={

"sender": context.default_sender,

"app_args": [algopy.Bytes(b"increment")],

}

):

# Invoke approval program

result = contract.approval_program()

# Assert approval program result

assert result == algopy.UInt64(1)

# Assert counter value

assert contract.counter == algopy.UInt64(1)

# Test clear state program

assert contract.clear_state_program() == algopy.UInt64(1)

def test_counter_contract_multiple_txns(context: AlgopyTestContext):

contract = CounterContract()

# For scenarios with multiple transactions, you can still use gtxns

extra_payment = context.any.txn.payment()

with context.txn.create_group(

gtxns=[

extra_payment,

context.any.txn.application_call(

sender=context.default_sender,

app_id=contract.app_id,

app_args=[algopy.Bytes(b"increment")],

),

],

active_txn_index=1  # Set the application call as the active transaction

):

result = contract.approval_program()

assert result == algopy.UInt64(1)

assert contract.counter == algopy.UInt64(1)

assert len(context.txn.last_group.txns) == 2

In this updated example:

  1. We use context.txn.create_group() with active_txn_overrides to set up the transaction context for a single application call. This simplifies the process when you don’t need to specify a full transaction group.

  2. The active_txn_overrides parameter allows you to specify app_args and other transaction fields directly, without creating a full ApplicationCallTransaction object.

  3. For scenarios involving multiple transactions, you can still use the gtxns parameter to create a transaction group, as shown in the test_counter_contract_multiple_txns function.

  4. The app_id is automatically set to the contract’s application ID, so you don’t need to specify it explicitly when using active_txn_overrides.

This approach provides more flexibility in setting up the transaction context for testing Contract classes, allowing for both simple single-transaction scenarios and more complex multi-transaction tests.

Defer contract method invocation

You can create deferred application calls for more complex testing scenarios where order of transactions needs to be controlled:

def test_deferred_call(context):

contract = MyARC4Contract()

extra_payment = context.any.txn.payment()

extra_asset_transfer = context.any.txn.asset_transfer()

implicit_payment = context.any.txn.payment()

deferred_call = context.txn.defer_app_call(contract.some_method, implicit_payment)

with context.txn.create_group([extra_payment, deferred_call, extra_asset_transfer]):

result = deferred_call.submit()

print(context.txn.last_group) # [extra_payment, implicit_payment, app call, extra_asset_transfer]

A deferred application call prepares the application call transaction without immediately executing it. The call can be executed later by invoking the .submit() method on the deferred application call instance. As demonstrated in the example, you can also include the deferred call in a transaction group creation context manager to execute it as part of a larger transaction group. When .submit() is called, only the specific method passed to defer_app_call() will be executed.