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
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:
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 initialFields
einrichten und die Testargumente für add
mit testArgs
bereitstellen.
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
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
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
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
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
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:
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
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:
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:
# 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.