Skip to content

Commit

Permalink
Merge pull request #45 from a-bahdanau/positive-negative-testing
Browse files Browse the repository at this point in the history
chore: add positive and negative testing docs and examples
  • Loading branch information
krigga authored May 20, 2024
2 parents 06479e3 + c80dd60 commit 3e7cab2
Show file tree
Hide file tree
Showing 2 changed files with 259 additions and 0 deletions.
17 changes: 17 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ The key difference of this package from [ton-contract-executor](https://github.c
* [Test a transaction with matcher](#test-a-transaction-with-matcher)
* [Testing transaction fees](#testing-transaction-fees)
* [Cross contract tests](#cross-contract-tests)
* [Testing key points](#testing-key-points)
* [Test examples](#test-examples)
* [Viewing logs](#viewing-logs)
* [Setting smart contract state directly](#setting-smart-contract-state-directly)
Expand Down Expand Up @@ -265,6 +266,22 @@ it('minter admin should be able to mint jettons', async () => {
});
```

### Testing key points

In order to make sure that the contract will work as expected, you need to follow the following points in testing

* Test positive flows to make sure your contracts work
* Test negative flows to make sure that smart contracts behave correctly under abnormal conditions. Abnormal conditions includes:
* incorrect input
* action list overflow
* insufficient toncoin amount
* integer overflow
* owner assertions

More information about testing key points can be found here:
* [Testing key point](docs/testing-key-points.md)


### Test Examples
You can typically find various tests for Sandbox-based project contracts in the `./tests` directory.
Learn more from examples:
Expand Down
242 changes: 242 additions & 0 deletions docs/testing-key-points.md
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 │
...
```



0 comments on commit 3e7cab2

Please sign in to comment.