Testing Guidelines

Ensuring the quality and security of smart contracts in Decentra projects requires rigorous testing. This page provides detailed guidelines on how to write and execute Foundry tests, along with a style guide to ensure consistency and comprehensiveness.

General Guidelines for Writing Foundry Tests

  • Setup and Teardown: Use a base test to initialize the state before each test and clean up after each test.

  • Coverage: Aim for 100% test coverage to ensure all parts of the code are tested.

  • Structure: Follow a consistent structure for all tests to maintain readability and organization.

Style Guide for Foundry Tests

1. Base Test:

The base test should set up the environment for the contracts and hold any state-setting logic and mocks. All other test contracts will inherit from this base test.

Example:

// BaseTest.sol
import "forge-std/Test.sol";
import "../src/MyContract.sol";

contract BaseTest is Test {
    MyContract internal myContract;

    function setUp() public virtual {
        myContract = new MyContract();
        // Additional setup logic, such as setting initial state or deploying mock contracts
    }
}

2. Inheriting Base Test:

All other test contracts should inherit from the base test and implement their specific tests. This ensures consistency and reduces duplication.

Example:

// MyContractTest.sol
import "./BaseTest.sol";

contract MyContractTest is BaseTest {
    function setUp() public override {
        super.setUp();
        // Additional setup specific to this test contract
    }

    function test_addRewards() public {
        // Test logic for addRewards function
    }

    function test_RevertWhen_addRewardsIsNotCalledByOwner() public {
        // Test logic for revert case when addRewards is not called by the owner
    }
}

3. Test Naming and Structure:

Tests should be ordered in the same way as the functions in the contract and should cover possible edge cases and security issues. Use descriptive names for test functions to clearly indicate their purpose.

Example:

// ExampleTest.sol
import "./BaseTest.sol";

contract ExampleTest is BaseTest {
    function test_transfer() public {
        // Test logic for transfer function
    }

    function test_RevertWhen_transferToZeroAddress() public {
        // Test logic for revert case when transferring to the zero address
    }

    function test_transferFrom() public {
        // Test logic for transferFrom function
    }

    function test_RevertWhen_transferFromWithoutApproval() public {
        // Test logic for revert case when transferFrom is called without approval
    }
}

4. Coverage:

Ensure that your tests cover 100% of the code. This includes all functions, branches, and lines. Use tools to measure test coverage and identify any untested parts of the code.

Writing Effective Tests

  • Unit Tests: Test individual functions in isolation to ensure they behave as expected.

  • Integration Tests: Test interactions between multiple functions or contracts to ensure they work together correctly.

  • Edge Cases: Test unusual or extreme inputs to ensure the contract handles them gracefully.

  • Revert Tests: Test that the contract correctly reverts in invalid scenarios using descriptive test names such as test_RevertWhen_<condition>().

  • Security Tests: Include tests for common security vulnerabilities, such as reentrancy, overflow/underflow, and unauthorized access.

Running Tests

To run the tests, use the Foundry framework’s command-line tools. Ensure that all tests pass before submitting your code.

  • Running All Tests:

forge test
  • Running a Specific Test:

forge test --match-path test/MyContractTest.sol
  • Measuring Coverage:

forge coverage

Example Test File Structure

contracts/
  - MyContract.sol
test/
  - BaseTest.sol
  - MyContractTest.sol
  - ExampleTest.sol

Optimization Note

The codebase and tests should be optimized to compile without the --via-ir flag. The Intermediate Representation (IR) compiler flag can increase the complexity and potentially introduce inefficiencies. Ensuring your code is compatible with standard compilation settings maintains optimal performance, simplifies the compilation process, and avoids unnecessary dependencies. By following these testing guidelines and style guide, developers can ensure that their smart contracts are thoroughly tested, secure, and reliable. This contributes to the overall integrity and robustness of Decentra projects.