Zum Inhalt

Testen und Debuggen

Testing ist unerlässlich, um die Funktionalität, Qualität und Sicherheit von Softwareprodukten zu gewährleisten. Dies gilt insbesondere für die Entwicklung von Smart Contracts, da Smart Contracts nach der Bereitstellung viel schwieriger, wenn überhaupt möglich, zu aktualisieren sind als traditionelle Software, und Fehler in ihnen potenziell zu erheblichen finanziellen Verlusten führen können.

Testing ist ein sehr komplexes Thema. Alephiums Web3 SDK trifft bei seinem Testframework die folgenden wohlüberlegten Designentscheidungen:

  • Unit-Tests und Integrationstests sind beide wichtig. Obwohl die Unterscheidung zwischen ihnen im Allgemeinen verschwommen sein kann, zieht das Testframework die Grenze daran, ob die zu testenden Smart Contracts bereitgestellt werden müssen oder nicht.
  • Testcode ist ebenfalls Code, er sollte sauber und wartbar sein. Das Web3 SDK generiert automatisch Test-Boilerplates, um das Schreiben und Warten von Testfällen zu erleichtern.
  • Tests werden gegen den Alephium-Full-Node im devnet ausgeführt, der dieselbe Codebasis wie Alephium mainnet hat und sich nur in den Konfigurationen unterscheidet.

Alephium unterstützt auch die Möglichkeit, Debug-Anweisungen in den Smart Contracts auszugeben, was während der Entwicklung sehr nützlich ist, um Probleme zu diagnostizieren.


Unit Test

Ein Unit-Test überprüft eine spezifische Funktion eines Vertrags, ohne dass der Vertrag bereitgestellt werden muss. Fangen wir mit einem einfachen Beispiel an:

Unit Test
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
    }
}

Die Math-Vertragsfunktion add addiert zwei Zahlen zusammen. Jedes Mal, wenn sie aufgerufen wird, inkrementiert sie auch den counter und gibt ein Add-Ereignis aus. Hier ist, wie wir es mit dem Web3 SDKtesten können:

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)

Um die add-Funktion im Math-Vertrag zu testen, müssen wir den Anfangszustand von Math mit initialFieldseinrichten und die Testargumente für add mit testArgsbereitstellen. Nach Ausführung des Tests können wir überprüfen, dass die add-Funktion den richtigen Wert zurückgibt, das counter-Feld ordnungsgemäß aktualisiert wird und das Add-Ereignis auch mit den richtigen Feldern ausgelöst wird.

Nun lassen Sie uns Math etwas aufpeppen: Jedes Mal, wenn die add-Funktion aufgerufen wird, kostet sie den Aufrufer 1 ALPH:

Unit Test | Math-Contract
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
    }
}

Um die Logik des Asset-Transfers in der add-Funktion zu testen, verwenden wir den folgenden Testcode:

Unit Test | 'add'-Funktion
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 richtet das anfängliche Asset für den Math-Vertrag ein, in diesem Fall 2 ALPH. inputAssets richtet alle Eingangsassets für die Transaktion ein, in diesem Fall 2 ALPH von testAddress, das auch die callerAddress beim Aufruf der add-Funktion ist, weil es im ersten Eingang der inputAssets liegt. Nach Ausführung des Tests können wir überprüfen, dass das Guthaben des ersten Ausgangs auf 3 ALPH erhöht wird, da der Math-Vertrag 1 ALPH für die Ausführung der add-Funktion erhält. Das Guthaben des zweiten Ausgangs wird weniger, weil testAddress ebenfalls 1 ALPH sowie die Gasgebühr ausgibt.

Nachdem wir die Assets getestet haben, wie wäre es, wenn der Math-Vertrag auf einen anderen Vertrag angewiesen ist, um die Arbeit zu erledigen?

Unit Test | Math-Vertrag
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
    }
}

In diesem Fall ist der Math-Vertrag auf den Add-Vertrag angewiesen, um die Additionsoperation auszuführen. So testen wir die add-Funktion:

Add-Vertrag
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)
// restliche Überprüfungen ..

Add.stateForTest({}) erstellt einen Zustand des Add-Vertrags, den wir an den Parameter existingContracts übergeben können. Wir müssen auch addState.contractId als Anfangsfeld an den Math-Vertrag übergeben. Nach Ausführung des Tests können wir das Ergebnis mit denselben Behauptungen wie zuvor überprüfen.


Integrations-Test

Ein Integrations-Test überprüft eine Funktion eines Satzes bereitgestellter Verträge. Verwenden wir das letzte Beispiel aus dem Abschnitt Unit Test:

Integrations-Test | Math-Contract / Add-Contract
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
    }
}

Da die add-Funktion im Math-Vertrag nicht nur den Vertragszustand aktualisiert, sondern auch Assets überträgt, müssen wir sie über TxScript aufrufen:

Rust
TxScript AddScript(math: Math, x: U256, y: U256) {
    let _ = math.add{ callerAddress!() -> ALPH: 1 alph }(x, y)
}

Im Integrations-Test deployen wir sowohl die Verträge Add als auch Math und führen dann das Skript AddScript aus:

Integrations-Test | 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,
})

// Das counter-Feld in Math wird aktualisiert
const mathContractState = await mathContract.fetchState()
expect(mathContractState.fields.counter).toEqual(1n)

// Der Vertragsguthaben in Math wird aktualisiert
expect(BigInt(mathContractState.asset.alphAmount)).toEqual(3n * ONE_ALPH)

// Ein Add-Ereignis in Math wird ausgelöst
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' }])

Nach Ausführung von AddScript können wir den Zustand, das Guthaben und die Ereignisse des Math-Vertrags überprüfen. Bitte beachten Sie den Abschnitt "Mit Verträgen interagieren" für weitere Details.


Debuggen

Debug-Anweisungen in Alephium unterstützen die Zeichenketteninterpolation. Das Drucken von Debug-Nachrichten hat die gleiche Syntax wie das Auslösen von Vertragsereignissen. Zum Beispiel:

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

In obigem Beispiel ist die add-Funktion im Math-Vertrag eine reine Funktion, die den Zustand der Blockchain nicht aktualisiert. Wenn wir die add-Funktion sowohl mit Unit- als auch mit Integrations-Tests testen, wird die Debug-Nachricht sowohl in der Terminalkonsole als auch im Full-Node-Protokoll ausgegeben:

Bash
# Ihre Vertragsadresse sollte unterschiedlich sein
> Contract @ vrcKqNuMrGpA32eUgUvAB3HBfkN4eycM5uvXukwn5SxP - 1 + 2 = 3

Wenn die add-Funktion den Blockchain-Zustand aktualisiert und daher TxScript zur Ausführung erfordert, wird die Debug-Nachricht nur im Full-Node-Protokoll für Integrations-Tests ausgegeben, da die Ausführung nicht sofort erfolgt und das Ergebnis nicht sofort an die Terminalkonsole zurückgegeben werden kann. Für Unit-Tests werden Debug-Nachrichten weiterhin sowohl in der Terminalkonsole als auch im Full-Node-Protokoll ausgegeben.

Im Hintergrund ist Debug ein spezielles Systemereignis das nur in devnet verfügbar ist.