Write scripts to automate your testing and deployments
Framework
It can be instructive to directly interact with contracts via the CLI as we are learning, but it is impractical once we are dealing with multiple contracts, calls and environments. To help you deploy and test more efficiently, Completium comes with two TypeScript packages for interacting with smart contracts:
@completium/dapp-ts
@completium/experiment-ts
We will use dapp-ts directly in our Next application. It is meant to be ran from the browser.
We will use experiment-ts for local development.
Installation
We need to add some dependencies to use our scripts:
ts-mocha is a TypeScript version of the testing framework mocha. To enable loading ts modules for our tests, open tsconfig.json and confirm that the property compilerOptions.module is set to "CommonJS".
Script
Create a new file at ./tests/deployAndTest.ts
Before we start writing our script, we'll generate TypeScript bindings of our smart contract, so we can easily interact with them.
It will generate one TS file per contract, in the ./tests/bindings folder.
The bindings are TypeScript modules that expose smart contract features such as entrypoints, assets and variables. Each contract produces a TypeScript class, whose methods are the contract features.
The generate binding-ts script extracts information from the contract code, including type definitions, and function interfaces. The script should be run every time changes are made to the smart contract source. The generated files should not be directly modified, as they will be overwritten.
In deployAndTest.ts, we'll use mocha to create test cases, first the required imports:
This will allow you to run the tests with the terminal command yarn test
With our current settings, the tests will run in the sandbox blockchain. The sandbox simulates a real blockchain that produces a block every 5 seconds. This is faster than the ghostnet or mainnet chains, but far from optimal for development purposes.
To get quicker results of the tests, we can use the mockup mode, which is a lighter version of the local chain. This executes transactions immediately but does not provide any RPC access. It is suitable for local scripted tests, but not integration tests (with a wallet and a front-end application).
To enable the mockup mode:
cclimockupinitcclisetendpointmockup
And run the tests again.
You can use ccli switch endpoint to return to sandbox mode.
The next block will register the Zombie and Brainz NFT, again with their magic token metadata byte string.
Let's test the mint entrypoint. This next test will mint 1 Zombie (id 1) with Alice's wallet and check her balance.
describe("Mint and trade",async () => {/* we'll add more test cases in this block */})
In this block, add the mint test:
it("mint zombie",async () => {/* calls the contract entrypoint: * `entry mint (tow : address, tid : nat, nbt : nat)` */awaitfa2.mint(alice.get_address(),// townewNat(1),// tidnewNat(1),// nbt { as: alice, amount:newTez(2), } )/* check that Alice now has 1 zombie * in order to check the balance, we need to read the contract storage defined by: * `asset ledger identified by lowner ltokenid to big_map` * in the following, we create the key we need to lookup (`lowner`, `ltokenid`) * then we use the generated `get_<asset_name>_value()` method */constkey=newledger_key(alice.get_address(),newNat(1))constamount=awaitfa2.get_ledger_value(key)assert(amount?.to_number() ===1) })
As with the CLI testing, we'll check that the buy will fail if the marketplace is not an operator:
it("buy zombie before operator update should fail",async () => {awaitexpect_to_fail(async () => {/* call the entrypoint: * `entry buy(order_id: nat, amount_: nat)` */awaitmarket.buy(newNat(1),newNat(1), { amount:newTez(5), as: bob }) },fa2.errors.INVALID_CALLER)// check that Alice still has 1 zombieconstkey=newledger_key(alice.get_address(),newNat(1))constamount=awaitfa2.get_ledger_value(key)assert(amount?.to_number() ===1) })
Now let's approve the marketplace for all of Alice's tokens:
it("approve marketplace",async () => {/* the entrypoint * `entry update_operators_for_all (upl : list<update_for_all_op>)` * takes an enum value as parameter, the values are exposed as classes as well: */constarg=newadd_for_all(market.get_address())awaitfa2.update_operators_for_all([arg], { as: alice }) })
If we try to buy again, but with insufficient funds:
it("buy zombie without enough tez should fail",async () => {awaitexpect_to_fail(async () => {awaitmarket.buy(newNat(1),newNat(1), { amount:newTez(1), as: bob }) },market.errors.r_value)// check that Alice still has 1 zombieconstkey=newledger_key(alice.get_address(),newNat(1))constamount=awaitfa2.get_ledger_value(key)assert(amount?.to_number() ===1) })
Now with the correct amount, the purchase will go through:
it("buy zombie",async () => {awaitmarket.buy(newNat(1),newNat(1), { amount:newTez(5), as: bob })// check that Alice now has 0 zombieconstkey=newledger_key(alice.get_address(),newNat(1))constamount=awaitfa2.get_ledger_value(key)assert(amount ===undefined)// check that Bob now has 1 zombieconstkey2=newledger_key(bob.get_address(),newNat(1))constamount2=awaitfa2.get_ledger_value(key2)assert(amount2?.to_number() ===1) })