Wie können Sie mit einem erwarteten Wurf in einem Vertragstest mit Truffle und Ethereum-testRPC umgehen?

Ist es möglich, einen Test mit Trüffel zu schreiben, der versucht zu bestätigen, dass ein Wurf in einem Vertrag auftritt? Wenn ich zum Beispiel einen Vertrag mit Funktion hätte...

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

Wenn ich einen Test in Truffle schreibe, der diese Funktion aufruft, stürzt der Truffle-Test im Grunde ab mit:

Error: VM Exception while executing transaction: invalid JUMP

Gibt es eine Möglichkeit, diese Ausnahme innerhalb Ihres Tests zu behandeln, um zu überprüfen, ob der Wurf tatsächlich aufgetreten ist? Der Grund, warum ich dies tun möchte, ist zu testen, ob meine Funktionen tatsächlich auslösen, wenn der Benutzer ungültige Eingaben übergibt?

Antworten (10)

Sie können den ExpectThrow-Helfer von OpenZeppelin verwenden -

Quelle: 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');
    };

Ich benutze es meine Testfälle so -

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
        });
});
funktioniert nicht, wenn es in parity/geth ausgeführt wird. Funktioniert nur mit testrpc
Welchen Fehler bekommst du in Geth? Parity kenne ich nicht und kann dazu nichts sagen.
Außerdem können Sie weitere Fehler hinzufügen, die Sie im Testfall erwarten können. Im Moment werden in helpers/expectThrow.js nur kein Gas mehr und ungültige Sprünge als Fehler aufgeführt
SyntaxError: Unexpected token import github.com/trufflesuite/truffle/issues/664
Truffle dev hier - dies sollte mit Geth & Parity funktionieren, solange Sie die neueste Version von verwenden truffle-contract. Wenn dies nicht der Fall ist, melden Sie sich bitte !
Ab v2.0 ist der Helfer shouldFail, überprüfen Sie meine Antwort für Details.

Meiner Meinung nach ist der sauberste Weg der folgende:

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'");
    }
});

Keine Notwendigkeit für return. In derselben Funktion können mehrere Prüfungen durchgeführt werden.

Die anderen Antworten in diesem Thread scheinen gültig zu sein, aber ich glaube, dass dieser Code prägnanter und lesbarer ist.

Das funktioniert mitsolidity 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.'
                )
         });
});

Mir ist aufgefallen, dass bei der Verwendung von truffle+testrpc einige throwseine „out of gas“-Ausnahme und andere eine „invalid opcode“-Ausnahme verursachen. Ich habe die Ursachen dieser unterschiedlichen Meldungen nicht bestätigt, aber sie scheinen konsistent zu sein. Ich rate davon ab, beide Ausnahmen naiv zu testen, da es möglicherweise nützliche Informationen sind, wenn sich die Ausnahmemeldung ändert.

Hier ist das Muster, das ich derzeit verwende, um erwartete Würfe zu testen (z. B. bei ungültiger Eingabe). Solidity implementiert throw by JUMPing zu einem ungültigen Ziel, also fangen wir den Fehler ab und suchen dann in der Fehlermeldung nach der Zeichenfolge "invalid JUMP" ... Ich hätte lieber einen robusteren Weg, habe aber noch nichts anderes gefunden.

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());
      }
    });
  });
});
Sie können diesen Code verkürzen (und die Lesbarkeit verbessern, IMO), indem Sie Ihren .then()Block durch ersetzen .then(assert.fail). Ich habe eine andere Antwort mit dem vollständigen Code in dieser Frage gepostet.

Sie können diesen von mir erstellten Kern verwenden :

var erwarteteAusnahmeVersprechen = Funktion (Aktion, gasToUse) {
  return new Promise(function (auflösen, ablehnen) {
      Versuchen {
        lösen (aktion ());
      } Fang(e) {
        ablehnen (e);
      }
    })
    .then(Funktion (txn) {
      // https://gist.github.com/xavierlepretre/88682e871f4ad07be4534ae560692ee6
      web3.eth.getTransactionReceiptMined(txn) zurückgeben;
    })
    .then(Funktion (Empfang) {
      // Wir sind in Geth
      assert.equal(receipt.gasUsed, gasToUse, "sollte das ganze Gas verbraucht haben");
    })
    .catch(Funktion (e) {
      if ((e + "").indexOf("ungültiger SPRUNG") || (e + "").indexOf("kein Gas") > -1) {
        // Wir befinden uns in TestRPC
      } else if ((e + "").indexOf("bitte überprüfen Sie Ihre Gasmenge") > -1) {
        // Wir sind für einen Einsatz in Geth
      } anders {
        wirf e;
      }
    });
};
Vielen Dank! Ich konnte dieses Snippet verwenden und den Beispielen aus dem Kern folgen, um in einem einzigen Fall erfolgreich nach einem Wurf zu suchen, aber es scheint die Ausführung nachfolgender Tests zu vermasseln. Verwenden Sie dieses "expectedExceptionPromise" über mehrere Tests hinweg, die nach unterschiedlichen Auslösungen suchen, und sehen Sie nicht, dass testRPC weiterhin den ungültigen JUMP-Fehler aufdeckt?
Es bringt meine nachfolgenden Tests nicht durcheinander it("", function() {}), weder auf TestRPC noch auf Geth. Möglicherweise verwenden Ihre beiden Tests eine gemeinsame Variable.

Die anderen Antworten funktionieren nicht für neuere Versionen von Solidity ( 0.4.10und höher, glaube ich).

Stattdessen verwende ich ein ähnliches Muster, aber mit zwei Zeichenfolgenvergleichen, um die neue Fehlermeldung sowie die ältere (nur für ältere Verträge/Tests) abzufangen.

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();
      })
  })
}

Relevantes Problem in Solidity GH

Ab v2.0 hat OpenZeppelin den ExpectEvent- Helfer anstelle von expectThrow. Hier ist eine Möglichkeit, es zu verwenden:

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

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

Die meisten Antworten auf diese Frage, die Inline-Try-Catch-Anweisungen verwenden, fügen allen Tests, die versuchen, diese Methode zu verwenden, einiges an Boilerplate hinzu. Stattdessen können Sie mit meiner truffle-assertionsBibliothek auf sehr einfache Weise Behauptungen für jede Art von Solidity-Wurf oder Funktionsfehler aufstellen.

Die Bibliothek kann über npm installiert und am Anfang der Test-Javascript-Datei importiert werden:

npm install truffle-assertions

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

Danach kann es in den Tests verwendet werden:

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

OpenZeppelin hat einen expectThrowHelfer, der dafür nützlich ist. Es liegt intest/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');
};

Beispielverwendung ist test/MintableToken.jszum Beispiel in:

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

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

Hier ist ein anderer Ansatz (inspiriert von obigen Lösungen).

Beim Definieren von benutzerdefinierten Erwartungsfunktionen wie diesen (Sie können gerne weitere hinzufügen), sind die Tests meiner Meinung nach dann expliziter in Bezug auf das, was Sie erwarten.

// 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')
 }

Dann machen Sie in Ihrem Test zum Beispiel:

/// test.js

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

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