Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

CU-86dtu8tcn - Add boa test constructor to the documentation #1265

Merged
merged 2 commits into from
Jun 19, 2024
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
130 changes: 77 additions & 53 deletions docs/source/testing-and-debugging.md
Original file line number Diff line number Diff line change
Expand Up @@ -43,71 +43,95 @@ 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).

### 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 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.
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.

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.
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.

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.
### Testing

Your Python Script should look something like this:
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, 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

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

# 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 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"
Copy link
Contributor

@meevee98 meevee98 Jun 18, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

is there a reason that the password needs to be "123"? is it a boa-test-constructor issue?

# 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())

@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)

cls.contract_hash = await cls.deploy("./hello_world_with_deploy.nef", cls.genesis)
```

Alternatively you can change the value of `env.NEO_EXPRESS_INSTANCE_DIRECTORY` to the path of your .neo-express
data file:
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)
```

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
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.

invoke = runner.call_contract(path, 'main')
runner.execute()
assert runner.vm_state is VMState.HALT
assert invoke.result is None
```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,
signing_accounts=[self.user1])
self.assertIsNone(result)

result, _ = await self.call("get_message", return_type=str)
self.assertEqual(new_message, result)
```

Loading