# Zero to Hero in Foundry - Part 3: Testing Fundamentals

> ## Recap

> Welcome back folks! Part 2 took us through some pretty good stuff. We learnt how to use `anvil` to run a local simulation of a blockchain in our systems. Then we used a simple `forge` command to deploy our contract to our local chain. And finally used `cast` to interact with our deployed contract

> Please read it if you haven't already before continuing

> [**Foundry: Zero to Hero - Part 2**](https://dev.to/abhiramelf/get-from-zero-to-hero-in-foundry-part-2-deploy-interact-2ea4)

> **Learn Web3 through live and interactive challenges at** [**Web3Compass**](https://www.web3compass.xyz/)

## Today's Outcome

**Topic of focus: Testing fundamentals**

* **Write a SimpleBank.sol contract**
    
* **Let users deposit & withdraw funds**
    
* **Use function modifiers to restrict access to only owner of the deployed contract**
    
* **And the most important bit - Write extensive tests for it!**
    

**SHALL WE?!**

![Shall we](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/oxi3c61bk3y1mjp3c5ol.gif align="center")

---

## 📝 Writing the Contract

By now we know how to initiatize our project using `forge`. So, let’s jump right into the contract code

Here’s the `SimpleBank.sol` contract

```solidity
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.30;

// Simple bank contract that allows users to deposit, withdraw, and check their balances.
contract SimpleBank {
    address public owner; // Owner of the contract
    mapping(address => uint256) private balances; // Mapping of user addresses to their balances

    constructor(address _owner) {
        owner = _owner;
    }

    // Set owner
    function setOwner(address newOwner) onlyOwner public {
        owner = newOwner;
    }

    // Deposit Ether into the bank
    function deposit() public payable {
        require(msg.value > 0, "Deposit amount must be greater than zero");
        balances[msg.sender] += msg.value;
    }

    // Withdraw Ether from the bank, only owner can call
    function withdraw(uint256 amount) onlyOwner public {
        require(balances[owner] >= amount, "Insufficient balance");
        balances[owner] -= amount;
        payable(owner).transfer(amount);
    }

    // Get the balance of the owner
    function getBalance() onlyOwner public view returns (uint256) {
        return balances[owner];
    }

    // Modifier to restrict access to the owner
    modifier onlyOwner() {
        require(msg.sender == owner, "Not the contract owner");
        _;
    }
}
```

### **SimpleBank Contract Explained**

Our contract lets a single owner safely deposit and withdraw Ether. Here’s how it works:

* **Owner:** The contract is created with an owner address. Only this owner can withdraw funds or check the balance.
    
* **Deposit:** Anyone can deposit Ether by calling the `deposit()` function. The contract keeps track of how much Ether each address has deposited.
    
* **Withdraw:** Only the owner can withdraw Ether using the `withdraw()` function. The contract checks that the owner has enough balance before allowing the withdrawal.
    
* **Check Balance:** The owner can check their balance with the `getBalance()` function.
    
* **Change Owner:** The owner can transfer ownership to another address using `setOwner()`.
    
* **Security:** The `onlyOwner` modifier makes sure that only the owner can perform sensitive actions like withdrawing or checking the balance.
    

### ***Time to test!!***

![](https://cdn.hashnode.com/res/hashnode/image/upload/v1755086285086/cc90b4a2-b463-4cfb-b3d6-090fbcd619ac.gif align="center")

---

## 🧪 Testing the Contract

Let’s extensively test our `SimpleBank.sol` contract.

`SimpleBank.t.sol` test file

```solidity
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.30;

import {Test, console} from "forge-std/Test.sol";
import {SimpleBank} from "../src/SimpleBank.sol";

contract SimpleBankTest is Test {
    SimpleBank public bank;

    // Set up the contract
    function setUp() public {
        bank = new SimpleBank(msg.sender);
    }

    // Test setting a new owner
    function testSetOwnerByOwner() public {
        vm.startPrank(bank.owner());
        bank.setOwner(address(0));
        assertEq(bank.owner(), address(0));
        vm.stopPrank();
    }

    // Test setting a new owner by non-owner
    function testSetOwnerByNonOwner() public {
        vm.expectRevert("Not the contract owner");
        bank.setOwner(address(0));
    }

    // Test deposit by owner
    function testDepositByOwner() public {
        vm.startPrank(bank.owner());
        uint256 initialBalance = bank.getBalance();
        bank.deposit{value: 1 ether}();
        uint256 newBalance = bank.getBalance();
        assertEq(newBalance, initialBalance + 1 ether);
        vm.stopPrank();
    }

    // Test deposit by non-owner
    function testDepositByNonOwner() public {
        vm.prank(bank.owner());
        uint256 initialBalance = 0;
        bank.deposit{value: 1 ether}();
        vm.prank(bank.owner());
        uint256 newBalance = bank.getBalance();
        assertEq(newBalance, initialBalance + 1 ether);
    }

    // Test withdraw by owner
    function testWithdrawByOwner() public {
        vm.startPrank(bank.owner());
        bank.deposit{value: 1 ether}();
        uint256 initialBalance = bank.getBalance();
        bank.withdraw(0.5 ether);
        uint256 newBalance = bank.getBalance();
        assertEq(newBalance, initialBalance - 0.5 ether);
        vm.stopPrank();
    }

    // Test withdraw with insufficient balance
    function testWithdrawNoBalance() public {
        vm.startPrank(bank.owner());
        bank.deposit{value: 1 ether}();
        vm.expectRevert("Insufficient balance");
        bank.withdraw(2 ether);
        vm.stopPrank();
    }

    // Test withdraw by non-owner
    function testWithdrawByNonOwner() public {
        bank.deposit{value: 1 ether}();
        vm.expectRevert("Not the contract owner");
        bank.withdraw(0.5 ether);
    }

    // Test get balance by owner
    function testGetBalanceByOwner() public {
        vm.startPrank(bank.owner());
        bank.deposit{value: 1 ether}();
        uint256 balance = bank.getBalance();
        assertEq(balance, 1 ether);
        vm.stopPrank();
    }

    // Test get balance by non-owner
    function testGetBalanceByNonOwner() public {
        vm.expectRevert("Not the contract owner");
        bank.getBalance();
    }

    // Fuzz test for withdraw
    function testFuzz_Withdraw(uint8 amount) public {
        vm.startPrank(bank.owner());
        bank.deposit{value: 10000 ether}();
        uint256 initialBalance = bank.getBalance();
        bank.withdraw(amount);
        uint256 newBalance = bank.getBalance();
        assertEq(newBalance, initialBalance - amount);
        vm.stopPrank();
    }
}
```

### **SimpleBankTest Explained**

Our test contract checks that the `SimpleBank.sol` smart contract works as expected. Here’s what each part does:

* **Setup:** Before each test, a new bank is created with the test contract as the owner.
    
* **Owner Functions:** Tests that only the owner can change ownership, withdraw funds, and check the balance. If anyone else tries, the contract should revert with an error.
    
* **Deposits:** Checks that both the owner and other users can deposit Ether, and that the balance updates correctly.
    
* **Withdrawals:** Verifies that the owner can withdraw funds, but not more than their balance. Also checks that non-owners cannot withdraw.
    
* **Balance Checks:** Ensures only the owner can see their balance.
    
* **Fuzz Testing:** Randomly tests withdrawals with different amounts to make sure the contract handles all cases safely.
    

Now, instead of running the `forge test` command, let’s try something else. Try running

```bash
forge coverage
```

We’ll see that the tests all run correctly, but we also see a coverage report of the entire test suite

![](https://cdn.hashnode.com/res/hashnode/image/upload/v1755087178371/06b1551b-3864-4c8a-9e31-3f3b936f025b.png align="center")

Coverage measures how much of our smart contract code is actually tested by our test suite. It shows which lines of code were executed during testing and which lines were missed.

* **Why is it useful?**  
    High coverage means our tests are checking most parts of your contract, making it less likely that bugs or vulnerabilities go unnoticed.
    
* **How do we check coverage?**  
    In Foundry, we can run `forge coverage` to see a report. This report highlights which functions and lines were tested and gives us a percentage score.
    
* **What should we aim for?**  
    Aim for as close to 100% coverage as possible, but remember: coverage alone doesn’t guarantee your contract is bug-free. Good tests and thoughtful scenarios are just as important.
    

![](https://cdn.hashnode.com/res/hashnode/image/upload/v1755086452913/14a2fc06-d5fe-419f-b3ed-61f6684d124e.gif align="center")

---

## And that's a Wrap! (For now 😬)

By now we should have

* Our SimpleBank.sol contract written
    
* We have written proper & extensive tests for it
    
* Checked our test coverage and got close to 100%
    

## What's Next?

Part 4, we'll be

* Writing a 1:1 token swap contract
    
* Focusing majorly on writing Fuzz and Invariant tests for it
    

**Don't miss it! I'll see you soon!** 😎
