WEB3DEV

Cover image for Teste Solidity Com o Truffle e o Async/Await
Panegali
Panegali

Posted on • Atualizado em

Teste Solidity Com o Truffle e o Async/Await

Se você perguntasse a um programador/desenvolvedor qual é a pior parte do trabalho, acho que a maioria responderia escrever testes. Você precisa criar dados simulados e fazer alteraçoes de codificação para que coisas específicas funcionem corretamente. Além disso, você precisa escrever muitos casos diferentes para um único recurso/função. Leva muito tempo para escrever bons testes. Mas com a introdução do async/await no node 8, escrever testes se torna muito melhor! É o que usamos para criar mais de 100 testes para o HelloSugoi.

Descubra e revise os melhores softwares Blockchain

Público:

Principalmente para desenvolvedores que estão escrevendo aplicativos Ethereum no Solidity e estão escrevendo testes para seu código em Javascript. E para qualquer pessoa curiosa sobre como fazer async/await e como isso pode ajudar a escrever testes melhores.

Requisitos/instalação:

Com o node 8, async/await foi habilitado por padrão. Antes da versão 7.6, você podia usar async/await desde que adicionasse o sinalizador --harmony-async-await. Para a versão anterior, você também pode usar o Babel para transpilar para javascript compatível com ES5. Sugiro instalar o Node 8 ou superior para que este projeto funcione corretamente. Você pode usar homebrew ou nvm para gerenciar a versão que está usando.

Além de ter o node instalado, você precisa ter o Truffle e o Ganache-cli instalados globalmente.

  1. npm install -g truffle
  2. npm install -g ganache-cli

Truffle é usado para compilar e migrar contratos Solidity para redes Ethereum. Ganache-cli é usado como um nó Ethereum falso que retorna imediatamente em vez de 15 segundos. Para confirmar que você tem tudo instalado, tente o seguinte:

  1. Node ≥ 7.6 confirme digitando node -v na linha de comando
  2. npm ≥ 4 confirme digitando npm -v na linha de comando
  3. Truffle ≥ 3.3 confirme digitando truffle -v na linha de comando
  4. Ganache-cli ≥ 5.0.0 confirme digitando ganache-cli -v na linha de comando

Exemplo de configuração do projeto:

Eu tenho um repositório de projeto de exemplo com todo o nosso código. Se você quiser trabalhar nele, por favor clone e instale suas dependências.

  1. git clone https://github.com/sogoiii/trufa-async-await-tests
  2. cd truffle-async-await-tests
  3. npm installou yarn install
  4. truffle compile
  5. truffle test

O quarto comando executa todos os testes e eles devem ser concluídos com êxito.

Como funciona o Async/Await:

Conceitualmente, async/await faz com que o código JavaScript assíncrono pareça um código síncrono. Vamos comparar os dois:

const status = () => {
    return MetaCoin.deployed()
          .then(instance => {
             return instance.getBalance.call(accounts[0]);
          })
          .then(balance => {
             assert.equal(balance.valueOf(), 10000, "10000 não estava na primeira conta");
          })
}

const status2 = function async () {
    let meta = await MetaCoin.deployed();
    let balance = await meta.getBalance.call(accounts[0]);
    assert.equal(balance.valueOf(), 10000, "10000 não estava na primeira conta")
}
Enter fullscreen mode Exit fullscreen mode

Comparação simples do teste truffle como cadeia de promise ou async/await

A partir do exemplo acima, acho que o método status2 parece mais limpo que o método status. Ele literalmente tem 4 linhas de código pouco expressivo, que é mais do que as 3 linhas de lógica em status2. Acho fácil a escolha!

Para usar async/await, você precisa adicionar o moniker async à declaração da função. Isso diz ao interpretador v8 para procurar a chave await, caso contrário, o nó gera um erro perguntando o que é await. Você coloca await na frente de qualquer função/método promise (é um objeto usado para processamento assíncrono. Uma Promise (de "promessa") representa um valor que pode estar disponível agora, no futuro ou nunca), não um retorno de chamada! Se sua função é um retorno de chamada, sugiro usar um pacote como bluebird ou o método nativo promisify que foi introduzido no Node 8. Felizmente, Truffle retorna promises em vez de retornos de chamada.

Uma função assíncrona (async function(){}) também retorna uma promise! Isso é bom porque você pode encadear _promises _ou funções assíncronas, se desejar. Como no exemplo abaixo:

const operationOne = function async (data) {
  let step1 = await somePromise(data)
  let step2 = await anotherPromise(step1)
  return step2.data
}
actionOne()
  .then(operationOne)
  .then(data => {
    //more work
  })
Enter fullscreen mode Exit fullscreen mode

Async retorna uma promise

Um tempo atrás, o async/await teve um desempenho muito grande. Era grande o suficiente para que não usar async/await fosse razoável. Desde o lançamento da v8 5.5, o desempenho só melhorou. A menos que você esteja fazendo algo computacionalmente pesado, usar async/await pode ser uma boa alternativa.

Exemplo (Caso Fácil):

Truffle é legal porque .deployed retorna uma promise. Lembre-se, só posicionar await na frente das promises!

Abra o arquivo test/metacoin.js e veja o primeiro teste.

var MetaCoin = artifacts.require("./MetaCoin.sol");
// ... mais código
contract('MetaCoin', function(accounts) {

  it("deve colocar 10000 MetaCoin na primeira conta", async function() {
    let meta = await MetaCoin.deployed();
    let balance = await meta.getBalance.call(accounts[0]);
    assert.equal(balance.valueOf(), 10000, "10000 não estava na primeira conta")
  });
   // ... mais código
})
Enter fullscreen mode Exit fullscreen mode

Exemplo de teste fácil Truffle

O primeiro teste faz apenas 3 operações. A primeira é obter o objeto JavaScript do contrato implantado e defini-lo para a variável meta. Em seguida, ele chama a função de getBalance.call(), função por trás da palavra chave await. Novamente, isso ocorre porque .call retorna uma promise. Por fim, simplesmente verificamos se a função retornou o valor esperado dentro de um assert. Mole-mole! Toda a sua verificação é se o saldo de metaé 1000conforme o título do teste.

Ao executar truffle test test/metacoin.jsna linha de comando, você verá como todos os testes são executados com êxito.

Exemplo (caso de erro):

Para capturar erros com async/await, você precisa encapsular a chamada dentro de um try/catch.

it("deve falhar porque a função não existe no contrato", async function () {
    let meta = await MetaCoin.deployed();
    try {
      await meta.someNonExistentFn.call();
    } catch (e) {
      return true;
    }
    throw new Error("Eu nunca deveria ver isto!")
  })
Enter fullscreen mode Exit fullscreen mode

Exemplo de teste de falha no Truffle

No teste acima, estou chamando uma função que sei que não existe no contrato Solidity. Await lançará um erro que é capturado no try/catch. Como quero verificar se a chamada someNonExistentFn gera um erro, retorno true dentro do catch para sair do teste. Se await meta.someNonExistentFn() fosse bem-sucedido, o bloco catch não seria chamado e a próxima linha throw new Error(...) seria atingida. Dizendo-me assim que a função dele falhou em lançar.

Exemplo (viagem no tempo):

Às vezes, um contrato Solidity pode ter uma função que só funciona quando chamada no futuro. Em MetaCoin.sol eu adiciono o modificadorde função onlyAfterDate para specialFnonde eu defino quando no futuro uma função pode ser chamada.

contract MetaCoin {
 //...Código
 uint endTime;
 //...Código
 modifier onlyAfterDate() {
  if(now <= endTime) {
   throw;
  }
  _;
 }
 //...Código
 function MetaCoin() {
  balances[tx.origin] = 10000;
  endTime = 1 days; // I set endTime
 }
 //...Código
 function specialFn() onlyAfterDate returns(bool) { // adicionar modificador
  return true;
 }
}
Enter fullscreen mode Exit fullscreen mode

MetaCoin.sol apenas com informações de tempo

Dentro do construtor MetaCoin, defini manualmente endTime como 1 days. As variáveis ​​de tempo no Solidity são uint de segundos de era. O modificador de função onlyAfterDate usa endTime, o que significa que qualquer função com onlyAfterDate só poderá ser chamada em 1 days.

Para testar essa funcionalidade, não vamos esperar 1 dia inteiro! E o que acontece quando o tempo é mais dinâmico, em vez de codificado? Ganache-cli adicionou um método RPC chamadoevm_increaseTime que viajará no tempo pela blockchain.

const timeTravel = function (time) {
  return new Promise((resolve, reject) => {
    web3.currentProvider.sendAsync({
      jsonrpc: "2.0",
      method: "evm_increaseTime",
      params: [time], // 86400 é o número de segundos no dia
      id: new Date().getTime()
    }, (err, result) => {
      if(err){ return reject(err) }
      return resolve(result)
    });
  })
}
Enter fullscreen mode Exit fullscreen mode

Ganache-cli evm_increaseTime como uma promise

Tudo o que temos a fazer é realizar uma chamada RPC para a web3 com evn_increaseTime e parâmetros. Eu embrulhei a web3 baseada em callback em uma promise para que pudéssemos usá-la com async/await.

Com essa nova função, nós simplesmente esperamos um tempo e boom! Alguns dias depois! Se você estiver no Testrpc 4.0.0 (e versão mais antiga do ganache-cli), há um bug em que um bloco deve ser minerado paraevm_timeTravel entrar efeito. Você pode revisar o bug aqui.

it("deve chamar specialFn com sucesso porque passou tempo suficiente", async function () {
    let meta = await MetaCoin.new();
    await timeTravel(86400 * 3) //3 days later
    await mineBlock() // workaround for https://github.com/ethereumjs/testrpc/issues/336
    let status = await meta.specialFn.call();
    assert.equal(status, true, "specialFn deve ser chamado após 1 dia")
})
Enter fullscreen mode Exit fullscreen mode

Viagem no tempo bem sucedida com Truffle e Ganache-cli

Grande Comparação:

Os dois trechos abaixo testam exatamente a mesma coisa. A única diferença é que um é escrito como uma cadeia de promise e o outro com async/await. Apenas conduzindo o ponto para casa!

it("deve enviar as moedas corretamente", function() {
    var meta;

    //    Obter saldos iniciais da primeira e segunda conta.
    var account_one = accounts[0];
    var account_two = accounts[1];

    var account_one_starting_balance;
    var account_two_starting_balance;
    var account_one_ending_balance;
    var account_two_ending_balance;

    var amount = 10;

    return MetaCoin.deployed().then(function(instance) {
      meta = instance;
      return meta.getBalance.call(account_one);
    }).then(function(balance) {
      account_one_starting_balance = balance.toNumber();
      return meta.getBalance.call(account_two);
    }).then(function(balance) {
      account_two_starting_balance = balance.toNumber();
      return meta.sendCoin(account_two, amount, {from: account_one});
    }).then(function() {
      return meta.getBalance.call(account_one);
    }).then(function(balance) {
      account_one_ending_balance = balance.toNumber();
      return meta.getBalance.call(account_two);
    }).then(function(balance) {
      account_two_ending_balance = balance.toNumber();

      assert.equal(account_one_ending_balance, account_one_starting_balance - amount, "A quantidade não foi retirada corretamente do remetente");
      assert.equal(account_two_ending_balance, account_two_starting_balance + amount, "A quantia não foi enviada corretamente para o receptor");
    });
  });
Enter fullscreen mode Exit fullscreen mode

Grande Teste Truffle em Cadeia de Promise

 it("deve enviar as moedas corretamente", async function () {
    // Obter saldos iniciais da primeira e segunda conta.
    var account_one = accounts[0];
    var account_two = accounts[1];

    var amount = 10;

    let meta = await MetaCoin.deployed();
    let balance1 = await meta.getBalance.call(account_one);
    let balance2 = await meta.getBalance.call(account_two);

    let account_one_starting_balance = balance1.toNumber();
    let account_two_starting_balance = balance2.toNumber();

    await meta.sendCoin(account_two, amount, {from: account_one});

    let balance3 = await meta.getBalance.call(account_one);
    let balance4 = await meta.getBalance.call(account_two);

    let account_one_ending_balance = balance3.toNumber();
    let account_two_ending_balance = balance4.toNumber();

    assert.equal(account_one_ending_balance, account_one_starting_balance - 10, "A quantidade não foi retirada corretamente do remetente");
    assert.equal(account_two_ending_balance, account_two_starting_balance + 10, "A quantia não foi enviada corretamente para o receptor");
  });
Enter fullscreen mode Exit fullscreen mode

Grande Teste Truffle Async/Await

Conclusão:

Espero que você possa ver o quanto os testes podem ser melhores com async/await. Muitos boilerplate (se refere a seções de código que devem ser incluídas em muitos lugares com pouca ou nenhuma alteração) desaparecem, o que aumenta a legibilidade. Capturar erros pode ser mais tedioso, mas eles são gerenciáveis. Além disso, se você precisar viajar no tempo para seus testes, o Ganache-cli fornece um método para atualizar o tempo. Se você já estiver usando promises, a integração com async/await é muito fácil. Se você tiver muitos retornos de chamada, o uso require('utils').promisify facilita sua vida. Vê a tendência?

Junte-se ao canal Coinmonks no Telegram e ao canal do Youtube receba notícias diárias sobre criptomoedas

Leia também


Artigo escrito por Angello Pozo e traduzido por Marcelo Panegali. A versão original pode ser encontrada aqui.

Oldest comments (0)