Skip to content

Latest commit

 

History

History
242 lines (196 loc) · 9.19 KB

testing-key-points.md

File metadata and controls

242 lines (196 loc) · 9.19 KB

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. Code of contract wrapper located here.

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.

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.

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

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.

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.

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.

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!

() 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.

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.

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          │
...