Was macht Property-basiertes-Testen?
Bietet Anleitungen zum eigenschaftsbasierten Testen (Property-Based Testing) für mehrere Sprachen und Smart Contracts. Verwenden Sie dies beim Schreiben von Tests, beim Überprüfen von Code mit Serialisierungs-/Validierungs-/Parsing-Mustern, beim Entwerfen von Funktionen oder wenn eigenschaftsbasiertes Testen eine stärkere Abdeckung als beispielbasierte Tests bietet.
Was ist Property-basiertes Testen?
Property-basiertes Testen ist eine Testmethode, bei der Sie allgemeine Eigenschaften definieren, die Ihr Code immer erfüllen sollte, und ein Framework automatisch zufällige Eingaben generiert, um zu überprüfen, ob diese Eigenschaften zutreffen. Dies steht im Gegensatz zu beispielbasierten Tests, bei denen Sie bestimmte Ein-/Ausgabe-Paare manuell definieren.
Wann sollte Property-basiertes Testen verwendet werden?
- Serialisierung/Deserialisierung: Überprüfen Sie, dass das Parsen und Serialisieren von Daten strukturell korrekt ist.
- Validierung: Testen Sie, dass Validierungsregeln konsistent angewendet werden.
- API-Design: Stellen Sie sicher, dass API-Funktionen korrekt auf verschiedene Eingaben reagieren.
- Smart Contracts: Überprüfen Sie Invarianten wie Token-Bilanzen oder Zugriffskontrollen.
- Datenstrukturen: Testen Sie, dass Datenstrukturen konsistente Eigenschaften beibehalten.
Unterstützte Frameworks und Sprachen
Python
- Hypothesis: Ein ausgereiftes Property-basiertes Test-Framework.
- Definieren Sie Strategien für Eingabedaten mit
hypothesis.strategies. - Dekorieren Sie Tests mit
@givenund geben Sie die Strategien als Argumente an. - Verwenden Sie
assume, um ungültige Eingabekombinationen herauszufiltern. - Beispiel:
@given(text())testet eine Funktion mit zufälligen Zeichenketten.
- Definieren Sie Strategien für Eingabedaten mit
JavaScript/TypeScript
- fast-check: Ein leistungsstarkes Property-basiertes Test-Framework.
- Verwenden Sie vordefinierte Erzeuger wie
fc.string(),fc.integer(),fc.array(). - Kombinieren Sie Erzeuger, um komplexe Eingabestrukturen zu erzeugen.
- Testen Sie Invarianten mit
fc.assert.
- Verwenden Sie vordefinierte Erzeuger wie
Haskell
- QuickCheck: Das ursprüngliche Property-basiertes Test-Framework.
- Definieren Sie Eigenschaften als Haskell-Funktionen, die einen Booleschen Wert zurückgeben.
- Verwenden Sie
quickCheck, um die Eigenschaft mit zufälligen Eingaben zu testen. - Schreiben Sie benutzerdefinierte Generatoren für komplexe Datenstrukturen.
Smart Contracts (Solidity)
- Foundry (forge): Integriert Fuzz-Testing in Solidity.
- Verwenden Sie
vm.assume, um ungültige Eingaben herauszufiltern. - Schreiben Sie Invariantentests, die nach mehreren Transaktionen gelten.
- Beispiel: Token-Bilanzen sollten nach jeder Transaktion korrekt sein.
- Verwenden Sie
Best Practices
- Fangen Sie einfach an: Identifizieren Sie eine einzelne Invariante, die Ihr Code erfüllen sollte.
- Validierungslogik: Schreiben Sie Generator-basierte Tests, die Randfälle wie leere Zeichenketten,
null/None, extrem große Zahlen und verschachtelte Strukturen abdecken. - Verwenden Sie
assume/filter: Entfernen Sie ungültige Eingabekombinationen, die Ihre Invariante nicht erfüllen können. - Schrumpfen (Shrinking): Die Frameworks schrumpfen automatisch fehlgeschlagene Eingaben auf die minimale Reproduktion.
- Dokumentieren Sie Invarianten: Machen Sie deutlich, welche Eigenschaften Ihr Code garantiert.
Beispiel: Property-basiertes Testen mit Hypothesis (Python)
from hypothesis import given, strategies as st
from mymodule import parse_config
# Eigenschaft: Parsen einer gültigen Konfiguration sollte immer erfolgreich sein
@given(st.dictionaries(
keys=st.text(min_size=1, max_size=10),
values=st.integers(min_value=0, max_value=1000)
))
result = parse_config(config)
assert result is not None (config):
assert isinstance(result, dict)
for key, value in result.items():
assert key in config
assert value == config[key] * 2
Beispiel: Fuzz-Testing in Solidity (Foundry)
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
import "forge-std/Test.sol";
contract MyTokenTest is Test {
MyToken token;
function setUp() public {
token = new MyToken();
}
// Invariante: Der Gesamtvorrat sollte nach jeder Transaktion gleich der Summe aller Salden sein
function invariant_totalSupply() public {
assertEq(token.totalSupply(), token.balanceOf(address(this)));
}
// Fuzz-Test: Überweisungen sollten nie den Gesamtvorrat ändern
function testFuzz_transfer(address recipient, uint256 amount) public {
token.transfer(recipient, amount);
assertEq(token.totalSupply(), token.balanceOf(address(this)) + token.balanceOf(recipient));
}
}