Este artículo describirá cómo crear pruebas unitarias en Solidity utilizando Foundry. Cubriremos cómo probar todas las transiciones de estado que pueden ocurrir en un smart contract, además de algunas características útiles adicionales que proporciona Foundry. Foundry tiene capacidades de prueba muy extensas, por lo que en lugar de repetir la documentación, nos centraremos en las partes que utilizará la mayor parte del tiempo.
Este artículo asume que ya se siente cómodo con Solidity. Si no es así, consulte nuestro tutorial gratuito para aprender Solidity.
Instalar Foundry
Si aún no tiene Foundry instalado, siga las instrucciones aquí: https://book.getfoundry.sh/getting-started/installation
Autoría
Este artículo fue coescrito por Aymeric Taylor (LinkedIn, Twitter), un pasante de investigación en RareSkills.
Hola Mundo en Foundry
Simplemente ejecute los siguientes comandos y configurará el entorno, creará pruebas y las ejecutará por usted. (Esto asume que tiene Foundry instalado, por supuesto).
forge init
forge test
Mejores prácticas para pruebas en Solidity
Independientemente del framework, la calidad de las pruebas unitarias en Solidity depende de tres factores:
- Cobertura de líneas
- Cobertura de ramas, y
- Transiciones de estado completamente definidas.
Al comprender cada uno de estos, podemos justificar por qué nos enfocamos en ciertos aspectos de la API de Foundry.
Por supuesto, no es posible documentar cada rango de entrada para cada posible salida. Sin embargo, la calidad de las pruebas generalmente estará correlacionada con la cobertura de líneas, la cobertura de ramas y la definición de transiciones de estado. En nuestro otro artículo, hemos documentado cómo medir la cobertura de líneas y ramas con Foundry. Explicaremos la importancia de las tres métricas aquí:
1. Cobertura de líneas
La cobertura de líneas es justo lo que parece. Si una línea de código no se ejecutó durante las pruebas, entonces la cobertura de líneas no es del 100%. Si una línea nunca se ejecutó, no puede estar seguro de si funciona como se espera o si provocará un revert. No hay una buena razón para no tener una cobertura de líneas del 100% en un smart contract. Si está escribiendo código, significa que espera que se ejecute en algún momento en el futuro, así que ¿por qué no probarlo?
2. Cobertura de ramas
Incluso si se ejecuta cada línea, no significa que se pruebe cada variación en la lógica de negocio del smart contract.
Considere la siguiente función
function changeOwner(address newOwner) external {
require(msg.sender == owner, "onlyOwner");
owner = newOwner;
}
Si prueba esta dirección llamándola con el owner, obtendrá una cobertura de líneas del 100% pero no una cobertura de ramas del 100%. Eso se debe a que tanto la declaración require como la asignación del owner se ejecutaron, pero el caso donde el require hace revert no se probó.
Aquí hay un ejemplo más sutil.
// @notice anyone can pay off someone else's loan
// @param debtor the person who's loan the sender is making a payment for
function payDownLoan(address debtor) external payable {
uint256 loanAmount = loanAmounts[debtor];
require(loanAmount > 0, "no such loan");
if (msg.value >= debtAmount {
loanAmounts[debtor] = 0;
emit LoanFullyRepaid(debtor);
} else {
emit LoanPayment(debtor, debtAmount, msg.value);
loanAmount -= msg.value;
}
if (msg.value > loanAmount) {
msg.sender.call{value: msg.value - loanAmount}("");
}
}
¿Cuántas ramas hay que probar en este caso?
- El caso donde el préstamo es cero
- El caso donde alguien paga menos que el tamaño del préstamo
- El caso donde alguien paga exactamente el tamaño del préstamo
- El caso donde alguien paga más que el tamaño del préstamo
Es posible obtener una cobertura de líneas del 100% en esta prueba enviando más ether que el tamaño del préstamo y menos ether que el tamaño del préstamo. Esto ejecutaría ambas ramas del if else, y la declaración if final al final. Pero esto no probaría la declaración else donde el préstamo se paga exactamente hasta llegar a cero.
Cuantas más ramas tengan sus funciones, exponencialmente más difícil se vuelve realizar pruebas unitarias sobre ellas. El término técnico para esto es complejidad ciclomática.
3. Transiciones de estado completamente definidas
Las pruebas unitarias de calidad en Solidity documentan las transiciones de estado tan exhaustivamente como sea posible. Las transiciones de estado incluyen:
- un cambio en las variables de almacenamiento (storage)
- contratos siendo desplegados o autodestruidos
- balances de ether cambiando
- eventos siendo emitidos, con ciertos mensajes
- transacciones haciendo revert, con ciertos mensajes de error
Si una función hace cualquiera de estas cosas, la forma exacta en que modifica el estado debe ser capturada en las pruebas unitarias y cualquier desviación debería causar un revert. De esta manera, cualquier modificación accidental, sin importar cuán menor sea, será detectada automáticamente.
Volviendo al ejemplo anterior, ¿qué transiciones de estado se deberían medir?
- el Ether en el contrato aumenta en la misma cantidad que el prestatario devuelve del préstamo
- la variable de storage que rastrea el tamaño del préstamo se reduce en la cantidad esperada
- el revert ocurre con el mensaje de error esperado cuando el remitente paga por un préstamo inexistente
- se emiten los eventos correspondientes y los mensajes asociados
Si la lógica de negocio en su smart contract cambia, las pruebas deberían fallar. Normalmente, esto se considera una prueba unitaria “frágil” en otros dominios. Puede afectar la velocidad de iteración en el código fuente. Pero el código Solidity está destinado a ser escrito una vez y nunca modificado, por lo que esto no es un problema para las pruebas de smart contracts.
4. Conclusión de mejores prácticas para pruebas unitarias
¿Por qué estamos cubriendo todo esto antes de documentar cómo funcionan las pruebas unitarias de Foundry? Porque esto nos ayudará a aislar las utilidades de prueba de alto impacto que utilizará la mayor parte del tiempo. Las capacidades de Foundry son vastas, pero solo un pequeño subconjunto se utilizará en la mayoría de los casos de prueba.
Asserts en Foundry
Para asegurarse de que una transición de estado realmente ocurrió, necesitará asserts.
Comencemos con el archivo de prueba por defecto que Foundry proporciona después de llamar a forge init.
// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.13;
import "forge-std/Test.sol";
import "../src/Counter.sol";
contract CounterTest is Test {
Counter public counter;
function setUp() public {
counter = new Counter();
counter.setNumber(0);
}
function testIncrement() public {
counter.increment();
assertEq(counter.number(), 1);
}
function testSetNumber(uint256 x) public {
counter.setNumber(x);
assertEq(counter.number(), x);
}
}
La función setUp() despliega el contrato que está probando (así como cualquier otro contrato que desee en el ecosistema).
Cualquier función que comience con la palabra test se ejecutará como una prueba unitaria. Las funciones que no comiencen con test no se ejecutarán a menos que una función test o setUp las llame.
Aquí están los asserts que tiene a su disposición.
Los que utilizaría con más frecuencia son
assertEq, assert equal (afirmar igualdad)assertLt, assert less than (afirmar menor que)assertLe, assert less than or equal to (afirmar menor o igual a)assertGt, assert greater than (afirmar mayor que)assertGe, assert greater than or equal to (afirmar mayor o igual a)assertTrue, assert to be true (afirmar que es verdadero)
Los primeros dos argumentos del assert son la comparación, pero también puede agregar un mensaje de error útil como tercer argumento, lo cual siempre debería hacer (a pesar de que el ejemplo predeterminado no lo muestra). Esta es la forma sugerida de escribir aserciones:
function testIncrement() public {
counter.increment();
assertEq(counter.number(), 1, "expect x to equal to 1");
}
function testSetNumber(uint256 x) public {
counter.setNumber(x);
assertEq(counter.number(), x, "x should be setNumber");
}
Cambiando msg.sender con vm.prank de Foundry
El método bastante humorístico de Foundry para cambiar el remitente (cuenta o billetera) es la API vm.prank (lo que Foundry llama un cheatcode).
Aquí hay un ejemplo mínimo
function testChangeOwner() public {
vm.prank(owner);
contractToTest.changeOwner(newOwner);
assertEq(contractToTest.owner(), newOwner);
}
vm.prank solo funciona para la transacción que ocurre inmediatamente después. Si desea que una secuencia de transacciones utilice la misma dirección, use vm.startPrank y finalícelas con vm.stopPrank.
function testMultipleTransactions() public {
vm.startPrank(owner);
// behave as owner
vm.stopPrank();
}
Definiendo cuentas y direcciones en Foundry
La variable owner anterior se puede definir de varias maneras:
// an address created by casting a decimal to an address
address owner = address(1234);
// vitalik's addresss
address owner = 0x0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045;
// create an address from a known private key;
address owner = vm.addr(privateKey);
// create an attacker
address hacker = 0x00baddad
Prank a msg.sender y tx.origin
En los ejemplos anteriores, se altera msg.sender. Si desea específicamente control tanto sobre tx.origin como sobre msg.sender, tanto vm.prank como vm.startPrank toman opcionalmente dos argumentos donde el segundo argumento es tx.origin.
vm.prank(msgSender, txOrigin);
Depender de tx.origin es generalmente una mala práctica, por lo que rara vez necesitará usar la versión de dos argumentos de vm.prank.
Comprobando balances
Cuando transfiere ether, debe medir que los balances hayan cambiado como se esperaba. Afortunadamente, comprobar los balances en Foundry es fácil, ya que está escrito en Solidity.
Considere este contrato:
contract Deposit {
event Deposited(address indexed);
function buyerDeposit() external payable {
require(msg.value == 1 ether, "incorrect amount");
emit Deposited(msg.sender);
}
// rest of the logic
}
La función de prueba se vería así:
function testBuyerDeposit() public {
uint256 balanceBefore = address(depositContract).balance;
depositContract.buyerDeposit{value: 1 ether}();
uint256 balanceAfter = address(depositContract).balance;
assertEq(balanceAfter - balanceBefore, 1 ether, "expect increase of 1 ether");
}
Tenga en cuenta que no hemos probado los casos en los que el comprador envió una cantidad diferente a 1 ether, lo que causaría un revert. Hablaremos sobre cómo probar los reverts en la siguiente sección.
Esperando reverts con vm.expectRevert
El problema con la prueba anterior en su forma actual es que podría eliminar la declaración require y la prueba aún pasaría. Mejoremos la prueba para que eliminar la declaración require haga que una prueba falle.
function testBuyerDepositWrongPrice() public {
vm.expectRevert("incorrect amount");
depositContract.deposit{value: 1 ether + 1 wei}();
vm.expectRevert("incorrect amount");
depositContract.deposit{value: 1 ether - 1 wei}();
}
Tenga en cuenta que vm.expectRevert debe llamarse justo antes de ejecutar la función que esperamos que haga revert. Ahora bien, si eliminamos la declaración require, hará revert, por lo que hemos modelado mejor la funcionalidad prevista del smart contract.
Probando Custom Errors
Si usamos custom errors (errores personalizados) en lugar de declaraciones require, la forma de probar el revert sería la siguiente:
contract CustomErrorContract {
error SomeError(uint256);
function revertError(uint256 x) public pure {
revert SomeError(x);
}
}
Y el archivo de prueba sería así:
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.13;
import "forge-std/Test.sol";
import "../src/RevertCustomError.sol";
contract CounterTest is Test {
CustomErrorContract public customErrorContract;
error SomeError(uint256);
function setUp() public {
customErrorContract = new CustomErrorContract();
}
function testRevert() public {
// 5 is an arbitrary example
vm.expectRevert(abi.encodeWithSelector(SomeError.selector, 5));
customErrorContract.revertError(5);
}
}
En nuestro ejemplo, hemos creado un custom error parametrizado. Para que la prueba pase, el parámetro debe ser igual al que realmente se usó durante el revert.
Probando logs y eventos con vm.expectEvent
Aunque los eventos de Solidity no alteran la funcionalidad de un smart contract, implementarlos incorrectamente puede romper las aplicaciones cliente que leen el estado de un smart contract. Para asegurarnos de que nuestros eventos funcionen como se espera, podemos usar vm.expectEmit. Esta API se comporta de manera bastante contraintuitiva porque debe emitir el evento en la prueba para asegurarse de que funcionó en el smart contract.
Aquí hay un ejemplo mínimo.
function testBuyerDepositEvent() public {
vm.expectEmit();
emit Deposited(buyer);
depositContract.deposit{value: 1 ether}();
}
Ajustando block.timestamp con vm.warp
Ahora consideremos un retiro bloqueado por tiempo (time locked withdrawal). El vendedor puede retirar el pago después de 3 días.
contract Deposit {
address public seller;
mapping(address => uint256) public depositTime;
event Deposited(address indexed);
event SellerWithdraw(address indexed, uint256 indexed);
constructor(address _seller) {
seller = _seller;
}
function buyerDeposit() external payable {
require(msg.value == 1 ether, "incorrect amount");
uint256 _depositTime = depositTime[msg.sender];
require(_depositTime == 0, "already deposited");
depositTime[msg.sender] = block.timestamp;
emit Deposited(msg.sender);
}
function sellerWithdraw(address buyer) external {
require(msg.sender == seller, "not the seller");
uint256 _depositTime = depositTime[buyer];
require(_depositTime != 0, "buyer did not deposit");
require(block.timestamp - _depositTime > 3 days, "refund period not passed");
delete depositTime[buyer];
emit SellerWithdraw(buyer, block.timestamp);
(bool ok, ) = msg.sender.call{value: 1 ether}("");
require(ok, "seller did not withdraw");
}
}
Hemos añadido mucha funcionalidad que necesita ser probada, pero concentrémonos en el aspecto del tiempo por ahora.
Queremos probar que el vendedor no puede retirar el dinero hasta que pasen 3 días desde el depósito. (Obviamente falta una función para que el comprador pueda retirar antes de esa ventana de tiempo, pero llegaremos a eso más adelante).
Tenga en cuenta que block.timestamp comienza en 1 por defecto. Este no es un número realista contra el cual realizar pruebas, por lo que primero deberíamos avanzar hacia el día presente (warp).
Esto se puede hacer con vm.warp(x), pero seamos elegantes y usemos un modificador.
// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.13;
import "forge-std/Test.sol";
import "../src/Deposit.sol";
contract DepositTest is Test {
Deposit public deposit;
Deposit public faildeposit;
address constant SELLER = address(0x5E11E7);
//address constant Rejector = address(RejectTransaction);
RejectTransaction private rejector;
event Deposited(address indexed);
event SellerWithdraw(address indexed, uint256 indexed);
function setUp() public {
deposit = new Deposit(SELLER);
rejector = new RejectTransaction();
faildeposit = new Deposit(address(rejector));
}
modifier startAtPresentDay() {
vm.warp(1680616584);
_;
}
address public buyer = address(this); // the DepositTest contract is the "buyer"
address public buyer2 = address(0x5E11E1); // random address
address public FakeSELLER = address(0x5E1222); // random address
function testDepositAmount() public startAtPresentDay {
// this test checks that the buyer can only deposit 1 ether
vm.startPrank(buyer);
vm.expectRevert();
deposit.buyerDeposit{value: 1.5 ether}();
vm.expectRevert();
deposit.buyerDeposit{value: 2.5 ether}();
vm.stopPrank();
}
}
Ajustando block.number con vm.roll
Si desea ajustar el número de bloque (block.number) en Foundry, use
vm.roll(blockNumber)
Para cambiar el número de bloque. Para avanzar un cierto número de bloques, haga lo siguiente
vm.roll(block.number() + numberOfBlocks)
Agregando las pruebas adicionales
En aras de la exhaustividad, escribamos las pruebas unitarias para el resto de las funciones.
Algunas características adicionales necesitan ser probadas para la función de depósito:
- la variable pública
depositTimecoincide con el tiempo de la transacción - un usuario no puede depositar dos veces
Y para la función del vendedor:
- el vendedor no puede retirar para direcciones inexistentes
- la entrada para el comprador es eliminada (esto permite que el comprador compre de nuevo)
- se emite el evento
SellerWithdraw - el balance del contrato disminuye en 1 ether
- una dirección que no es el vendedor llamando a
sellerWithdrawhace revert
// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.13;
import "forge-std/Test.sol";
import "../src/Deposit.sol";
contract DepositTest is Test {
Deposit public deposit;
Deposit public faildeposit;
address constant SELLER = address(0x5E11E7);
//address constant Rejector = address(RejectTransaction);
RejectTransaction private rejector;
event Deposited(address indexed);
event SellerWithdraw(address indexed, uint256 indexed);
function setUp() public {
deposit = new Deposit(SELLER);
rejector = new RejectTransaction();
faildeposit = new Deposit(address(rejector));
}
modifier startAtPresentDay() {
vm.warp(1680616584);
_;
}
address public buyer = address(this); // the DepositTest contract is the "buyer"
address public buyer2 = address(0x5E11E1); // random address
address public FakeSELLER = address(0x5E1222); // random address
function testDepositAmount() public startAtPresentDay {
// this test checks that the buyer can only deposit 1 ether
vm.startPrank(buyer);
vm.expectRevert();
deposit.buyerDeposit{value: 1.5 ether}();
vm.expectRevert();
deposit.buyerDeposit{value: 2.5 ether}();
vm.stopPrank();
}
function testBuyerDepositSellerWithdrawAfter3days() public startAtPresentDay {
// This test checks that the seller is able to withdraw 3 days after the buyer deposits
// buyer deposits 1 ether
vm.startPrank(buyer); // msg.sender == buyer
deposit.buyerDeposit{value: 1 ether}();
assertEq(address(deposit).balance, 1 ether, "Contract balance did not increase"); // checks to see if the contract balance increases
vm.stopPrank();
// after three days the seller withdraws
vm.startPrank(SELLER); // msg.sender == SELLER
vm.warp(1680616584 + 3 days + 1 seconds);
deposit.sellerWithdraw(address(this));
assertEq(address(deposit).balance, 0 ether, "Contract balance did not decrease"); // checks to see if the contract balance decreases
}
function testBuyerDepositSellerWithdrawBefore3days() public startAtPresentDay {
// This test checks that the seller is able to withdraw 3 days after the buyer deposits
// buyer deposits 1 ether
vm.startPrank(buyer); // msg.sender == buyer
deposit.buyerDeposit{value: 1 ether}();
assertEq(address(deposit).balance, 1 ether, "Contract balance did not increase"); // checks to see if the contract balance increases
vm.stopPrank();
// before three days the seller withdraws
vm.startPrank(SELLER); // msg.sender == SELLER
vm.warp(1680616584 + 2 days);
vm.expectRevert(); // expects a revert
deposit.sellerWithdraw(address(this));
}
function testdepositTimeMatchesTimeofTransaction() public startAtPresentDay {
// This test checks that the public variable depositTime matches the time of the transaction
vm.startPrank(buyer); // msg.sender == buyer
deposit.buyerDeposit{value: 1 ether}();
// check that it deposits at the right time
assertEq(
deposit.depositTime(buyer),
1680616584, // time of startAtPresentDay
"Time of Deposit Doesnt Match"
);
vm.stopPrank();
}
function testUserDepositTwice() public startAtPresentDay {
// This test checks that a user cannot deposit twice
vm.startPrank(buyer); // msg.sender == buyer
deposit.buyerDeposit{value: 1 ether}();
vm.warp(1680616584 + 1 days); // one day later...
vm.expectRevert();
deposit.buyerDeposit{value: 1 ether}(); // should revert since it hasn't been 3 days
}
function testNonExistantContract() public startAtPresentDay {
// This test checks that the seller cannot withdraw for non-existent addresses
vm.startPrank(SELLER); // msg.sender == SELLER
vm.expectRevert();
deposit.sellerWithdraw(buyer);
}
function testBuyerBuysAgain() public startAtPresentDay {
// This test checks that the entry for the buyer is deleted (this allows the buyer to buy again)
vm.startPrank(buyer); // msg.sender == buyer
deposit.buyerDeposit{value: 1 ether}();
vm.stopPrank();
// seller withdraws
vm.warp(1680616584 + 3 days + 1 seconds);
vm.startPrank(SELLER); // msg.sender == SELLER
deposit.sellerWithdraw(buyer);
vm.stopPrank();
// checks depostitime[buyer] == 0
assertEq(deposit.depositTime(buyer), 0, "entry for buyer is not deleted");
// buyer deposits again
vm.startPrank(buyer); // msg.sender == buyer
vm.expectEmit();
emit Deposited(buyer);
deposit.buyerDeposit{value: 1 ether}();
vm.stopPrank();
}
function testSellerWithdrawEmitted() public startAtPresentDay {
// this test checks that the SellerWithdraw event is emitted
//buyer2 deposits
vm.deal(buyer2, 1 ether); // msg.sender == buyer2
vm.startPrank(buyer2);
vm.expectEmit(); // Deposited Emitter checked
emit Deposited(buyer2);
deposit.buyerDeposit{value: 1 ether}();
vm.stopPrank();
vm.warp(1680616584 + 3 days + 1 seconds);// 3 day and 1 second later...
// seller withdraws + checks SellerWithdraw event emmited or not
vm.startPrank(SELLER); // msg.sender == SELLER
vm.expectEmit(); // expects SellerWithdraw Emitterd
emit SellerWithdraw(buyer2, block.timestamp);
deposit.sellerWithdraw(buyer2);
vm.stopPrank();
}
function testFakeSeller2Withdraw() public startAtPresentDay {
// buyer deposits
vm.startPrank(buyer);
vm.deal(buyer, 2 ether); // this contract's address is the buyer
deposit.buyerDeposit{value: 1 ether}();
vm.stopPrank();
assertEq(address(deposit).balance, 1 ether, "Ether deposited somehow failed");
vm.warp(1680616584 + 3 days + 1 seconds); // 3 day and 1 second later...
vm.startPrank(FakeSELLER); // msg.sender == FakeSELLER
vm.expectRevert();
deposit.sellerWithdraw(buyer);
vm.stopPrank();
}
function testRejectedWithdrawl() public startAtPresentDay {
// This test checks that the entry for the buyer is deleted (this allows the buyer to buy again)
vm.startPrank(buyer); // msg.sender == buyer
faildeposit.buyerDeposit{value: 1 ether}();
vm.stopPrank();
assertEq(address(faildeposit).balance, 1 ether, "assertion failed");
vm.warp(1680616584 + 3 days + 1 seconds); // 3 days and 1 second later...
vm.startPrank(address(rejector)); // msg.sender == rejector
vm.expectRevert();
faildeposit.sellerWithdraw(buyer);
vm.stopPrank();
}
}
Probando transferencias de ether fallidas
Probar el retiro del comprador requiere un truco adicional para obtener una cobertura de líneas completa. Aquí está el fragmento de código que estamos probando, y explicaremos el contrato Rejector en el código de arriba.
function buyerWithdraw() external {
uint256 _depositTime = depositTime[msg.sender];
require(_depositTime != 0, "sender did not deposit");
require(block.timestamp - _depositTime <= 3 days);
emit BuyerRefunded(msg.sender, block.timestamp);
// this is the branch we are testing
(bool ok,) = msg.sender.call{value: 1 ether}("");
require(ok, "Failed to withdraw");
}
Para probar la condición de fallo de require(ok…) necesitamos que la transferencia de Ether falle. La forma en que la prueba logra esto es creando un smart contract que llama a la función buyerWithdraw, pero tiene su función receive configurada para hacer revert.
Fuzzing en Foundry
Aunque podemos especificar una dirección arbitraria que no sea la del vendedor para probar el revert de un retiro con dirección no autorizada, es mentalmente más tranquilizador probar muchos valores diferentes.
Si suministramos un argumento a las funciones de prueba, Foundry probará un montón de valores diferentes para los argumentos. Para evitar que use argumentos que no se aplican al caso de prueba (como cuando la dirección está autorizada), usaríamos vm.assume. Así es como podemos probar el retiro del vendedor para un vendedor no autorizado.
// notSeller will be chosen randomly
function testInvalidSellerAddress(address notSeller) public {
vm.assume(notSeller != seller);
vm.expectRevert("not the seller");
depositContract.sellerWithdraw(notSeller);
}
Aquí están todas las transiciones de estado
- El
balancedel contrato disminuye en 1 ether - Se emitió el evento
BuyerRefunded - El comprador puede realizar un reembolso antes de tres días
Aquí están las ramas que necesitan ser probadas
- el comprador no puede retirar después de 3 días
- El comprador no puede retirar si nunca depositó
Console.log en Foundry
Para hacer console.log en Foundry, importe lo siguiente
import "forge-std/console.sol";
Y ejecute la prueba con
forge test -vv
Probando firmas
Consulte nuestro tutorial sobre verificación de firmas en Solidity con Foundry, por lo que lo remitimos allí.
Probando funciones internas en Solidity
Consulte nuestro tutorial sobre cómo probar funciones internas en Solidity.
Estableciendo balances de direcciones con vm.deal y vm.hoax
El cheatcode vm.hoax le permite hacer prank a una dirección y establecer su balance simultáneamente.
vm.hoax(addressToPrank, balanceToGive);
// next call is a prank for addressToPrank
vm.deal(alice, balanceToGive);
Algunos errores comunes con Foundry
No tener una función fallback al recibir Ether
Si está probando retirar Ether del contrato, se enviará al contrato que está ejecutando las pruebas. Las pruebas de Foundry en sí mismas son un smart contract, y si envía Ether a un smart contract que no tiene una función fallback o receive, la transacción fallará. Asegúrese de tener una función fallback o receive en el contrato.
No tener un onERC…Received al recibir tokens
Por la misma razón (y valga el juego de palabras), el safeTransferFrom de ERC-721 y el transferFrom de ERC-1155 hacen revert al enviar tokens a un smart contract que no tiene la función hook de transferencia adecuada. Necesitará agregar esto a sus pruebas si desea probar la transferencia de NFTs (o tokens tipo ERC777) hacia usted mismo.
Resumen
- apunte a una cobertura de líneas y ramas del 100%
- defina completamente las transiciones de estado esperadas
- use mensajes de error en sus asserts
Aprender más sobre pruebas
Para aprender sobre pruebas avanzadas en Solidity más allá de las pruebas unitarias y el fuzzing básico, consulte nuestro Solidity Bootcamp avanzado.
Publicado originalmente el 11 de abril de 2023