Skip to content

Commit

Permalink
Implemented balance checker, implements native tokens part of #9
Browse files Browse the repository at this point in the history
  • Loading branch information
vanruch authored and marekkirejczyk committed Dec 27, 2018
1 parent 418ac5d commit faf7567
Show file tree
Hide file tree
Showing 4 changed files with 225 additions and 0 deletions.
16 changes: 16 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -207,6 +207,22 @@ expect('0x706618637b8ca922f6290ce1ecd4c31247e9ab75cf0530a0ac95c0332173d7c5').to.
expect('0x70').to.be.properHex(2);
```

* Testing whether the transaction changes balance
```js
await expect(() => myContract.transferWei(receiverWallet.address, 2)).to.changeBalance(receiverWallet, 2);
```
_Note:_ transaction call should be passed to the _expect_ as a callback (we need to check the balance before the call).
The matcher can accept numbers, strings and BigNumbers as a balance change, while the address should be cpecified as a wallet.

_changeBalance_ calls should not be chained. If you need to chain it, you probably want to use _changeBalances_ matcher.

* Testing whether the transaction changes balance for multiple accounts
```js
await expect(() => myContract.transferWei(receiverWallet.address, 2)).to.changeBalances([senderWallet, receiverWallet], [-2, 2]);
```
_Note:_ transaction call should be passed to the expect as a callback (we need to check the balance before the call)


## Roadmap

* New matcher: changeBalance (see [#9](https://github.com/EthWorks/Waffle/issues/9))
Expand Down
42 changes: 42 additions & 0 deletions lib/matchers.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
import overwriteBigNumberFunction from './matchers/overwriteBigNumberFunction';
import {bigNumberify} from 'ethers/utils';
import {getBalanceChange, getBalanceChanges} from './utils';

const solidity = (chai, utils) => {
const {Assertion} = chai;
Expand Down Expand Up @@ -140,6 +142,46 @@ const solidity = (chai, utils) => {
'proper address (eg.: 0x1234567890123456789012345678901234567890)',
subject);
});

Assertion.addMethod('changeBalance', function (wallet, balanceChange) {
const subject = this._obj;
if (typeof subject !== 'function') {
throw new Error(`Expect subject should be a callback returning the Promise
e.g.: await expect(() => wallet.send({to: '0xb', value: 200})).to.changeBalance('0xa', -200)`);
}
const derivedPromise = getBalanceChange(subject, wallet)
.then((actualChange) => {
this.assert(actualChange.eq(bigNumberify(balanceChange)),
`Expected "${wallet.address}" to change balance by ${balanceChange} wei, but it has changed by ${actualChange} wei`,
`Expected "${wallet.address}" to not change balance by ${balanceChange} wei,`,
balanceChange,
actualChange);
});
this.then = derivedPromise.then.bind(derivedPromise);
this.catch = derivedPromise.catch.bind(derivedPromise);
this.promise = derivedPromise;
return this;
});

Assertion.addMethod('changeBalances', function (wallets, balanceChanges) {
const subject = this._obj;
if (typeof subject !== 'function') {
throw new Error(`Expect subject should be a callback returning the Promise
e.g.: await expect(() => wallet.send({to: '0xb', value: 200})).to.changeBalances(['0xa', '0xb'], [-200, 200])`);
}
const derivedPromise = getBalanceChanges(subject, wallets)
.then((actualChanges) => {
this.assert(actualChanges.every((change, ind) => change.eq(bigNumberify(balanceChanges[ind]))),
`Expected ${wallets.map((wallet) => wallet.address)} to change balance by ${balanceChanges} wei, but it has changed by ${actualChanges} wei`,
`Expected ${wallets.map((wallet) => wallet.address)} to not change balance by ${balanceChanges} wei,`,
balanceChanges.map((balanceChange) => balanceChange.toString()),
actualChanges.map((actualChange) => actualChange.toString()));
});
this.then = derivedPromise.then.bind(derivedPromise);
this.catch = derivedPromise.catch.bind(derivedPromise);
this.promise = derivedPromise;
return this;
});
};

export default solidity;
14 changes: 14 additions & 0 deletions lib/utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,3 +13,17 @@ export const eventParseResultToArray = (eventResult) =>

export const isWarningMessage = (error) =>
/: Warning: /.test(error);

export const getBalanceChange = async (transactionCallback, wallet) => {
const balanceBefore = await wallet.getBalance();
await transactionCallback();
const balanceAfter = await wallet.getBalance();
return balanceAfter.sub(balanceBefore);
};

export const getBalanceChanges = async (transactionCallback, wallets) => {
const balancesBefore = await Promise.all(wallets.map((wallet) => wallet.getBalance()));
await transactionCallback();
const balancesAfter = await Promise.all(wallets.map((wallet) => wallet.getBalance()));
return balancesAfter.map((balancesAfter, ind) => balancesAfter.sub(balancesBefore[ind]));
};
153 changes: 153 additions & 0 deletions test/matchers/balance.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,153 @@
import chai, {AssertionError} from 'chai';
import {createMockProvider, getWallets, solidity} from '../../lib/waffle';
import {utils} from 'ethers';

chai.use(solidity);
const {expect} = chai;

describe('Balance observers', () => {
let provider;
let sender;
let receiver;

beforeEach(async () => {
provider = createMockProvider();
[sender, receiver] = await getWallets(provider);
});

describe('Change balance, one account', () => {
it('Should pass when expected balance change is passed as string and is equal to an actual', async () => {
await expect(() => sender.sendTransaction({
to: receiver.address,
gasPrice: 0,
value: 200
}))
.to.changeBalance(sender, '-200');
});

it('Should pass when expected balance change is passed as int and is equal to an actual', async () => {
await expect(() => sender.sendTransaction({
to: receiver.address,
gasPrice: 0,
value: 200
}))
.to.changeBalance(receiver, 200);
});

it('Should pass when expected balance change is passed as BN and is equal to an actual', async () => {
await expect(() => sender.sendTransaction({
to: receiver.address,
gasPrice: 0,
value: 200
}))
.to.changeBalance(receiver, utils.bigNumberify(200));
});

it('Should pass on negative case when expected balance change is not equal to an actual', async () => {
await expect(() => sender.sendTransaction({
to: receiver.address,
gasPrice: 0,
value: 200
}))
.to.not.changeBalance(receiver, utils.bigNumberify(300));
});

it('Should throw when expected balance change value was different from an actual', async () => {
await expect(
expect(() => sender.sendTransaction({
to: receiver.address,
gasPrice: 0,
value: 200
}))
.to.changeBalance(sender, '-500')
).to.be.eventually.rejectedWith(AssertionError, `Expected "${sender.address}" to change balance by -500 wei, but it has changed by -200 wei`);
});

it('Should throw in negative case when expected balance change value was equal to an actual', async () => {
await expect(
expect(() => sender.sendTransaction({
to: receiver.address,
gasPrice: 0,
value: 200
})).to.not.changeBalance(sender, '-200')
).to.be.eventually.rejectedWith(AssertionError, `Expected "${sender.address}" to not change balance by -200 wei`);
});

it('Should throw when not a callback is passed to expect', async () => {
expect(() =>
expect(sender.sendTransaction({
to: receiver.address,
gasPrice: 0,
value: 200
})).to.changeBalance(sender, '-200')
).to.throw(Error, `Expect subject should be a callback returning the Promise
e.g.: await expect(() => wallet.send({to: '0xb', value: 200})).to.changeBalance('0xa', -200)`);
});
});

describe('Change balance, multiple accounts', () => {
it('Should pass when all expected balance changes are equal to actual values', async () => {
await expect(() => sender.sendTransaction({
to: receiver.address,
gasPrice: 0,
value: 200
}))
.to.changeBalances([sender, receiver], ['-200', 200]);
});

it('Should pass on negative case when one of expected balance changes is not equal to an actual value', async () => {
await expect(() => sender.sendTransaction({
to: receiver.address,
gasPrice: 0,
value: 200
}))
.to.not.changeBalances([sender, receiver], [-201, 200]);
await expect(() => sender.sendTransaction({
to: receiver.address,
gasPrice: 0,
value: 200
}))
.to.not.changeBalances([sender, receiver], [-200, 201]);
});

it('Should throw when expected balance change value was different from an actual for any wallet', async () => {
await expect(
expect(() => sender.sendTransaction({
to: receiver.address,
gasPrice: 0,
value: 200
}))
.to.changeBalances([sender, receiver], [-200, 201])
).to.be.eventually.rejectedWith(AssertionError, 'Expected 0x17ec8597ff92C3F44523bDc65BF0f1bE632917ff,0x63FC2aD3d021a4D7e64323529a55a9442C444dA0 to change balance by -200,201 wei, but it has changed by -200,200 wei');
await expect(
expect(() => sender.sendTransaction({
to: receiver.address,
gasPrice: 0,
value: 200
}))
.to.changeBalances([sender, receiver], [-201, 200])
).to.be.eventually.rejectedWith(AssertionError, 'Expected 0x17ec8597ff92C3F44523bDc65BF0f1bE632917ff,0x63FC2aD3d021a4D7e64323529a55a9442C444dA0 to change balance by -201,200 wei, but it has changed by -200,200 wei');
});

it('Should throw in negative case when expected balance changes value were equal to an actual', async () => {
await expect(
expect(() => sender.sendTransaction({
to: receiver.address,
gasPrice: 0,
value: 200
})).to.not.changeBalances([sender, receiver], [-200, 200])
).to.be.eventually.rejectedWith(AssertionError, `Expected 0x17ec8597ff92C3F44523bDc65BF0f1bE632917ff,0x63FC2aD3d021a4D7e64323529a55a9442C444dA0 to not change balance by -200,200 wei`);
});

it('Should throw when not a callback is passed to expect', async () => {
expect(() =>
expect(sender.sendTransaction({
to: receiver.address,
gasPrice: 0,
value: 200
})).to.changeBalances([sender, receiver], ['-200', 200])
).to.throw(Error, `Expect subject should be a callback returning the Promise
e.g.: await expect(() => wallet.send({to: '0xb', value: 200})).to.changeBalances(['0xa', '0xb'], [-200, 200])`);
});
});
});

0 comments on commit faf7567

Please sign in to comment.