Testing a solidity house-swap contract using HardHat and Typescript
Tags: hardhat, typescript, smartcontract, solidity
Published: 2023-10-02 14:28:53

In the previous post of this series, we saw how to build an smart-contract using solidity which managed a house-swap between two users.

In this post, we are going to learn how to test the contract using [hardhat](https://hardhat.org/) and [typescript](https://www.typescriptlang.org/).

## Preparing hardhat
Before installing hardhat you have to install node in your computer. You can see how to install node in this [link](https://hardhat.org/tutorial/setting-up-the-environment). It is a simple process.
Once you have node installed, you are ready to install hardhat. Let's do it using the next steps:

### Initialize a node project
To initialize a node project, create a folder on which you want to hold your project, move into the new folder and then start a node project.

```shell
mkdir <myprojectfolder>
cd <myprojectfolder>
npm init
```

### Install hardhat
To install hardhat, keep on your project folder and run:

```shell
npm install --save-dev hardhat
npx hardhat init
```
The first command will install hardhat as a dev dependency in your project and the next one will start a hardhat project. 

> Choose *Create an empty hardhat.config.js* after executing the *hardhat init* command.

Now you have initialized a hardhat project, move the solidity contract (the file with a *.sol* extension) to the contracts directory. If that directory does not exist, create it.

## Install Typescript
To install and use typescript, you will have to install the *@nomicfoundation/hardhat-toolbox* plugin provided by hardhat. This plugin contains all the common features to work with hardhat. You can learn more about it [here](https://hardhat.org/hardhat-runner/plugins/nomicfoundation-hardhat-toolbox).

After installing this plugin, you are ready to move to typescript. First of all, find the file *hardhat.config.js* on your project root folder and change its name to *hardhat.config.ts*.

```shell
mv hardhat.config.js hardhat.config.ts
```
After doing it, you will have to change the contents to adapt them to typescript. You can copy the contents from the [github project](https://github.com/icolomina/house-swap)

```typescript
import { HardhatUserConfig } from "hardhat/config";
import "@nomicfoundation/hardhat-toolbox";
import "@typechain/hardhat"

const config: HardhatUserConfig = {
  solidity: "0.8.19",
};

export default config;
```
> As you can see in the code above, we import *hardhat-toolbox* and also *typechain/hardhat*, but, what is typechain ?. Typechain is a library that, basically, generate typescript types for your contracts and its functions. This is really useful since you do not need to remember the contract function names. They will be listed by your ide as if they were methods of a class. 
In the next sections, we will see how to generate this types ans use them.
 
Finally, you only have to create a file named *tsconfig.json* in your project root folder and paste the following content:

```json
{
    "compilerOptions": {
      "target": "es2020",
      "module": "commonjs",
      "esModuleInterop": true,
      "forceConsistentCasingInFileNames": true,
      "strict": true,
      "skipLibCheck": true,
      "resolveJsonModule": true
    }
}
```
## Write the tests
The tests should be located under *test* directory so, if it is no created, create the folder under your project root folder. Then, move into this new folder and create a file named *HouseSwap.ts*.
This file will hold our contract tests. In this post we are going to show and explain each test separately. Check the [github project](https://github.com/icolomina/house-swap) to see the complete file.

### Imports and Common variables for all tests

```typescript
import { expect } from "chai";
import { ethers } from "hardhat";
import { HouseSwap } from "../typechain-types";
import { SignerWithAddress } from "@nomiclabs/hardhat-ethers/signers";
```

```typescript
let houseSwapContract: HouseSwap;
let owner: SignerWithAddress;
let addr1: SignerWithAddress;
let addr2: SignerWithAddress;
let targetAddress: string;
let targetAddress2: string;
let house: HouseSwap.HouseStruct;
let houseToSwap: HouseSwap.HouseStruct;
let amountPayOriginToTarget: number;
let amountPayTargetToOrigin: number;

beforeEach(async function() {
  [owner, addr1, addr2] = await ethers.getSigners();
  targetAddress   = await addr1.getAddress();
  targetAddress2  = await addr2.getAddress();
  house = {houseType: "semi-detached house", value: 16698645, link: "https://example.com", propietary: targetAddress};
  houseToSwap = {houseType: "duplex", value: 16698645, link: "https://example2.com", propietary: targetAddress}
  amountPayOriginToTarget = 0;
  amountPayTargetToOrigin = 0;

  houseSwapContract = await ethers.deployContract("HouseSwap") as HouseSwap;
  });
```

Let's start with the imports. As we can see, the first one imports [chai](https://www.chaijs.com/). Chai is a javascript assertion library (which is included with the hardhat installation).
> As a test runner, hardhat uses [mocha](https://mochajs.org/) which is also included with the hardhat installation. Mocha is internally called by hardhat when you run the test suite (we will see it later).

The second one imports [ethers](https://docs.ethers.org/v6/). Ethers is a library for interacting with the ethereum blockchain and its escosystem. In this post, we will use ethers to get the test addresses and deploy the contract.

The third one is really important. Here is where *typechain* comes into play. This line imports *HouseSwap* from *typechain-types* directory. HouseSwap type contains all contract types and functions mapped to typescript types. So, how we can generate such folder?. Let's do a break here and show it.
As we have imported typechain in our *hardhat.config.ts* file, this folder will be automatically generated after compiling the contract. 

```shell
npx hardhat compile
```
> If you import typechain after compiling the contract, you will have to clean before compiling again to generate the typechain folder.

```shell
npx hardhat clean
npx hardhat compile
```

Going back to the imports, the last one imports the *SignerWithAddress* class. We will assing this type to the vars that we will use as addresses since the addresses are retrieved using ethers *getSigners* method and the addresses returned by this method are *SignerWithAddress* instances. 

Now, let's move to the next piece of code. There we can see the *beforeEach* function. This function is executed before the execution of each test. In this case, this function creates a set of variables which will be available for all the tests, and deploys the contract so we can invoke its functions. Let's explain those variables one by one:

- **[owner, addr1, addr2]**: These are the addresses which we are going to use to communicate with the contract. The *owner* address will be the one which will deploy the contract.
- **targetAddress** and **targetAddress2**: These variables holds the *addr1* and *addr2* raw addresses. When we send an address as a function parameter to solidity, we have to send it as a string.
- **house** and *houseToSwap*: These variable are  *HouseSwap.HouseStruct* type variable. This type contains the house information which our contract expects as a parameter in some of its functions.
- **amountPayOriginToTarget**: This is a number variable which specifies if the origin has to pay some amount to the target.
- **amountPayTargetToOrigin**: This is a number variable which specifies if the target has to pay some amount to the origin.

After declaring and initialize the variables, the *beforeEach* function deploys the contract using *ethers*. After deployed, we get a *HouseSwap* type (thanks to typechain) which holds all the mapped contract functions.

### Expects initial status to be PENDING

```typescript
it("Expects initial status to be P", async function () {
    expect(await houseSwapContract.getStatus()).to.equal(0);
});
```

This test is pretty straightforward. It simply ensures that contract status is *PENDING* just after the contract is deployed. If we go back to the contract code, we could see that the contract sets such status in the constructor (which is executed when the contract is deployed).

### Should reject adding a new offer since contract has not initialized

```typescript
await expect(houseSwapContract.connect(addr1).addOffer(house, amountPayOriginToTarget, amountPayTargetToOrigin)).to.be.revertedWith('An offer has been already accepted or contract has not been initialized');
```

In this case, we ensure that the contract funcion reverts since the contract has not been initialized and we cannot send an offer yet.

> Let's look at the use of the *connect* method. It allows us to change the contract caller.

### Should emit a *NewOffer* event after sending an offer

```typescript
await houseSwapContract.initialize(house);
expect(await houseSwapContract.getStatus()).to.equal(1);
await expect(houseSwapContract.connect(addr1).addOffer(house, amountPayOriginToTarget, amountPayTargetToOrigin)).to.emit(houseSwapContract, "NewOffer");
```
In this test, we check two cases. 
- We ensure that the contract status holds the *INITIALIZED* value after calling the *initialize* function.
- Then, we ensure that a *NewOffer* event is emitted after sending an offer using *addOffer* method.

### Should not accept the offer since only the owner can accept an offer

```typescript
await houseSwapContract.initialize(house);
await expect(houseSwapContract.connect(addr1).addOffer(houseToSwap, amountPayOriginToTarget, amountPayTargetToOrigin)).to.emit(houseSwapContract, "NewOffer");
await expect(houseSwapContract.connect(addr2).acceptOffer(targetAddress, house, amountPayOriginToTarget, amountPayTargetToOrigin)).to.be.revertedWith('Required contract owner');
```
In this test, we check that an offer cannot be accepted since the address which is calling *acceptOffer* is not the contract owner.

### Should accept an offer and reject others

```typescript
await houseSwapContract.initialize(house);
await expect(houseSwapContract.connect(addr1).addOffer(houseToSwap, amountPayOriginToTarget, amountPayTargetToOrigin)).to.emit(houseSwapContract, "NewOffer");
houseSwapContract.acceptOffer(targetAddress, houseToSwap, amountPayOriginToTarget, amountPayTargetToOrigin);
expect(await houseSwapContract.getStatus()).to.equal(2);
await expect(houseSwapContract.connect(addr1).addOffer(houseToSwap, amountPayOriginToTarget, amountPayTargetToOrigin)).to.be.revertedWith('An offer has been already accepted or contract has not been initialized');
```
In this case, we also test two cases:

- We initialize the contract, send an offer and accept it. The we ensure that the contract status holds the value *ACCEPTED*.
- Then, we ensure that if we try to send another offer, the function reverts since an offer has already accepted.

### Should not perform swap because sender is not who sent the offer

```typescript
await houseSwapContract.initialize(house);
await houseSwapContract.connect(addr1).addOffer(houseToSwap, amountPayOriginToTarget, amountPayTargetToOrigin);
await houseSwapContract.acceptOffer(targetAddress, houseToSwap, amountPayOriginToTarget, amountPayTargetToOrigin);
await expect(houseSwapContract.connect(addr2).performSwap()).to.be.revertedWith('Only target user can confirm swap');
```
In this case, we add an offer and the owner accepts it. Then, when the *performSwap* function is called, as the sender is not the same that sent the accepted offer, the function reverts.

### Should perform swap without extra transfers

```typescript
await houseSwapContract.initialize(house);
await houseSwapContract.connect(addr1).addOffer(houseToSwap, amountPayOriginToTarget, amountPayTargetToOrigin);
await houseSwapContract.acceptOffer(targetAddress, houseToSwap, amountPayOriginToTarget, amountPayTargetToOrigin);
await houseSwapContract.connect(addr1).performSwap();

expect(await houseSwapContract.getStatus()).to.equal(3);
```

In this test, we ensures that contract change the status to *FINISHED* value after performing swap. As *amountPayOriginToTarget* and *amountPayTargetToOrigin* holds a 0 value, no transfers need to be sent.

### Deposit should fail since owner must send funds

```typescript
await houseSwapContract.initialize(house);
await houseSwapContract.connect(addr1).addOffer(houseToSwap, 1, amountPayTargetToOrigin);
await houseSwapContract.acceptOffer(targetAddress, houseToSwap, 1, amountPayTargetToOrigin);
await expect(houseSwapContract.connect(addr1).deposit({ value: ethers.utils.parseEther('1') })).to.be.revertedWith("Origin must deposit enougth funds");
```

Now, we test *deposit* function. In this case, as *amountPayOriginToTarget* is greater that 0, the origin must deposit enought ETH so the contract can send the transfer. The test will revert since it is not the owner who tries to deposit.

### Deposit should fail since target must send funds

```typescript
await houseSwapContract.initialize(house);
await houseSwapContract.connect(addr1).addOffer(houseToSwap, amountPayOriginToTarget, 1);
await houseSwapContract.acceptOffer(targetAddress, houseToSwap, amountPayOriginToTarget, 1);
await expect(houseSwapContract.deposit({ value: ethers.utils.parseEther('1') })).to.be.revertedWith("Target must deposit enougth funds");
```

This test the same case as the last one, but in this case it reverts because the target should be who sends the deposit.

### Owner deposit should work

```typescript
await houseSwapContract.initialize(house);
await houseSwapContract.connect(addr1).addOffer(houseToSwap, 1, amountPayTargetToOrigin);
await houseSwapContract.acceptOffer(targetAddress, houseToSwap, 1, amountPayTargetToOrigin);
await expect(houseSwapContract.deposit({ value: ethers.utils.parseEther('1') })).to.emit(houseSwapContract, "BalanceUpdated");
```

In this case, as the deposit is sent by the right address, it  is done successfully and the function emits a *BalanceUpdated* event.

### Swap should fail because funds deposited are not enought

```typescript
await houseSwapContract.initialize(house);
await houseSwapContract.connect(addr1).addOffer(houseToSwap, 1, amountPayTargetToOrigin);
await houseSwapContract.acceptOffer(targetAddress, houseToSwap, 1, amountPayTargetToOrigin);
await houseSwapContract.deposit({ value: ethers.utils.parseEther('0.8') });

await expect(houseSwapContract.connect(addr1).performSwap()).to.be.revertedWith("Deposit has not been sent or is lower than required")
```

In this test, we succcessfully deposit 0.8 ETH but, as origin must sent 1 ETH to the target, the swap process reverts as there are not enought funds.

### Swap transferring funds from origin to target should be done

```typescript
await houseSwapContract.initialize(house);
await houseSwapContract.connect(addr1).addOffer(houseToSwap, 1, amountPayTargetToOrigin);
await houseSwapContract.acceptOffer(targetAddress, houseToSwap, 1, amountPayTargetToOrigin);
await houseSwapContract.deposit({ value: ethers.utils.parseEther('1.05') });

await houseSwapContract.connect(addr1).performSwap();
expect(await houseSwapContract.getStatus()).to.equal(3);
```
Finally, as we deposit 1.05 ETH and origin must sent to target 1 ETH, the swap process is performed and the contract change its status to *FINISHED*.

## Executing the tests

To execute the tests, we have to compile the contract first. We've shown how to do it at the begining of the article. Let's remember again here:

```shell
npx hardhat compile
```

Then, after the contract is compiled without errors, we have to execute the following command to run the tests. Here, is when hardhat relies on mocha to run them (this is transparent for us).

```shell
npx hardhat test
```

After executing the tests, you should see the following output:


![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/lm2tsb0cw6hlza3y7yay.png)

## Conclusion

This post, as a continuation of the [previous one](https://dev.to/icolomina/creating-a-house-swap-contract-using-solidity-and-hardhat-part-1-4jeh), has shown us how to test a contract using hardhat. Those tests tries to ensure the functions works as expected but also tries to test that the contract reverts when it has to.
After explaining the tests, we have seen how to use hardhat shell to compile and run the tests.
In the next post, we will learn how to deploy the contract to a test network and how to call the contract functions.