Michelson integration tests

Step by step guide

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

Loading contract

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

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

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

# From blockchain
contract = pymavryk.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 pymavryk.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 pymavryk.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. PyMavryk has pymavryk.sandbox.node.SandboxedNodeTestCase helper to simplify spinning up sandboxed node in Docker. Use self.client interact with it from within your tests.

class pymavryk.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: PyMavrykClient

PyMavryk 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 pymavryk import ContractInterface
from pymavryk.sandbox.node import SandboxedNodeTestCase
from pymavryk.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 PyMavryk

See how PyMavryk testing engine is used in production: