Como fazer mock de contratos inteligentes em Solidity para testes
Objetos mock (opens in a new tab) são um padrão de projeto comum na programação orientada a objetos. Vindo da antiga palavra francesa 'mocquer' com o significado de 'zombar', evoluiu para 'imitar algo real', que é exatamente o que estamos fazendo na programação. Por favor, só zombe dos seus contratos inteligentes se você quiser, mas faça mock deles sempre que puder. Isso facilita a sua vida.
Testes unitários de contratos com mocks
Fazer o mock de um contrato significa essencialmente criar uma segunda versão desse contrato que se comporta de maneira muito semelhante à original, mas de uma forma que pode ser facilmente controlada pelo desenvolvedor. Muitas vezes você acaba com contratos complexos onde você só quer fazer testes unitários de pequenas partes do contrato. O problema é: e se testar essa pequena parte exigir um estado de contrato muito específico que é difícil de alcançar?
Você poderia escrever uma lógica complexa de configuração de teste toda vez que colocar o contrato no estado necessário ou você escreve um mock. Fazer mock de um contrato é fácil com herança. Basta criar um segundo contrato mock que herda do original. Agora você pode sobrescrever funções para o seu mock. Vamos ver isso com um exemplo.
Exemplo: ERC-20 Privado
Usamos um contrato ERC-20 de exemplo que tem um tempo privado inicial. O proprietário pode gerenciar usuários privados e apenas esses terão permissão para receber tokens no início. Uma vez que um certo tempo tenha passado, todos terão permissão para usar os tokens. Se você estiver curioso, estamos usando o hook _beforeTokenTransfer (opens in a new tab) dos novos contratos v3 da OpenZeppelin.
pragma solidity ^0.6.0;
import "@openzeppelin/contracts/token/ERC20/ERC20.sol";
import "@openzeppelin/contracts/access/Ownable.sol";
contract PrivateERC20 is ERC20, Ownable {
mapping (address => bool) public isPrivateUser;
uint256 private publicAfterTime;
constructor(uint256 privateERC20timeInSec) ERC20("PrivateERC20", "PRIV") public {
publicAfterTime = now + privateERC20timeInSec;
}
function addUser(address user) external onlyOwner {
isPrivateUser[user] = true;
}
function isPublic() public view returns (bool) {
return now >= publicAfterTime;
}
function _beforeTokenTransfer(address from, address to, uint256 amount) internal virtual override {
super._beforeTokenTransfer(from, to, amount);
require(_validRecipient(to), "PrivateERC20: invalid recipient");
}
function _validRecipient(address to) private view returns (bool) {
if (isPublic()) {
return true;
}
return isPrivateUser[to];
}
}
E agora vamos fazer o mock dele.
pragma solidity ^0.6.0;
import "../PrivateERC20.sol";
contract PrivateERC20Mock is PrivateERC20 {
bool isPublicConfig;
constructor() public PrivateERC20(0) {}
function setIsPublic(bool isPublic) external {
isPublicConfig = isPublic;
}
function isPublic() public view returns (bool) {
return isPublicConfig;
}
}
Você receberá uma das seguintes mensagens de erro:
PrivateERC20Mock.sol: TypeError: Overriding function is missing "override" specifier.PrivateERC20.sol: TypeError: Trying to override non-virtual function. Did you forget to add "virtual"?.
Como estamos usando a nova versão 0.6 da Solidity, temos que adicionar a palavra-chave virtual para funções que podem ser sobrescritas e override para a função que está sobrescrevendo. Então, vamos adicioná-las a ambas as funções isPublic.
Agora, em seus testes unitários, você pode usar PrivateERC20Mock em vez disso. Quando você quiser testar o comportamento durante o tempo de uso privado, use setIsPublic(false) e da mesma forma setIsPublic(true) para testar o tempo de uso público. Claro que em nosso exemplo, poderíamos apenas usar ajudantes de tempo (time helpers) (opens in a new tab) para alterar os tempos de acordo também. Mas a ideia de fazer mock deve estar clara agora e você pode imaginar cenários onde não é tão fácil quanto simplesmente avançar o tempo.
Fazendo mock de muitos contratos
Pode se tornar uma bagunça se você tiver que criar outro contrato para cada mock. Se isso te incomoda, você pode dar uma olhada na biblioteca MockContract (opens in a new tab). Ela permite que você sobrescreva e altere os comportamentos dos contratos dinamicamente (on-the-fly). No entanto, ela funciona apenas para fazer mock de chamadas para outro contrato, então não funcionaria para o nosso exemplo.
Fazer mock pode ser ainda mais poderoso
Os poderes do mock não terminam aí.
- Adicionando funções: Não apenas sobrescrever uma função específica é útil, mas também apenas adicionar funções adicionais. Um bom exemplo para tokens é apenas ter uma função
mintadicional para permitir que qualquer usuário obtenha novos tokens gratuitamente. - Uso em testnets: Quando você implantar e testar seus contratos em testnets junto com seu aplicativo descentralizado (dapp), considere usar uma versão mock. Evite sobrescrever funções a menos que você realmente precise. Afinal, você quer testar a lógica real. Mas adicionar, por exemplo, uma função de redefinição (reset) pode ser útil, pois simplesmente redefine o estado do contrato para o início, sem a necessidade de uma nova implantação. Obviamente, você não gostaria de ter isso em um contrato na Mainnet.