From c9d1693e206c7fe1a300e0cd4e759dc475cb4dee Mon Sep 17 00:00:00 2001 From: luc10921 Date: Mon, 17 Jun 2024 18:03:08 -0300 Subject: [PATCH 1/2] CU-86dtu8tcn - Add boa test constructor to the documentation --- docs/source/testing-and-debugging.md | 118 ++++++++++++++------------- 1 file changed, 63 insertions(+), 55 deletions(-) diff --git a/docs/source/testing-and-debugging.md b/docs/source/testing-and-debugging.md index 352f455d..d159e091 100644 --- a/docs/source/testing-and-debugging.md +++ b/docs/source/testing-and-debugging.md @@ -43,71 +43,79 @@ from boa3.boa3 import Boa3 Boa3.compile_and_save('path/to/your/file.py', debug=True) ``` -## Neo Test Runner +## boa test constructor ### Downloading -Install [Neo-Express](https://github.com/neo-project/neo-express#installation) and [Neo Test Runner](https://github.com/ngdenterprise/neo-test#neo-test-runner). +Install [boa-test-constructor](https://pypi.org/project/boa-test-constructor/) with pip. We use this extension to run an isolated +test environment for smart contracts with a neo-go node. When installing boa-test-constructor, [neo-mamba](https://dojo.coz.io/neo3/mamba/index.html) +will be installed too. ### Testing -Before writing your tests, make sure you have a Neo-Express network for local tests. -If you do not yet have a local network, open a terminal and run `neoxp create`. -Please refer to [Neo-Express documentation](https://github.com/neo-project/neo-express/blob/master/docs/command-reference.md#neoxp-create) -for more details of how to configure your local network. +Create a Python Script, import the `SmartContractTestCase` class, and create a test class that inherits it. To set up +the test environment, you'll need to override the `setUpClass` method from `SmartContractTestCase`. This method is +synchronous, so if you need to set up asynchronous tasks, you can create another async method and use it int the +`asyncio.run` method from `asyncio`. Common operations would be: creating accounts, deploying the smart contract, +selecting your "main" smart contract, and transferring GAS to the new accounts. -Create a Python Script, import the NeoTestRunner class, and define a function to test your smart contract. In this -function you'll need a NeoTestRunner object, which takes the path of your Neo-Express network configuration file as an -argument to set up the test environment. +Then, create functions to test the expected behavior of your smart contract. To invoke or test invoke your smart +contract, use the `call` method from `SmartContractTestCase`. The two positional parameters are the name of the method +you want to invoke and a list of its arguments. The keyword parameters are the return type, a list of signing accounts, +a list of signers, and the smart contract you want to invoke. If you get an error when calling a smart contract, then an +error will be raised. -You'll have to call the method `call_contract()` to interact with your smart contract. Its parameters are the path of -the compiled smart contract, the smart contract's method, and the arguments if necessary. -This call doesn't return the result directly, but includes it in a queue of invocations. To execute all the invocations -set up, call the method `execute()`. Then assert the result of your invoke to see if it's correct. - -Note that `invoke.result` won't be set if the execution fails, so you should also assert if `runner.vm_state` is valid -for your test case. +To persist an invocation, use the `signing_accounts` parameter to pass a list of signing accounts when calling the +smart contract. If you don't pass it, then it will always be a test invoke. The `signers` parameter can be used +alongside the `signing_accounts` if you want to change the witness scope of the invocation, or by itself if you want to +test invoke but also define the signers of the transaction. Your Python Script should look something like this: ```python - -from boa3.sc.types import VMState -from boa3_test.test_drive.testrunner.neo_test_runner import NeoTestRunner - - -def test_hello_world_main(): - neoxp_config_file = '{path-to-neo-express-config-file}' - project_root_folder = '{path-to-project-root-folder}' - path = f'{project_root_folder}/boa3_test/examples/hello_world.nef' - runner = NeoTestRunner(neoxp_config_file) - - invoke = runner.call_contract(path, 'main') - runner.execute() - assert runner.vm_state is VMState.HALT - assert invoke.result is None +import asyncio +from boaconstructor import SmartContractTestCase +from neo3.core import types +from neo3.wallet.account import Account +from neo3.contracts.contract import CONTRACT_HASHES + +GAS = CONTRACT_HASHES.GAS_TOKEN + +class SmartContractTest(SmartContractTestCase): + genesis: Account + user1: Account + + # if this variable is set, then this contract hash will be called whenever you don't use the `target_contract` parameter on the `call` method + contract_hash: types.UInt160 + + @classmethod + def setUpClass(cls) -> None: + super().setUpClass() + # you can name the account whatever you want, but the password needs to be "123" + cls.user1 = cls.node.wallet.account_new("123", "alice") + cls.genesis = cls.node.wallet.account_get_by_label("committee") + + asyncio.run(cls.asyncSetupClass()) + + @classmethod + async def asyncSetupClass(cls) -> None: + # this `transfer` method already uses the correct amount of decimals for the token + await cls.transfer(GAS, cls.genesis.script_hash, cls.user1.script_hash, 100) + + # the smart contract I'm deploying is hello_world_with_deploy.py from the "Neo Methods" https://dojo.coz.io/neo3/boa/getting-started.html#neo-methods + cls.contract_hash = await cls.deploy("./smart_contract.nef", cls.genesis) + + async def test_message(self): + expected = "Hello World" + result, _ = await self.call("get_message", return_type=str) + self.assertEqual(expected, result) + + new_message = "New Message" + # since we want this change to persist, we need to pass the signing account + result, _ = await self.call("set_message", [new_message], return_type=None, + signing_accounts=[self.user1]) + self.assertIsNone(result) + + result, _ = await self.call("get_message", return_type=str) + self.assertEqual(new_message, result) ``` - -Alternatively you can change the value of `env.NEO_EXPRESS_INSTANCE_DIRECTORY` to the path of your .neo-express -data file: - -```python - -from boa3.sc.types import VMState -from boa3_test.test_drive.testrunner.neo_test_runner import NeoTestRunner -from boa3.internal import env - -env.NEO_EXPRESS_INSTANCE_DIRECTORY = '{path-to-neo-express-config-file}' - - -def test_hello_world_main(): - root_folder = '{path-to-project-root-folder}' - path = f'{root_folder}/boa3_test/examples/hello_world.nef' - runner = NeoTestRunner() # the default path to the Neo-Express is the one on env.NEO_EXPRESS_INSTANCE_DIRECTORY - - invoke = runner.call_contract(path, 'main') - runner.execute() - assert runner.vm_state is VMState.HALT - assert invoke.result is None -``` - From 8177c9f47884a142fb177419314350eb7a634c3f Mon Sep 17 00:00:00 2001 From: luc10921 Date: Wed, 19 Jun 2024 11:17:13 -0300 Subject: [PATCH 2/2] CU-86dtu8tcn - Add boa test constructor to the documentation --- docs/source/testing-and-debugging.md | 66 +++++++++++++++++----------- 1 file changed, 41 insertions(+), 25 deletions(-) diff --git a/docs/source/testing-and-debugging.md b/docs/source/testing-and-debugging.md index d159e091..4cb68c55 100644 --- a/docs/source/testing-and-debugging.md +++ b/docs/source/testing-and-debugging.md @@ -47,30 +47,24 @@ Boa3.compile_and_save('path/to/your/file.py', debug=True) ### Downloading -Install [boa-test-constructor](https://pypi.org/project/boa-test-constructor/) with pip. We use this extension to run an isolated -test environment for smart contracts with a neo-go node. When installing boa-test-constructor, [neo-mamba](https://dojo.coz.io/neo3/mamba/index.html) -will be installed too. +Install [boa-test-constructor](https://pypi.org/project/boa-test-constructor/) with pip by running: +```shell +$ pip install neo3-boa[test] +``` +This will ensure that the boa-test-constructor version that will be installed is compatible with the latest version of +neo3-boa. + +We use this extension to run an isolated test environment for smart contracts with a neo-go node. When installing +boa-test-constructor, [neo-mamba](https://dojo.coz.io/neo3/mamba/index.html) will be installed too. ### Testing Create a Python Script, import the `SmartContractTestCase` class, and create a test class that inherits it. To set up the test environment, you'll need to override the `setUpClass` method from `SmartContractTestCase`. This method is -synchronous, so if you need to set up asynchronous tasks, you can create another async method and use it int the -`asyncio.run` method from `asyncio`. Common operations would be: creating accounts, deploying the smart contract, -selecting your "main" smart contract, and transferring GAS to the new accounts. - -Then, create functions to test the expected behavior of your smart contract. To invoke or test invoke your smart -contract, use the `call` method from `SmartContractTestCase`. The two positional parameters are the name of the method -you want to invoke and a list of its arguments. The keyword parameters are the return type, a list of signing accounts, -a list of signers, and the smart contract you want to invoke. If you get an error when calling a smart contract, then an -error will be raised. - -To persist an invocation, use the `signing_accounts` parameter to pass a list of signing accounts when calling the -smart contract. If you don't pass it, then it will always be a test invoke. The `signers` parameter can be used -alongside the `signing_accounts` if you want to change the witness scope of the invocation, or by itself if you want to -test invoke but also define the signers of the transaction. - -Your Python Script should look something like this: +synchronous, so if you need to set up asynchronous tasks, like tasks that need to interact with the local blockchain, +then you can create another async method and use it int the `asyncio.run` method from `asyncio`. Common operations would +be: creating accounts, deploying the smart contract, selecting your "main" smart contract, and transferring GAS to the +new accounts. ```python import asyncio @@ -81,18 +75,21 @@ from neo3.contracts.contract import CONTRACT_HASHES GAS = CONTRACT_HASHES.GAS_TOKEN -class SmartContractTest(SmartContractTestCase): +# the smart contract that will be tested is hello_world_with_deploy.py from the "Neo Methods" https://dojo.coz.io/neo3/boa/getting-started.html#neo-methods +class HelloWorldWithDeployTest(SmartContractTestCase): genesis: Account user1: Account - # if this variable is set, then this contract hash will be called whenever you don't use the `target_contract` parameter on the `call` method + # if this variable is set, then this contract hash will be used whenever you don't specify which smart contract you'll want to invoke contract_hash: types.UInt160 @classmethod def setUpClass(cls) -> None: + # whenever a new test is run, the local blockchain will be reset, that's why we need to set up the environment again super().setUpClass() # you can name the account whatever you want, but the password needs to be "123" - cls.user1 = cls.node.wallet.account_new("123", "alice") + # this is a boa-test-constructor deliberate decision to make the tests run faster + cls.user1 = cls.node.wallet.account_new(label="alice", password="123") cls.genesis = cls.node.wallet.account_get_by_label("committee") asyncio.run(cls.asyncSetupClass()) @@ -101,15 +98,34 @@ class SmartContractTest(SmartContractTestCase): async def asyncSetupClass(cls) -> None: # this `transfer` method already uses the correct amount of decimals for the token await cls.transfer(GAS, cls.genesis.script_hash, cls.user1.script_hash, 100) - - # the smart contract I'm deploying is hello_world_with_deploy.py from the "Neo Methods" https://dojo.coz.io/neo3/boa/getting-started.html#neo-methods - cls.contract_hash = await cls.deploy("./smart_contract.nef", cls.genesis) + + cls.contract_hash = await cls.deploy("./hello_world_with_deploy.nef", cls.genesis) +``` + +Then, create functions to test the expected behavior of your smart contract. To invoke your smart contract, use the +`call` method from `SmartContractTestCase`. The two positional parameters are the name of the method you want to invoke +and a list of its arguments. The keyword parameters are the return type, a list of signing accounts, a list of signers, +and the smart contract you want to invoke. Method name, and return type are obligatory, but you'll most likely also need +to pass the args too. If you get an error when calling a smart contract, then an error will be raised. +```python + # inside the HelloWorldWithDeployTest class async def test_message(self): expected = "Hello World" result, _ = await self.call("get_message", return_type=str) self.assertEqual(expected, result) +``` +To persist an invocation, use the `signing_accounts` parameter to pass a list of signing accounts when calling the +smart contract. If you don't pass it, then it will always be a test invoke, meaning it won't be saved on the local +blockchain. The `signers` parameter can be used alongside the `signing_accounts` if you want to change the +[witness scope](https://developers.neo.org/docs/n3/foundation/Transactions#signature-scope) of the invocation, or by +itself if you want to test invoke but also define the [signers](https://developers.neo.org/docs/n3/foundation/Transactions#signers) +of the transaction. + +```python + # continuation of the 'async def test_message(self)' function + # to set this message in the smart contract, we need to pass the signing account new_message = "New Message" # since we want this change to persist, we need to pass the signing account result, _ = await self.call("set_message", [new_message], return_type=None,