Part 2: Inter-contract calls and testing
Previously, you learned how to create your first dApp. In this second session, you will enhance your skills on:
- How to do inter-contract calls.
- How to use views.
- How to do unit & mutation tests.
On the first version of the poke game, you were able to poke any deployed contract. Now, you will add a new function to store on the trace an additional feedback message coming from another contract.
Poke and Get Feedback sequence diagram
Get the code
Get the code from the first session: https://github.com/marigold-dev/training-dapp-1/blob/main/solution
git clone https://github.com/marigold-dev/training-dapp-1.git
Reuse the code from the previous smart contract: https://github.com/marigold-dev/training-dapp-1/blob/main/solution/contracts/pokeGame.jsligo
Install all libraries locally:
cd solution && npm i && cd app && yarn install && cd ..
Modify the poke function
Change the storage to reflect the changes:
- If you poke directly, you just register the contract's owner address and no feedback.
- If you poke and ask to get feedback from another contract, then you register the other contract address and an additional feedback message.
Here the new sequence diagram of the poke function.
-
Edit
./contracts/pokeGame.jsligo
and replace storage definition by this one:export type pokeMessage = {
receiver : address,
feedback : string
};
export type storage = {
pokeTraces : map<address, pokeMessage>,
feedback : string
}; -
Replace your poke function with theses lines:
@entry
const poke = (_ : unit, store : storage) : return_ => {
let feedbackMessage = {receiver : Tezos.get_self_address() ,feedback: ""};
return [ list([]) as list<operation>, {...store,
pokeTraces : Map.add(Tezos.get_source(), feedbackMessage, store.pokeTraces) }];
};Explanation:
...store
do a copy by value of your object. Have a look on the Functional updates documentation. Note: you cannot do assignment like thisstore.pokeTraces=...
in JsLIGO, there are no concept of Classes, useFunctional updates
instead.Map.add(...
: Add a key, value entry to a map. For more information about Map.export type storage = {...};
aRecord
type is declared, it is an object structure.Tezos.get_self_address()
is a native function that returns the current contract address running this code. Have a look on Tezos native functions.feedback: ""
: poking directly does not store feedbacks.
-
Edit
pokeGame.storageList.jsligo
to change the storage initialization.#import "pokeGame.jsligo" "Contract"
const default_storage: Contract.storage = {
pokeTraces: Map.empty as map<address, Contract.pokeMessage>,
feedback: "kiss"
}; -
Compile your contract.
TAQ_LIGO_IMAGE=ligolang/ligo:1.1.0 taq compile pokeGame.jsligo
Write a second function
pokeAndGetFeedback
involving the call to another contract a bit later, let's do unit testing first!
Write unit tests
-
Add a new unit test smart-contract file
unit_pokeGame.jsligo
.taq create contract unit_pokeGame.jsligo
ℹ️ Testing documentation can be found here ℹ️ Test module with specific functions here
-
Edit the file.
#import "./pokeGame.jsligo" "PokeGame"
export type main_fn = module_contract<parameter_of PokeGame, PokeGame.storage>;
// reset state
const _ = Test.reset_state(2 as nat, list([]) as list<tez>);
const faucet = Test.nth_bootstrap_account(0);
const sender1: address = Test.nth_bootstrap_account(1);
const _2 = Test.log("Sender 1 has balance : ");
const _3 = Test.log(Test.get_balance_of_address(sender1));
const _4 = Test.set_baker(faucet);
const _5 = Test.set_source(faucet);
export const initial_storage = {
pokeTraces: Map.empty as map<address, PokeGame.pokeMessage>,
feedback: "kiss"
};
export const initial_tez = 0mutez;
//functions
export const _testPoke = (
taddr: typed_address<parameter_of PokeGame, PokeGame.storage>,
s: address
): unit => {
const contr = Test.to_contract(taddr);
const contrAddress = Tezos.address(contr);
Test.log("contract deployed with values : ");
Test.log(contr);
Test.set_source(s);
const status = Test.transfer_to_contract(contr, Poke(), 0 as tez);
Test.log(status);
const store: PokeGame.storage = Test.get_storage(taddr);
Test.log(store);
//check poke is registered
match(Map.find_opt(s, store.pokeTraces)) {
when (Some(pokeMessage)):
do {
assert_with_error(
pokeMessage.feedback == "",
"feedback " + pokeMessage.feedback + " is not equal to expected "
+ "(empty)"
);
assert_with_error(
pokeMessage.receiver == contrAddress,
"receiver is not equal"
);
}
when (None()):
assert_with_error(false, "don't find traces")
};
};
// TESTS //
const testSender1Poke =
(
(): unit => {
const orig =
Test.originate(contract_of(PokeGame), initial_storage, initial_tez);
_testPoke(orig.addr, sender1);
}
)();Explanations:
#import "./pokeGame.jsligo" "PokeGame"
to import the source file as module in order to call functions and use object definitions.export type main_fn
it will be useful later for the mutation tests to point to the main function to call/mutate.Test.reset_state ( 2...
this creates two user accounts on the test environment.Test.nth_bootstrap_account
this return the nth account from the environment.Test.to_contract(taddr)
andTezos.address(contr)
are util functions to convert typed addresses, contract and contract addresses.let _testPoke = (s : address) : unit => {...}
declaring function starting with_
is escaping the test for execution. Use this to factorize tests changing only the parameters of the function for different scenarios.Test.set_source
do not forget to set this value for the transaction signer.Test.transfer_to_contract(CONTRACT, PARAMS, TEZ_COST)
A transaction to send, it returns an operation.Test.get_storage
this is how to retrieve the contract's storage.assert_with_error(CONDITION,MESSAGE)
Use assertion for unit testing.const testSender1Poke = ...
This test function will be part of the execution report.Test.originate_module(MODULE_CONVERTED_TO_CONTRACT,INIT_STORAGE, INIT_BALANCE)
It originates a smart contract into the Test environment. A module is converted to a smart contract.
-
Run the test.
TAQ_LIGO_IMAGE=ligolang/ligo:1.1.0 taq test unit_pokeGame.jsligo
Output should give you intermediary logs and finally the test results.
┌──────────────────────┬────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┐
│ Contract │ Test Results │
├──────────────────────┼────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┤
│ unit_pokeGame.jsligo │ "Sender 1 has balance : " │
│ │ 3800000000000mutez │
│ │ "contract deployed with values : " │
│ │ KT1KwMWUjU6jYyLCTWpZAtT634Vai7paUnRN(None) │
│ │ Success (2130n) │
│ │ {feedback = "kiss" ; pokeTraces = [tz1TDZG4vFoA2xutZMYauUnS4HVucnAGQSpZ -> {feedback = "" ; receiver = KT1KwMWUjU6jYyLCTWpZAtT634Vai7paUnRN}]} │
│ │ Everything at the top-level was executed. │