Michelson integration tests

Step by step guide

Read this Medium article: https://medium.com/tezoscommons/testing-michelson-contracts-with-pytezos-513718499e93

Loading contract

First step in testing contracts is to create pytezos.contract.interface.ContractInterface from Michelson code, .tz file or existing deployed contract.

from os.path import dirname, join
from pytezos import ContractInterface, pytezos

contract_michelson = """
  parameter string;
  storage string;
  code { DUP;
        DIP { CAR ; NIL string ; SWAP ; CONS } ;
        CDR ; CONS ;
        CONCAT ;
        NIL operation; PAIR }
"""

# From blockchain
contract = pytezos.contract('KT1...')

# From michelson
contract = ContractInterface.from_michelson(contract_michelson)

# From file
with open('my_contract.tz', 'w+') as file:
  file.write(contract_michelson)
contract = ContractInterface.from_file('my_contract.tz')

# From URL
contract = ContractInterface.from_url('https://raw.githubusercontent.com/atomex-me/atomex-michelson/master/src/atomex.tz')

Calling contract entrypoints

Entrypoints are accessed by name of contract attributes. Call them with parameters to create pytezos.contract.call.ContractCall wrapper.

# Create call to `default` entrypoint
call = contract.default("foo")

# Add amount to transaction
call = call.with_amount('0.01')

Now you can call inject method to send operation to blockchain. But we’re interested in simulating contract calls without interacting with public blockchain. There are several methods to do this:

  • Use remote node interpreter

  • Use built-in Michelson interpreter (result may vary from node interpreter)

  • Interact with real contract deployed on sandboxed node

Let’s talk about the first two methods.

# Node interpreter
result = call.run_code(storage="foo")

# Built-in interpreter
result = call.interpret(storage="foo")

# In both cases you could patch SENDER, AMOUNT and other context variables
result = call.interpret(storage="foo", balance=1000)

Each method will return pytezos.contract.result.ContractCallResult instance. Now let’s ensure our call has modified storage correctly.

assert result.storage == "foobar"

Deploying contract to sandboxed node

Another option is to deploy contract to sandboxed node and interact with it with real transactions. PyTezos has pytezos.sandbox.node.SandboxedNodeTestCase helper to simplify spinning up sandboxed node in Docker. Use self.client interact with it from within your tests.

class pytezos.sandbox.node.SandboxedNodeTestCase(methodName='runTest')[source]

Perform tests with sanboxed node in Docker container.

IMAGE: str = 'bakingbad/sandboxed-node:v17.0-1'

Docker image to use

PORT: int = 8732

Port to expose to host machine

PROTOCOL: str = 'PtNairobiyssHuh87hEhfVBGCVrK3WnS8Z2FT4ymB5tAa4r1nQf'

Hash of protocol to activate

classmethod activate(protocol_alias: str) OperationGroup[source]

Activate protocol.

classmethod bake_block(min_fee: int = 0) OperationGroup[source]

Bake new block.

Parameters:

min_fee – minimum fee of operation to be included in block

property client: PyTezosClient

PyTezos client to interact with sandboxed node.

classmethod setUpClass() None[source]

Spin up sandboxed node container and activate protocol.

classmethod tearDownClass() None[source]

Hook method for deconstructing the class fixture after running all tests in the class.

from pytezos import ContractInterface
from pytezos.sandbox.node import SandboxedNodeTestCase
from pytezos.contract.result import ContractCallResult

contract_michelson = """
    parameter string;
    storage string;
    code { DUP;
        DIP { CAR ; NIL string ; SWAP ; CONS } ;
        CDR ; CONS ;
        CONCAT ;
        NIL operation; PAIR }
"""

class SandboxedContractTest(SandboxedNodeTestCase):
    def test_deploy_contract(self):
        # Create client
        client = self.client.using(key='bootstrap1')
        client.reveal()

        # Originate contract with initial storage
        contract = ContractInterface.from_michelson(contract_michelson)
        opg = contract.using(shell=self.get_node_url(), key='bootstrap1').originate(initial_storage="foo")
        opg = opg.fill().sign().inject()

        self.bake_block()

        # Find originated contract address by operation hash
        opg = client.shell.blocks['head':].find_operation(opg['hash'])
        contract_address = opg['contents'][0]['metadata']['operation_result']['originated_contracts'][0]

        # Load originated contract from blockchain
        originated_contract = client.contract(contract_address).using(shell=self.get_node_url(), key='bootstrap1')

        # Perform real contract call
        call = originated_contract.default("bar")
        opg = call.inject()

        self.bake_block()

        # Get injected operation and convert to ContractCallResult
        opg = client.shell.blocks['head':].find_operation(opg['hash'])
        result = ContractCallResult.from_operation_group(opg)[0]

        self.assertEqual({'string': 'foobar'}, result.storage)

Sandboxed node will be rolled back to genesis block between run of multiple testcases.

Examples

Projects using PyTezos

See how PyTezos testing engine is used in production: