¿Cómo puede manejar un lanzamiento esperado en una prueba de contrato usando truffle y ethereum-testRPC?

¿Es posible escribir una prueba usando trufa que intente confirmar que se produce un lanzamiento en un contrato? Por ejemplo, si tuviera un contrato con función...

contract TestContract {
  function testThrow() {
    throw;
  }
}

si escribo una prueba en truffle que invoque esta función, entonces la prueba de truffle básicamente falla con:

Error: VM Exception while executing transaction: invalid JUMP

¿Hay alguna forma de manejar esta excepción desde su prueba para verificar que realmente ocurrió el lanzamiento? ¿La razón por la que quiero hacerlo es para probar que mis funciones realmente se lanzan cuando el usuario pasa una entrada no válida?

Respuestas (10)

Puede usar el ayudante expectThrow de OpenZeppelin:

Fuente: https://github.com/OpenZeppelin/zeppelin-solidity/blob/master/test/helpers/expectThrow.js

export default async promise => {
      try {
        await promise;
      } catch (error) {
        // TODO: Check jump destination to destinguish between a throw
        //       and an actual invalid jump.
        const invalidJump = error.message.search('invalid JUMP') >= 0;
        // TODO: When we contract A calls contract B, and B throws, instead
        //       of an 'invalid jump', we get an 'out of gas' error. How do
        //       we distinguish this from an actual out of gas event? (The
        //       testrpc log actually show an 'invalid jump' event.)
        const outOfGas = error.message.search('out of gas') >= 0;
        assert(
          invalidJump || outOfGas,
          "Expected throw, got '" + error + "' instead",
        );
        return;
      }
      assert.fail('Expected throw not received');
    };

Lo uso en mis casos de prueba así:

import expectThrow from './helpers/expectThrow';
.
.
.
describe('borrowBook', function() {
        it("should not allow borrowing book if value send is less than 100", async function() {
            await lms.addBook('a', 'b', 'c', 'e', 'f', 'g');
            await lms.addMember('Michael Scofield', accounts[2], "Ms@gmail.com");
            await lms.borrowBook(1, {from: accounts[2], value: 10**12})
            await expectThrow(lms.borrowBook(1, {from: accounts[2], value: 10000})); // should throw exception
        });
});
no funciona si se ejecuta en paridad/geth. Solo lo hace con testrpc
¿Qué error te da en geth? No estoy familiarizado con la paridad, así que no puedo decir nada al respecto.
Además, puede agregar más errores que puede esperar en el caso de prueba. En este momento, solo la falta de combustible y el salto no válido se enumeran como errores en helpers/expectThrow.js
SyntaxError: Unexpected token import github.com/trufflesuite/truffle/issues/664
Truffle dev aquí: esto debería funcionar con geth y paridad siempre que esté usando la última versión de truffle-contract. Si no es así, por favor plantee un problema .
a partir de v2.0, el ayudante es shouldFail, verifique mi respuesta para obtener más detalles.

En mi opinión, la forma más limpia posible es la siguiente:

it("should reject", async function () {
    try {
        await deployedInstance.myOperation1();
        assert.fail("The transaction should have thrown an error");
    }
    catch (err) {
        assert.include(err.message, "revert", "The error message should contain 'revert'");
    }
});

No hay necesidad de return. Se pueden realizar varias comprobaciones en la misma función.

Las otras respuestas en este hilo parecen ser válidas, sin embargo, creo que este código es más breve y legible.

esto funciona consolidity 0.4.12-develop

it("should throw if the car is not blue", function() {
    return CarFactory.deployed()
        .then(function(factory) {
            return factory.createCar("red");
         })
         .then(assert.fail)
         .catch(function(error) {
                assert.include(
                    error.message,
                    'out of gas',
                    'red cars should throw an out of gas exception.'
                )
         });
});

He notado que cuando uso truffle+testrpc algunos throwscausan una excepción de 'sin gasolina' y otros una excepción de 'código de operación no válido'. No he confirmado las causas de estos diferentes mensajes, pero parecen ser consistentes. No aconsejo probar ingenuamente ambas excepciones, ya que es información potencialmente útil si cambia el mensaje de excepción.

Aquí está el patrón que uso actualmente para probar los lanzamientos esperados (por ejemplo, en una entrada no válida). Solidity implementa throw by JUMPing a un destino no válido, por lo que detectamos el error y luego buscamos la cadena "JUMP no válido" en el mensaje de error... Prefiero tener una forma más robusta pero no he encontrado nada más todavía.

var EthWall = artifacts.require("./EthWall.sol");

contract('TestContract', function(accounts) {
  it("should throw an exception", function() {
    return EthWall.deployed().then(function(instance) {
      return instance.testThrow.call();
    }).then(function(returnValue) {
      assert(false, "testThrow was supposed to throw but didn't.");
    }).catch(function(error) {
      if(error.toString().indexOf("invalid JUMP") != -1) {
        console.log("We were expecting a Solidity throw (aka an invalid JUMP), we got one. Test succeeded.");
      } else {
        // if the error is something else (e.g., the assert from previous promise), then we fail the test
        assert(false, error.toString());
      }
    });
  });
});
Puede acortar este código (y mejorar la legibilidad, en mi opinión) reemplazando su .then()bloque con .then(assert.fail). He publicado otra respuesta con el código completo en esta pregunta.

Puedes usar esta esencia que creé :

var ExpectedExceptionPromise = function (acción, gasToUse) {
  return new Promise(función (resolver, rechazar) {
      probar {
        resolver (acción ());
      } atrapar (e) {
        rechazar (e);
      }
    })
    .entonces(función (txn) {
      // https://gist.github.com/xavierlepretre/88682e871f4ad07be4534ae560692ee6
      volver web3.eth.getTransactionReceiptMined(txn);
    })
    .then(función (recibo) {
      // Estamos en Geth
      afirmar.equal(recibo.gasUsed, gasToUse, "debería haber usado todo el gas");
    })
    .catch(función (e) {
      if ((e + "").indexOf("JUMP no válido") || (e + "").indexOf("sin gasolina") > -1) {
        // Estamos en TestRPC
      } else if ((e + "").indexOf("verifique la cantidad de gasolina") > -1) {
        // Estamos en Geth para un despliegue
      } más {
        tirar e;
      }
    });
};
¡Gracias! Pude usar este fragmento y seguir los ejemplos de la esencia para verificar con éxito un lanzamiento en un solo caso, sin embargo, parece arruinar la ejecución de las pruebas posteriores. ¿Utiliza esta promesa de excepción esperada en varias pruebas que verifican diferentes lanzamientos y no ve que testRPC continúa exponiendo el error JUMP no válido?
No estropea mis it("", function() {})pruebas posteriores, ni en TestRPC ni en Geth. Quizás ambas pruebas estén usando una variable común.

Las otras respuestas no funcionarán para las versiones más nuevas de Solidity ( 0.4.10y superiores, creo).

En su lugar, uso un patrón similar, pero con dos comparaciones de cadenas para detectar el nuevo mensaje de error, así como el anterior (solo para contratos/pruebas heredados).

function assertThrows (fn, args) {
  //Asserts that `fn(args)` will throw a specific type of error.
  return new Promise(
    function(resolve, reject){
      fn.apply(this, args)
      .then(() => {
        assert(false, 'No error thrown.');
        resolve();
      },
      (error) => {
        var errstr = error.toString();
        var newErrMsg = errstr.indexOf('invalid opcode') != -1;
        var oldErrMsg = errstr.indexOf('invalid JUMP') != -1;
        if(!newErrMsg && !oldErrMsg)
          assert(false, 'Did not receive expected error message');
        resolve();
      })
  })
}

Problema relevante en Solidity GH

A partir de la versión 2.0, OpenZeppelin tiene el asistente expectEvent en lugar de expectThrow. Aquí hay una manera de usarlo:

import {reverting} from 'openzeppelin-solidity/test/helpers/shouldFail';

it('your test name', async () => {
    await reverting(contract.myMethod(argument1, argument2, {from: myAccount}));
})

La mayoría de las respuestas a esta pregunta que usan declaraciones de captura de prueba en línea agregan un poco de repetitivo a todas las pruebas que intentan usar este método. En cambio, mi truffle-assertionsbiblioteca le permite hacer afirmaciones para cualquier tipo de lanzamiento de Solidity o falla de función de una manera muy sencilla.

La biblioteca se puede instalar a través de npm e importar en la parte superior del archivo javascript de prueba:

npm install truffle-assertions

const truffleAssert = require('truffle-assertions');

Después de lo cual se puede utilizar dentro de las pruebas:

await truffleAssert.fails(contract.failingFunction(), truffleAssert.ErrorType.INVALID_JUMP);

OpenZeppelin tiene un expectThrowayudante que es útil para esto. Está localizado entest/helpers/expectThrow.js

module.exports = async promise => {
  try {
    await promise;
  } catch (error) {
    // TODO: Check jump destination to destinguish between a throw
    //       and an actual invalid jump.
    const invalidOpcode = error.message.search('invalid opcode') >= 0;
    // TODO: When we contract A calls contract B, and B throws, instead
    //       of an 'invalid jump', we get an 'out of gas' error. How do
    //       we distinguish this from an actual out of gas event? (The
    //       testrpc log actually show an 'invalid jump' event.)
    const outOfGas = error.message.search('out of gas') >= 0;
    assert(
      invalidOpcode || outOfGas,
      "Expected throw, got '" + error + "' instead",
    );
    return;
  }
  assert.fail('Expected throw not received');
};

el uso de ejemplo está en, test/MintableToken.jspor ejemplo:

import expectThrow from './helpers/expectThrow';
...

await expectThrow(token.mint(accounts[0], 100));
...

Aquí hay otro enfoque (inspirado en las soluciones anteriores).

Al definir funciones esperadas personalizadas como estas (siéntase libre de agregar más), creo que las pruebas son más explícitas sobre lo que espera.

// expectThrow.js

const expectThrow = (text) => async (promise) => {
   try {
     await promise;
   } catch (error) {
     assert(error.message.search(text) >= 0, "Expected throw, got '" + error + "' instead")
     return
   }
   assert.fail('Expected throw not received')
 }

 module.exports =  {
   expectOutOfGas: expectThrow('out of gas'),
   expectRevert: expectThrow('revert'),
   expectInvalidJump: expectThrow('invalid JUMP')
 }

Luego, en tu prueba, haces, por ejemplo:

/// test.js

const { expectRevert } from './expectThrow.js'

it('your test name', async () => {
  await expectRevert(
    // your contract call
  )
})