Spaces:
Runtime error
Runtime error
File size: 10,036 Bytes
711e9c6 |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 |
---
sidebar_position: 5
---
# How to Test a Contract
Before using a smart contract in production, one should always test it carefully, especially because any bug in it may cause **real economic losses**.
There are two different kinds of tests recommended for every project using `sCrypt`:
* **Local Unit Testing**
* **Testnet Integration Testing**
Now we will take a look at the file `tests/local/demo.ts`. This file contains code for deployment of our `Demo` contract on the Bitcoin testnet and a subsequent public method call on the contract.
But before going into details, you should learn some basic models of sCrypt for signing and sending transactions.
## Compile the Contract
First, call function `SmartContract.compile()` to compile the contract before doing any testing.
```ts
await Demo.compile()
```
## Provider
A `Provider` is an abstraction of a standard Bitcoin node that provides connection to the Bitcoin network, for read and write access to the blockchain.
sCrypt already has a few built-in providers:
* `DummyProvider`: A mockup provider just for local tests. It does not connect to the Bitcoin blockchain and thus cannot send transactions.
* `DefaultProvider`: The default provider is the safest, easiest way to begin developing on Bitcoin, and it is also robust enough for use in production. It can be used in testnet as well as mainnet.
* See full list of providers [here](./reference/classes/Provider.md#hierarchy).
You can initialize these providers like this:
```ts
let dummyProvider = new DummyProvider();
// Mainnet
let provider = new DefaultProvider();
// Or explicitly: let provider = new DefaultProvider(bsv.Networks.mainnet);
// Testnet
let provider = new DefaultProvider(bsv.Networks.testnet);
```
## Signer
A `Signer` is an abstraction of private keys, which can be used to sign messages and transactions. A simple signer would be a single private key, while a complex signer is a wallet.
### TestWallet
For testing purposes only, we have a built-in wallet called `TestWallet`. It can be created like this:
```ts
const signer = new TestWallet(privateKey, provider);
```
`privateKey` can be a single private key or an array of private keys that the wallet can use to sign transactions. The ability of the wallet to send transactions is assigned to `provider`. In other words, a `TestWallet` serves as both a signer and a provider.
## Test a Contract Locally
Compared to other blockchains, smart contracts on Bitcoin are **pure**.
* Given the same input, its public method always returns the same boolean output: success or failure. It has no internal state.
* A public method call causes no side effects.
Smart contracts are similar to mathematical functions. Thus, we can test a contract locally without touching the Bitcoin blockchain. If it passes tests off chain, we are confident it will behave the same on chain.
### Prepare a Signer and Provider
For local testing, we can use the `TestWallet`, with a mock provider. The `TestWallet` and `DummyProvider` combination would be ideal for local tests because it can sign the contract call transactions without actually sending them.
Such a signer may be declared as below:
```ts
let signer = new TestWallet(privateKey, new DummyProvider());
```
Don't forget to connect the signer to the contract instance as well:
```ts
await instance.connect(signer);
```
### Call a Public Method
Similar to what we described in [this section](../how-to-test-a-contract#call-a-public-method), you can call a contract's public `@method` on the blockchain as follows:
```ts
// build and send tx for calling `foo`
const { tx, atInputIndex } = await instance.methods.foo(arg1, arg2, options);
console.log(`Smart contract method successfully called with txid ${tx.id}`);
```
Remember that the tx is not actually sent anywhere in a local test because we connect to a mock provider.
### Verify the Tx input for the method call
In the previous step, the signed `tx` for the contract call and its input index are returned. You can call `verifyScript` on the returned `tx` to verify that the contract method call at the given tx input index is successful.
```ts
let result = tx.verifyScript(atInputIndex)
console.log(result.success) // Output: true or false
```
### Integrate with a testing framework
You can use whatever testing framework you like to write unit tests for your contract. For example, a local test using [Mocha](https://mochajs.org/) is shown below:
```js
describe('Test SmartContract `Demo`', () => {
let signer;
let demo;
before(async () => {
// compile contract
await Demo.compile()
// create a test wallet as signer, connected to a dummy provider
signer = new TestWallet(privateKey, new DummyProvider())
// initialize a contract instance
demo = new Demo(1n, 2n)
// connect the instance to signer
await demo.connect(signer)
})
it('should pass the public method unit test successfully.', async () => {
// call `demo.methods.add` to get a signed tx
const { tx: callTx, atInputIndex } = await demo.methods.add(
// pass in the right argument
3n,
// set method call options
{
// Since `demo.deploy` hasn't been called before, a fake UTXO of the contract should be passed in.
fromUTXO: dummyUTXO
} as MethodCallOptions<Demo>
)
let result = callTx.verifyScript(atInputIndex)
expect(result.success, result.error).to.eq(true)
})
it('should pass the non-public method unit test', () => {
expect(demo.sum(3n, 4n)).to.be.eq(7n)
})
it('should throw error', () => {
return expect(
// Using the wrong argument when calling this function just results in an error.
demo.methods.add(4n, { fromUTXO: dummyUTXO })
).to.be.rejectedWith(/add check failed/)
})
})
```
## Test a Stateful Contract
Stateful contact testing is very similar to what we have described above. The only different is that you have to be aware of smart contract instance changes after method calls.
As described in the [Overview](./how-to-write-a-contract/stateful-contract.md#overview), for each method call, a tx contains new contract UTXO(s) with the latest updated state, i.e., the next instance. From the perspective of the current spending tx, the public `@method` of a contract instance is called in one of its inputs, and the next contract instance is stored in one (or more) of its outputs.
Now, let's look at how to test the `incrementOnChain` method call:
```ts
// initialize the first instance, i.e., deployment
let counter = new Counter(0n);
// connect it to a signer
counter.connect(dummySigner());
// set the current instance to be the first instance
let current = counter;
// create the next instance from the current
let nextInstance = current.next();
// apply the same updates on the next instance locally
nextInstance.increment();
// call the method of current instance to apply the updates on chain
const { tx: tx_i, atInputIndex } = await current.methods.incrementOnChain(
{
// Since `counter.deploy` hasn't been called before, a fake UTXO of the contract should be passed in.
fromUTXO: getDummyUTXO(balance),
// the `next` instance and its balance should be provided here
next: {
instance: nextInstance,
balance
}
} as MethodCallOptions<Counter>
);
// check the validity of the input script generated for the method call.
let result = tx_i.verifyScript(atInputIndex);
expect(result.success, result.error).to.eq(true);
```
In general, we call the method of a stateful contract in 3 steps:
### 1. Build the `current` instance
The `current` instance refers to the contract instance containing the latest state on the blockchain. The first instance is in the deployment transaction. In the above example, we initialize the `current` instance to be the first instance like this:
```ts
let current = counter;
```
### 2. Create a `next` instance and apply updates to it off chain
The `next` instance is the new instance in the UTXO of the method calling tx.
To create the `next` of a specific contract instance, you can simply call `next()` on it:
```ts
let nextInstance = instance.next();
```
It will make a deep copy of all properties and methods of `instance` to create a new one.
Then, you should apply all the state updates to the `next` instance. Please note that these are just local/off-chain updates and are yet to be applied to the blockchain.
```ts
nextInstance.increment();
```
This is the **SAME** method we call on chain in `incrementOnChain`, thanks to the fact that both the on-chain smart contract and off-chain code are written in TypeScript.
### 3. Call the method on the `current` instance to apply updates on chain
As described in [this section](#call-a-public-method), we can build a call transaction. The only difference here is that we pass in the `next` instance and its balance as a method call option in a stateful contract. So the method (i.e., `incrementOnChain`) have all the information to verify that all updates made to the `next` instance follow the state transition rules in it.
```ts
const { tx: tx_i, atInputIndex } = await current.methods.incrementOnChain(
{
// Since `counter.deploy` hasn't been called before, a fake UTXO of the contract should be passed in.
fromUTXO: getDummyUTXO(balance),
// the `next` instance and its balance should be provided here
next: {
instance: nextInstance,
balance
}
} as MethodCallOptions<Counter>
);
```
Finally, we can check the validity of the method call as before.
```ts
let result = tx_i.verifyScript(atInputIndex);
expect(result.success, result.error).to.eq(true);
```
### Running the tests
As before, we can just use the following command:
```sh
npm run test
```
Full code is [here](https://github.com/sCrypt-Inc/boilerplate/blob/master/tests/local/counter.test.ts).
You may visit [here](./how-to-deploy-and-call-a-contract/how-to-deploy-and-call-a-contract.md) to see more details on contract deployment and call.
|