Skip to content

Commit

Permalink
Balance change checker, implements native tokens part of TrueFiEng#9 (T…
Browse files Browse the repository at this point in the history
…rueFiEng#24)

* Implemented balance checker, implements native tokens part of TrueFiEng#9
  • Loading branch information
vanruch authored and marekkirejczyk committed Dec 27, 2018
1 parent 418ac5d commit 0d7ac6d
Show file tree
Hide file tree
Showing 6 changed files with 231 additions and 2 deletions.
14 changes: 14 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -207,6 +207,20 @@ 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 specified 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]);
```

## Roadmap

* New matcher: changeBalance (see [#9](https://github.com/EthWorks/Waffle/issues/9))
Expand Down
43 changes: 43 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,47 @@ 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) => {
const walletsAddresses = wallets.map((wallet) => wallet.address);
this.assert(actualChanges.every((change, ind) => change.eq(bigNumberify(balanceChanges[ind]))),
`Expected ${walletsAddresses} to change balance by ${balanceChanges} wei, but it has changed by ${actualChanges} wei`,
`Expected ${walletsAddresses} 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]));
};
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"name": "ethereum-waffle",
"description": "Sweeter and simpler than truffle.",
"version": "1.0.5",
"version": "1.0.6",
"author": "Marek Kirejczyk <[email protected]> (http://ethworks.io)",
"repository": "[email protected]:EthWorks/Waffle.git",
"private": false,
Expand Down
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])`);
});
});
});
7 changes: 6 additions & 1 deletion test/matchers/events.js
Original file line number Diff line number Diff line change
Expand Up @@ -40,11 +40,16 @@ describe('Events', () => {
).to.be.eventually.rejectedWith(AssertionError, 'Expected event "One" to emitted, but wasn\'t');
});

it('Emit both: success', async () => {
it('Emit both: success (two expects)', async () => {
await expect(events.emitBoth()).to.emit(events, 'One', '0x0000000000000000000000000000000000000000000000000000000000000001');
await expect(events.emitBoth()).to.emit(events, 'Two');
});

it('Emit both: success (one expect with two to)' , async () => {
await expect(events.emitBoth()).to.emit(events, 'One', '0x0000000000000000000000000000000000000000000000000000000000000001')
.and.to.emit(events, 'Two');
});

it('Event with proper args', async () => {
await (expect(events.emitOne()).to.emit(events, 'One')).withArgs(1, 'One', '0x00cfbbaf7ddb3a1476767101c12a0162e241fbad2a0162e2410cfbbaf7162123');
});
Expand Down

0 comments on commit 0d7ac6d

Please sign in to comment.