Aller au contenu

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 :

Rust
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:

TypeScript
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:

Rust
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:

TypeScript
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 ?

Rust
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
    }
}
Dans ce cas, le contrat Math dépend du contrat Add pour effectuer l'opération d'addition. Voici comment nous testons la fonction add:

TypeScript
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:

Rust
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
    }
}
Puisque la fonction 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:

Rust
TxScript AddScript(math: Math, x: U256, y: U256) {
    let _ = math.add{ callerAddress!() -> ALPH: 1 alph }(x, y)
}
Dans le test d'intégration, nous déployons à la fois les contrats Add et Math puis nous exécutons le script AddScript:

TypeScript
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:

Rust
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
    }
}
Dans l'exemple ci-dessus, la fonction 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:

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