Настройка тестовой среды для смарт-контрактов
Проект со зрелой кодовой базой, но без настроенной тестовой среды — это ручная проверка каждого изменения. Новый разработчик в команде тратит день, чтобы запустить первый тест. CI упал, никто не знает почему. Деплой на testnet вместо локального — 10 минут ожидания на каждую итерацию.
Настройка тестовой среды — это разовая инвестиция, которая окупается на второй неделе активной разработки.
Выбор фреймворка под задачу
Для Solidity-проектов сейчас два реальных варианта: Foundry и Hardhat. Они решают разные задачи и часто используются вместе.
| Параметр | Foundry | Hardhat |
|---|---|---|
| Язык тестов | Solidity | TypeScript/JavaScript |
| Скорость | Очень быстро (revm на Rust) | Медленнее |
| Fuzz-тестирование | Встроено | Только через плагины |
| Mainnet fork | vm.createFork() |
--fork-url |
| Frontend интеграция | Сложнее | Проще (ethers.js) |
| Скрипты деплоя | Solidity scripts | TypeScript + ethers.js |
| Отладка транзакций | forge debug |
console.log() в контракте |
Наш стандарт: Foundry для unit и fuzz-тестов, Hardhat для скриптов деплоя и интеграции с frontend. Оба конфига сосуществуют в одном репозитории.
Структура проекта
project/
├── foundry.toml # Foundry конфиг
├── hardhat.config.ts # Hardhat конфиг
├── contracts/
│ ├── core/
│ └── interfaces/
├── test/
│ ├── unit/ # Foundry тесты
│ ├── integration/ # Форк-тесты
│ └── invariant/ # Invariant тесты
├── script/ # Foundry деплой скрипты
├── deploy/ # Hardhat деплой скрипты
└── fixtures/ # Общие фикстуры
Локальная сеть
Anvil (входит в Foundry) — локальный EVM-нод. Быстрее Ganache, активно поддерживается. Для разработки запускаем в режиме форка mainnet или testnet:
# Форк Ethereum mainnet с конкретным блоком (воспроизводимость)
anvil --fork-url $MAINNET_RPC --fork-block-number 19500000
# Форк с предопределёнными аккаунтами и балансами
anvil --fork-url $MAINNET_RPC --accounts 10 --balance 10000
Форк-тестирование — единственный способ проверить интеграцию с Uniswap, Aave, Chainlink без деплоя на testnet. Транзакция в локальном форке — мгновенно. На Sepolia — 12-15 секунд.
Моки и фикстуры
Для изоляции тестов используем иерархию фикстур:
// BaseFixture.sol — общие зависимости
abstract contract BaseFixture is Test {
MockERC20 token;
MockChainlinkOracle oracle;
function setUp() public virtual {
token = new MockERC20("Test", "TST", 18);
oracle = new MockChainlinkOracle(2000e8); // $2000 price
}
}
// ProtocolFixture.sol — деплой тестируемого протокола
contract ProtocolFixture is BaseFixture {
Protocol protocol;
function setUp() public override {
super.setUp();
protocol = new Protocol(address(token), address(oracle));
}
}
Не используем vm.mockCall для основных зависимостей — это хрупко и не проверяет интерфейс. Создаём полноценные mock-контракты с минимальной реализацией.
CI/CD конфигурация
GitHub Actions конфиг для Foundry:
name: Tests
on: [push, pull_request]
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Install Foundry
uses: foundry-rs/foundry-toolchain@v1
- name: Run unit tests
run: forge test --match-path "test/unit/*" -vvv
- name: Run integration tests
run: forge test --match-path "test/integration/*" --fork-url ${{ secrets.MAINNET_RPC }}
- name: Coverage check
run: forge coverage --min-line-coverage 80
Разделяем unit и integration тесты — unit-тесты должны работать без RPC-ключей. Интеграционные — только на PR в main.
Testnet конфигурация
Для тестирования в реальной сети настраиваем multi-network конфиг в Hardhat:
networks: {
sepolia: {
url: process.env.SEPOLIA_RPC,
accounts: [process.env.DEPLOYER_KEY],
chainId: 11155111,
},
polygon_amoy: {
url: process.env.AMOY_RPC,
accounts: [process.env.DEPLOYER_KEY],
chainId: 80002,
},
}
Faucets для основных testnet: Sepolia Faucet (Alchemy/Chainlink), Amoy Faucet (официальный Polygon).
Сроки
Базовая настройка (Foundry + Hardhat, Anvil, CI) — 1 рабочий день. С кастомными моками под конкретный протокол и фикстурами — 1-2 дня. Для проектов с несколькими чейнами (EVM + Solana) — 2-3 дня.







