-
Notifications
You must be signed in to change notification settings - Fork 32
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #45 from a-bahdanau/positive-negative-testing
chore: add positive and negative testing docs and examples
- Loading branch information
Showing
2 changed files
with
259 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,242 @@ | ||
## Testing flow | ||
|
||
This page describes testing of distributor smart contract. The goal of contract is to distribute some ton among users. | ||
Code of contract located [here](https://github.com/ton-community/onboarding-sandbox/blob/main/sandbox-examples/contracts/distributor.fc). | ||
Code of contract wrapper located [here](https://github.com/ton-community/onboarding-sandbox/blob/main/sandbox-examples/wrappers/Distributor.ts). | ||
|
||
### Positive testing | ||
|
||
First of all let's make sure our contract is deployed. | ||
Note that we are using `toHaveTransaction` from `@ton/test-utils` to make sure result have transaction that matches pattern. | ||
|
||
```typescript | ||
it('should deploy', async () => { | ||
const result = await distributor.sendDeploy(owner.getSender(), toNano('0.05')); | ||
|
||
expect(result.transactions).toHaveTransaction({ | ||
from: owner.address, | ||
on: distributor.address, | ||
deploy: true, | ||
success: true | ||
}); | ||
}); | ||
``` | ||
|
||
Let's check that our get methods are working. | ||
Note that we are using `toEqualAddress` from `@ton/test-utils`. Under the hood it uses `Address.equals` to compare addresses. | ||
|
||
```typescript | ||
it('should get owner', async () => { | ||
const ownerFromContract = await distributor.getOwner(); | ||
|
||
expect(ownerFromContract).toEqualAddress(owner.address); | ||
}); | ||
|
||
it('should get shares dict', async () => { | ||
const shares = await distributor.getShares(); | ||
|
||
expect(shares.keys().length).toEqual(0); | ||
}); | ||
``` | ||
|
||
To share our coins we need to add some users to shares dict | ||
```typescript | ||
it('should add firstUser', async () => { | ||
const result = await distributor.sendAddUser(owner.getSender(), { | ||
value: toNano('0.05'), | ||
userAddress: firstUser.address, | ||
}); | ||
|
||
expect(result.transactions).toHaveTransaction({ | ||
from: owner.address, | ||
on: distributor.address, | ||
success: true, | ||
}); | ||
|
||
const shares = await distributor.getShares(); | ||
|
||
expect(shares.keys()[0]).toEqualAddress(firstUser.address); | ||
}); | ||
``` | ||
|
||
After all preparations we now can finally share coins to users. | ||
```typescript | ||
it('should share coins to one user', async () => { | ||
const result = await distributor.sendShareCoins(owner.getSender(), { | ||
value: toNano('10'), | ||
}); | ||
|
||
expect(result.transactions).toHaveTransaction({ | ||
from: owner.address, | ||
on: distributor.address, | ||
outMessagesCount: 1, | ||
success: true | ||
}); | ||
expect(result.transactions).toHaveTransaction({ | ||
from: distributor.address, | ||
on: firstUser.address, | ||
op: 0x0, | ||
success: true | ||
}); | ||
}); | ||
``` | ||
|
||
Additionally, lets validate that our contract share coins to multiple users correctly. | ||
Function `findTransactionRequired` from `@ton/test-utils` will throw error if tx is not found. Also, it has silent version `findTransaction`. | ||
|
||
```typescript | ||
it('should add secondUser', async () => { | ||
const result = await distributor.sendAddUser(owner.getSender(), { | ||
value: toNano('0.05'), | ||
userAddress: secondUser.address | ||
}); | ||
|
||
expect(result.transactions).toHaveTransaction({ | ||
from: owner.address, | ||
on: distributor.address, | ||
success: true | ||
}); | ||
|
||
const shares = await distributor.getShares(); | ||
|
||
expect( | ||
shares.keys().some((addr) => secondUser.address.equals(addr)) | ||
).toBeTruthy(); | ||
}); | ||
|
||
|
||
it('should share coins to 2 users', async () => { | ||
const result = await distributor.sendShareCoins(owner.getSender(), { | ||
value: toNano('10') | ||
}); | ||
|
||
expect(result.transactions).toHaveTransaction({ | ||
from: owner.address, | ||
on: distributor.address, | ||
success: true, | ||
outMessagesCount: 2 | ||
}); | ||
expect(result.transactions).toHaveTransaction({ | ||
from: distributor.address, | ||
on: firstUser.address, | ||
op: 0x0, | ||
success: true | ||
}); | ||
expect(result.transactions).toHaveTransaction({ | ||
from: distributor.address, | ||
on: secondUser.address, | ||
op: 0x0, | ||
success: true | ||
}); | ||
|
||
const firstUserTransaction = findTransactionRequired(result.transactions, { on: firstUser.address }); | ||
const secondUserTransaction = findTransactionRequired(result.transactions, { on: secondUser.address }); | ||
|
||
expect(flattenTransaction(firstUserTransaction).value).toEqual(flattenTransaction(secondUserTransaction).value); | ||
}); | ||
``` | ||
|
||
By this simple steps positive flow was validated. But what if someone other than the owner tries to add user to shares? Negative testing comes in place. | ||
|
||
## Negative testing | ||
|
||
Below we make sure that no one except admin can call `add_user`. Function `randomAddress` from `@ton/test-utils` is useful for random address generation. | ||
|
||
```typescript | ||
it('should not add user as not owner', async () => { | ||
const notOwner = await blockchain.treasury(`not-owner`); | ||
|
||
const result = await distributor.sendAddUser(notOwner.getSender(), { | ||
value: toNano('0.5'), | ||
userAddress: randomAddress(), | ||
}); | ||
|
||
expect(result.transactions).toHaveTransaction({ | ||
from: notOwner.address, | ||
on: distributor.address, | ||
success: false, | ||
exitCode: ExitCode.MUST_BE_OWNER, | ||
}); | ||
}); | ||
``` | ||
|
||
We should put impure specifier in function below because it throws an exception. | ||
If impure is not specified compiler will delete this function call. This can lead to security issues so do not forget to test it! | ||
|
||
```func | ||
() throw_unless_owner(slice address) impure inline { | ||
throw_unless(err::must_be_owner, equal_slice_bits(address, storage::owner)); | ||
} | ||
``` | ||
|
||
Make sure that your contract does not output more than 255 out actions, or else action phase will fail with 33 exit code. | ||
|
||
```typescript | ||
it('should add 255 users', async () => { | ||
for (let i = 0; i < 255; ++i) { | ||
const userAddress = randomAddress(); | ||
const result = await distributor.sendAddUser(owner.getSender(), { | ||
value: toNano('0.5'), | ||
userAddress, | ||
}); | ||
expect(result.transactions).toHaveTransaction({ | ||
from: owner.address, | ||
on: distributor.address, | ||
success: true | ||
}); | ||
} | ||
}); | ||
|
||
it('should not add one more user', async () => { | ||
const userAddress = randomAddress(); | ||
const result = await distributor.sendAddUser(owner.getSender(), { | ||
value: toNano('0.5'), | ||
userAddress, | ||
}); | ||
expect(result.transactions).toHaveTransaction({ | ||
from: owner.address, | ||
on: distributor.address, | ||
success: false, | ||
exitCode: ExitCode.SHARES_SIZE_EXCEEDED_LIMIT | ||
}); | ||
}); | ||
``` | ||
|
||
Always test edge cases too. In this case we test 255 out actions. Function `filterTransactions` from `@ton/test-utils` provides nice interface for filtering transactions. | ||
|
||
```typescript | ||
it('should share money to 255 users', async () => { | ||
const result = await distributor.sendShareCoins(owner.getSender(), { | ||
value: toNano('1000') | ||
}); | ||
|
||
expect(result.transactions).toHaveTransaction({ | ||
from: owner.address, | ||
on: distributor.address, | ||
success: true | ||
}); | ||
|
||
printTransactionFees(result.transactions); | ||
|
||
const transferTransaction = filterTransactions(result.transactions, { op: 0x0 }); | ||
expect(transferTransaction.length).toEqual(255); | ||
}); | ||
``` | ||
|
||
Function `printTransactionFees` is useful for debugging costs of transactions. It provides output like this: | ||
``` | ||
┌─────────┬─────────────┬────────────────┬────────────────┬────────────────┬────────────────┬───────────────┬────────────┬────────────────┬──────────┬────────────┐ | ||
│ (index) │ op │ valueIn │ valueOut │ totalFees │ inForwardFee │ outForwardFee │ outActions │ computeFee │ exitCode │ actionCode │ | ||
├─────────┼─────────────┼────────────────┼────────────────┼────────────────┼────────────────┼───────────────┼────────────┼────────────────┼──────────┼────────────┤ | ||
│ 0 │ 'N/A' │ 'N/A' │ '1000 TON' │ '0.004007 TON' │ 'N/A' │ '0.001 TON' │ 1 │ '0.001937 TON' │ 0 │ 0 │ | ||
│ 1 │ '0x45ab564' │ '1000 TON' │ '998.8485 TON' │ '1.051473 TON' │ '0.000667 TON' │ '0.255 TON' │ 255 │ '0.966474 TON' │ 0 │ 0 │ | ||
│ 2 │ '0x0' │ '3.917053 TON' │ '0 TON' │ '0.00031 TON' │ '0.000667 TON' │ 'N/A' │ 0 │ '0.000309 TON' │ 0 │ 0 │ | ||
│ 3 │ '0x0' │ '3.917053 TON' │ '0 TON' │ '0.00031 TON' │ '0.000667 TON' │ 'N/A' │ 0 │ '0.000309 TON' │ 0 │ 0 │ | ||
│ 4 │ '0x0' │ '3.917053 TON' │ '0 TON' │ '0.00031 TON' │ '0.000667 TON' │ 'N/A' │ 0 │ '0.000309 TON' │ 0 │ 0 │ | ||
│ 5 │ '0x0' │ '3.917053 TON' │ '0 TON' │ '0.00031 TON' │ '0.000667 TON' │ 'N/A' │ 0 │ '0.000309 TON' │ 0 │ 0 │ | ||
│ 6 │ '0x0' │ '3.917053 TON' │ '0 TON' │ '0.00031 TON' │ '0.000667 TON' │ 'N/A' │ 0 │ '0.000309 TON' │ 0 │ 0 │ | ||
... | ||
``` | ||
|
||
|
||
|