Как вы можете справиться с ожидаемым броском в тесте контракта, используя truffle и ethereum-testRPC?

Можно ли написать тест с использованием трюфеля, который пытается подтвердить, что в контракте происходит бросок? Например, если бы у меня был контракт с функцией...

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

если я пишу тест в трюфеле, который вызывает эту функцию, то тест трюфеля в основном падает с:

Error: VM Exception while executing transaction: invalid JUMP

Есть ли способ обработать это исключение из вашего теста, чтобы убедиться, что бросок действительно произошел? Причина, по которой я хочу это сделать, состоит в том, чтобы проверить, действительно ли мои функции вызывают ошибки, когда пользователь вводит неверный ввод?

Ответы (10)

Вы можете использовать помощник OpenZeppelin expectThrow -

Источник: 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');
    };

Я использую его в своих тестовых примерах следующим образом:

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
        });
});
не работает, если запущен в parity/geth. Работает только с testrpc
Какую ошибку вы получаете в geth? Я не знаком с паритетом, поэтому не могу сказать об этом.
Кроме того, вы можете добавить больше ошибок, которые вы можете ожидать в тестовом примере. На данный момент в helpers/expectThrow.js в качестве ошибок указаны только отсутствие газа и неверный прыжок.
SyntaxError: Unexpected token import github.com/trufflesuite/truffle/issues/664
Truffle dev здесь — это должно работать с geth и четностью, если вы используете последнюю версию truffle-contract. Если это не так, пожалуйста, поднимите вопрос !
начиная с версии 2.0 помощником является shouldFail, подробности см. В моем ответе.

На мой взгляд, самый чистый из возможных способов заключается в следующем:

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

Нет необходимости в return. В одной и той же функции может быть выполнено несколько проверок.

Другие ответы в этой теме кажутся действительными, однако я считаю, что этот код более лаконичный и читабельный.

Это работает сsolidity 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.'
                )
         });
});

Я заметил, что при использовании truffle+testrpc некоторые throwsвызывают исключение «нет газа», а другие — исключение «неверный код операции». Я не подтвердил причины этих разных сообщений, но они кажутся последовательными. Я советую не проводить наивное тестирование обоих исключений, так как это потенциально полезная информация, если сообщение об исключении изменится.

Вот шаблон, который я сейчас использую для проверки ожидаемых бросков (например, при неверном вводе). Solidity реализует бросок с помощью JUMP в недопустимое место назначения, поэтому мы перехватываем ошибку, а затем ищем строку «invalid JUMP» в сообщении об ошибке... Я бы предпочел более надежный способ, но пока ничего другого не нашел.

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());
      }
    });
  });
});
Вы можете сократить этот код (и улучшить читаемость, IMO), заменив свой .then()блок на .then(assert.fail). Я опубликовал еще один ответ с полным кодом в этом вопросе.

Вы можете использовать этот gist, который я создал :

var expectExceptionPromise = функция (действие, gasToUse) {
  вернуть новое обещание (функция (разрешить, отклонить) {
      пытаться {
        решить (действие ());
      } поймать(е) {
        отклонить (е);
      }
    })
    .затем (функция (txn) {
      // https://gist.github.com/xavierlepretre/88682e871f4ad07be4534ae560692ee6
      вернуть web3.eth.getTransactionReceiptMined(txn);
    })
    .then(функция (квитанция) {
      // Мы в Гете
      assert.equal(receipt.gasUsed, gasToUse, "следует использовать весь газ");
    })
    .поймать (функция (е) {
      if ((e + "").indexOf("недопустимый ПРЫЖОК") || (e + "").indexOf("кончился газ") > -1) {
        // Мы в TestRPC
      } else if ((e + "").indexOf("пожалуйста, проверьте количество газа") > -1) {
        // Мы находимся в Geth для развертывания
      } еще {
        бросить е;
      }
    });
};
Спасибо! Я смог использовать этот фрагмент и следовать примерам из сути, чтобы успешно проверить бросок в одном случае, однако, похоже, это испортило выполнение последующих тестов. Используете ли вы это expectExceptionPromise в нескольких тестах, проверяющих разные броски, и не видите, что testRPC продолжает выявлять недопустимую ошибку JUMP?
Это не портит мои последующие it("", function() {})тесты ни на TestRPC, ни на Geth. Возможно, оба ваших теста используют общую переменную.

Другие ответы не будут работать для более новых версий Solidity ( 0.4.10и выше, я думаю).

Вместо этого я использую аналогичный шаблон, но с двумя сравнениями строк, чтобы поймать новое сообщение об ошибке, а также старое (только для устаревших контрактов/тестов).

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

Актуальная проблема в Solidity GH

Начиная с версии 2.0 в OpenZeppelin вместо expectThrow. Вот как это использовать:

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

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

Большинство ответов на этот вопрос, в которых используются встроенные операторы try catch, добавляют довольно много шаблонов ко всем тестам, которые пытаются использовать этот метод. Вместо этого моя truffle-assertionsбиблиотека позволяет вам делать утверждения для любого вида броска Solidity или сбоя функции очень простым способом.

Библиотеку можно установить через npm и импортировать в начало тестового файла javascript:

npm install truffle-assertions

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

После чего его можно использовать внутри тестов:

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

В OpenZeppelin есть expectThrowпомощник, который может помочь в этом. Это находится вtest/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');
};

пример использования, test/MintableToken.jsнапример:

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

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

Вот еще один подход (вдохновленный приведенными выше решениями).

При определении пользовательских функций ожидания, подобных этим (не стесняйтесь добавлять больше), я чувствую, что тесты более четко показывают, что вы ожидаете.

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

Затем в вашем тесте вы делаете, например:

/// test.js

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

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