Test et débogage
Les tests sont essentiels pour garantir la fonctionnalité, la qualité et la sécurité de tout produit logiciel. Cela est particulièrement vrai en ce qui concerne le développement de contrats intelligents car une fois déployés, les contrats intelligents sont beaucoup plus difficiles, voire impossibles, à mettre à jour par rapport aux logiciels traditionnels, et les bogues dans ceux-ci peuvent potentiellement entraîner des pertes financières importantes.
Les tests sont un sujet très complexe. Le Web3 SDK d'Alephium prend les décisions de conception suivantes et engagées en ce qui concerne son framework de test :
- Les tests unitaires et les tests d'intégration sont tous deux importants. Même si en général la distinction entre eux peut être floue, la ligne tracée par le framework de test est de savoir si les contrats intelligents testés doivent être déployés ou non.
- Le code de test est aussi du code, il doit être propre et maintenable également. Le Web3 SDK génère automatiquement des modèles de test pour faciliter l'écriture et la maintenance des cas de test.
- Les tests sont exécutés contre le nœud complet d'Alephium dans
devnet, qui a la même base de code que le
mainnet
d'Alephium avec des différences uniquement dans les configurations.
Alephium prend également en charge la possibilité d'émettre des déclarations de débogage dans les contrats intelligents, ce qui est très utile pour diagnostiquer les problèmes lors du développement.
Test unitaire
Un test unitaire teste une fonction spécifique d'un contrat, sans nécessiter que le contrat soit déployé. Commençons par un exemple simple :
Contract Math(mut counter: U256) {
event Add(x: U256, y: U256)
@using(updateFields = true, checkExternalCaller = false)
pub fn add(x: U256, y: U256) -> U256 {
emit Add(x, y)
counter = counter + 1
return x + y
}
}
Le contrat Math
dispose d'une fonction add
qui ajoute deux nombres ensemble.
Chaque fois qu'elle est appelée, elle incrémente également le counter
et émet
un événement Add. Voici comment nous pouvons le tester en utilisant le
Web3 SDK:
const result = await Math.tests.add({
initialFields: { counter: 0n },
testArgs: { x: 1n, y: 2n }
})
expect(result.returns).toEqual(3n)
expect(result.events[0].name).toEqual('Add')
expect(result.events[0].fields).toEqual({ x: 1n, y: 2n })
expect(result.contracts[0].fields.counter).toEqual(1n)
Pour tester la fonction add
dans le contrat Math
, nous devons configurer
l'état initial de Math
en utilisantinitialFields
, et fournir les arguments
de test pour add
en utilisant testArgs
. Après l'exécution du test, nous pouvons
vérifier que la fonction add
renvoie la valeur correcte, que le champ counter
est correctement mis à jour, et que l'événement Add
est émis avec les bons
champs également.
Maintenant, donnons un peu plus de peps à Math
: chaque fois que la fonction add
est appelée,
elle coûtera à l'appelant 1 ALPH
:
Contract Math(mut counter: U256) {
event Add(x: U256, y: U256)
@using(preapprovedAssets = true, assetsInContract = true, updateFields = true, checkExternalCaller = false)
pub fn add(x: U256, y: U256) -> U256 {
transferTokenToSelf!(callerAddress!(), ALPH, 1 alph)
emit Add(x, y)
counter = counter + 1
return x + y
}
}
Pour tester la logique de transfert d'actifs dans la fonction add
,
nous utilisons le code de test suivant:
const result = await Math.tests.add({
initialFields: { counter: 0n },
testArgs: { x: 1n, y: 2n },
initialAsset: { alphAmount: 2n * ONE_ALPH },
inputAssets: [{ address: testAddress, asset: { alphAmount: 2n * ONE_ALPH } }]
})
expect(result.txOutputs[0].alphAmount).toEqual(3n * ONE_ALPH)
expect(result.txOutputs[1].alphAmount).toEqual(ONE_ALPH - 625000n * (10n ** 11n))
initialAsset
configure l'actif initial pour le contrat Math
dans ce cas 2 ALPH
.
inputAssets
configure tous les actifs d'entrée pour la transaction, dans ce cas
2 ALPH
provenant de testAddress
wqui sera également l'callerAddress
lors de
l'appel de la fonction add
car il se trouve dans le premier input des inputAssets
.
Après l'exécution du test, nous pouvons vérifier que le solde de la première sortie
est augmenté à 3 ALPH
puisque le contrat Math
reçoit 1 ALPH
pour l'exécution de
la fonction add
. Le solde de la deuxième sortie devient moins élevé car testAddress
dépense 1 ALPH
ainsi que les frais de gaz.
Maintenant que nous avons testé les actifs, que se passe-t-il lorsque le contrat Math
dépend d'un autre contrat pour faire le travail ?
Contract Math(add: Add, mut counter: U256) {
event Add(x: U256, y: U256)
@using(preapprovedAssets = true, assetsInContract = true, updateFields = true, checkExternalCaller = false)
pub fn add(x: U256, y: U256) -> U256 {
emit Add(x, y)
counter = counter + 1
transferTokenToSelf!(callerAddress!(), ALPH, 1 alph)
return add.exec(x, y)
}
}
Contract Add() {
@using(checkExternalCaller = false)
pub fn exec(x: U256, y: U256) -> U256 {
return x + y
}
}
Math
dépend du contrat Add
pour effectuer l'opération d'addition.
Voici comment nous testons la fonction add
:
const addState = Add.stateForTest({})
const result = await Math.tests.add({
initialFields: { add: addState.contractId, counter: 0n },
testArgs: { x: 1n, y: 2n },
initialAsset: { alphAmount: 2n * ONE_ALPH },
inputAssets: [{ address: testAddress, asset: { alphAmount: 2n * ONE_ALPH } }],
existingContracts: [addState]
})
expect(result.returns).toEqual(3n)
// rest of the assertions ..
Add.stateForTest({})
crée un état du contrat Add
que nous pouvons
passer au paramètre existingContracts
. . Nous devons également passer
addState.contractId
comme champ initial au contrat Math
. Après
l'exécution du test, nous pouvons vérifier le résultat avec les mêmes
assertions qu'auparavant.
Test d'intégration
Un test d'intégration teste une fonctionnalité d'un ensemble de contrats déployés. Utilisons le dernier exemple de la section Test unitaire:
Contract Math(add: Add, mut counter: U256) {
event Add(x: U256, y: U256)
@using(preapprovedAssets = true, assetsInContract = true, updateFields = true, checkExternalCaller = false)
pub fn add(x: U256, y: U256) -> U256 {
emit Add(x, y)
counter = counter + 1
transferTokenToSelf!(callerAddress!(), ALPH, 1 alph)
return add.exec(x, y)
}
}
Contract Add() {
@using(checkExternalCaller = false)
pub fn exec(x: U256, y: U256) -> U256 {
return x + y
}
}
add
dans le contrat Math
non seulement met à
jour l'état du contrat mais transfère également des actifs, nous devons
l'appeler via TxScript:
TxScript AddScript(math: Math, x: U256, y: U256) {
let _ = math.add{ callerAddress!() -> ALPH: 1 alph }(x, y)
}
Add
et Math
puis
nous exécutons le script AddScript
:
const signer = await getSigner()
const { contractInstance: addContract } = await Add.deploy(signer, { initialFields: {} })
const { contractInstance: mathContract } = await Math.deploy(signer, {
initialFields: { add: addContract.contractId, counter: 0n },
initialAttoAlphAmount: 2n * ONE_ALPH
})
await AddScript.execute(signer, {
initialFields: { math: mathContract.address, x: 1n, y: 2n },
attoAlphAmount: 2n * ONE_ALPH,
})
// `counter` field in `Math` is updated
const mathContractState = await mathContract.fetchState()
expect(mathContractState.fields.counter).toEqual(1n)
// contract balance in `Math` is updated
expect(BigInt(mathContractState.asset.alphAmount)).toEqual(3n * ONE_ALPH)
// `Add` event in `Math` is emitted
const { events } = await signer.nodeProvider.events.getEventsContractContractaddress(mathContract.address, { start: 0 })
expect(events[0].eventIndex).toEqual(0)
expect(events[0].fields).toEqual([{ type: 'U256', value: '1' }, { type: 'U256', value: '2' }])
Après l'exécution de AddScript
nous pouvons vérifier l'état, le solde et
les événements du contrat Math
. Veuillez vous référer à
Interagir avec les contrats pour plus de détails.
Débogage
L'instruction de débogage dans Alephium prend en charge l'interpolation de chaînes. L'impression de messages de débogage a la même syntaxe que l'émission d'événements de contrat. Par exemple:
Contract Math(mut counter: U256) {
@using(checkExternalCaller = false)
pub fn add(x: U256, y: U256) -> U256 {
emit Debug(`${x} + ${y} = ${x + y}`)
return x + y
}
}
add
dans le contrat Math
est une fonction
pure qui ne met pas à jour l'état de la blockchain. Lorsque nous testons la
fonction add
à l'aide à la fois de tests unitaires et d'intégration, le message
de débogage sera imprimé à la fois dans la console du terminal et dans le journal
du nœud complet:
# Your contract address should be different
> Contract @ vrcKqNuMrGpA32eUgUvAB3HBfkN4eycM5uvXukwn5SxP - 1 + 2 = 3
Si la fonction add
met à jour l'état de la blockchain, nécessitant donc
TxScript
pour s'exécuter, le message de débogage ne sera imprimé que dans
le journal du nœud complet pour les tests d'intégration car l'exécution ne
se fait pas immédiatement et le résultat ne peut donc pas être renvoyé à la
console du terminal immédiatement. Pour les tests unitaires, les messages de
débogage seront toujours imprimés à la fois dans la console du terminal et
dans le journal du nœud complet.
Sous le capot, Debug
est un événement système
spécial qui n'est disponible que dans devnet.