Smart contract testing (integration tests)

The upgrade is already on mainnet, but the contract bundle behaves differently than on the staging environment. We design and run end-to-end integration scenarios turnkey: from role analysis to a report with reproducible steps. Verifying end-to-end paths before deployment removes manual runs from the team and eliminates an entire class of release failures.

Blockchain Development Services

Frequently Asked Questions

Latest works

  • Development of a web application for FEEDME
    Development of a web application for FEEDME
    1375
  • Development of an online store for the company FURNORO
    Development of an online store for the company FURNORO
    1349
  • B2B Advance company logo design
    B2B Advance company logo design
    794
  • Development of a web application for Enviok
    Development of a web application for Enviok
    1087
  • AIDER company logo development
    AIDER company logo development
    1032
  • CRM development for Chasseurs
    CRM development for Chasseurs
    1129

Integration testing of smart contracts and its blind spots

Unit tests confirm that an individual contract works according to its specification. The value of a protocol is born at the seams — between token and storage, between calculation and payout, between logic and an external data source.

Integration testing of smart contracts checks the entire bundle in the order of calls in which the Ethereum network will see it within a single transaction.

Failures at the seams are almost invisible individually: the recipient believes the payment went through, the sender has already recorded the debit, and a reversal occurred between them.

State desynchronization surfaces later, when limits, shares, or rewards are calculated from diverging data, and recovery has to be done manually.

A separate area is the order of calls and reentrancy. If a payout goes to an external address before the balance is updated, the recipient manages to call the function again and withdraw more than allowed. Such protection is verified only at the level of the call chain, not a single contract.

Missed scenario branches are the second typical hole.

The successful path and complete failure are checked, but not partial: a failure in the middle reverts the transaction entirely, while the external service has already received the event and recorded the operation in its database.

The discrepancy between the network and the accounting system is discovered during reconciliation, that is, too late.

Incidents after an upgrade cost the most: the logic is changed in a new contract, while adjacent modules continue to call the old one, and part of the state remains unaccounted for. We explain what exactly we check on the bundle and how we catch these discrepancies before rollout.

What end-to-end scenario testing provides before mainnet deployment

An end-to-end scenario runs the entire chain — from calling a public method to the state change and event read by an external service. We spin up an isolated testbed with a local copy of the mainnet state, pin the block number, and deploy the entire bundle of contracts.

This gives reproducibility: the same set of transactions on the same state snapshot produces an identical result for any engineer and in any build environment.

The full set of scenarios runs in minutes instead of hours of manual checks: parallel runs on testbed copies do not interfere with each other, and the state is rolled back by snapshot.

Repeatability of results is the main effect: a failure means a real logic error, not a difference in the test account balance or block time.

This removes entire classes of failures: mismatched permissions between roles, incorrect order of calls during transfers between contracts, reentrancy, overflow when calculating shares, mismatch between events and actual state.

The share of defects reaching users drops — they are caught where fixing takes hours, not incident investigation on mainnet.

An upgrade is checked by the same loop: the scenario applies the new version to a snapshot of the current state and runs the migration.

It is visible that old data is read correctly, and downtime during the upgrade is limited to a single replacement transaction, rather than searching for the cause of a failure after it.

Run formats: local testbed, closed network, mainnet

The choice of environment determines which defects a run is even capable of catching. A local testbed provides complete isolation of test data and seconds per cycle, but does not reproduce the behavior of real liquidity pools, oracles, and network load.

A closed network with the same contracts and state snapshots covers the middle ground: behavior is close to production, and data does not overlap with live data.

What we compare

Option When suitable Environment limitations
Local testbed Contract logic, access permissions, value boundaries, regression after every code change No real load, external calls are replaced with stubs
Closed network Scenarios between multiple contracts, state migrations, time-dependent conditions Nodes must be spun up and maintained, state is rebuilt for each run
Run on mainnet Final check before launch: real external calls, reliable gas usage estimate Transactions are irreversible, no test data isolation — read-only or separate accounts only

The main selection criterion is isolation of test data. A mainnet run is justified when checking integration with external contracts and the actual cost of operations, rather than logic.

A closed network usually becomes the working mode for integration scenarios: it is the only one that provides both reliability and the ability to roll back state.

The environment also imposes limitations: different node versions, access point limits, and block inclusion timings may diverge, so we keep test sets separate for each mode.

Work stages: from scenario analysis to testbed testing

Integration testing of smart contracts begins not with test code, but with analysis of the bundle: how contracts call each other, what they expect from external services, and what state the system remains in after each operation.

An error in the order of calls or in the expected state of a neighboring contract manifests after deployment, when fixing it requires migration. That is why our order of work is: first the bundle map, then the tests.

  1. Scenario audit — collect user paths and roles: who calls a method, which limits and states are checked, and where failures are expected. The output is a list of scenarios prioritized by risk.
  2. Call map — record the connections between contracts, tokens, and external data sources: who calls whom, what values are passed, and what is returned. The map immediately shows places where the test must also check side effects on state.
  3. Scenario scheme — turn each path into a set of steps with the expected state before and after the call, so a discrepancy can be read precisely, rather than as "the whole run failed."
  4. Environment setup — the testbed replicates the target network by fee parameters, confirmation order, and dependency versions; separately, we spin up a test network for checks that cannot be reproduced locally.
  5. Run and discrepancy analysis — launch scenarios in batches, log inputs, state, and events, and trace every discrepancy to its cause: an error in the contract, in the environment, or in the test expectation itself.
  6. Acceptance criteria — before the final run, agree on which scenarios are mandatory and what result is considered successful, so acceptance does not turn into negotiations.
  7. Handover — provide the test set, testbed configurations, and a report with path coverage, so your team can repeat the run without us.

Output artifacts: scenario sets, reports, policies

We hand over not a folder with tests, but a working loop: a runnable set of scenarios, reproducible data, and rules by which your team runs and updates checks after every contract change. Below is what physically remains in your repository and documentation.

  • Integration testing scenario set — grouped by domain: token, access, payouts, external calls. Each scenario runs separately, so a failed check points to a specific function, not to "something broke."
  • Fixtures and data documentation — describes what state the testbed starts from: a mainnet snapshot at the required block height, funded addresses, preconfigured roles. The testbed starts with one command and gives the same result for a developer and on the build server.
  • Module interaction scheme — a map of calls between contracts and external services, marked with what is covered by checks and what remains under manual control.
  • Run report — passed and failed scenarios, execution time, change in gas usage compared to the previous run, branch coverage. Regression is visible before rollout, not after.
  • Upgrade policy — the mandatory set of scenarios when a contract is changed, who confirms the run, how the new version is recorded, and how to roll back to the previous one.
  • Roles and access — a permission matrix for changing scenarios, testnet keys, and separation of access to the testbed and to production configurations.

Case study: how a class of failures was eliminated during an upgrade release

The client was releasing upgrades for a bundle of six contracts: token, pool, oracle, voting module, and two auxiliary ones.

The modules were tested separately, and joint behavior was checked manually on a test network — the release took two days, and about a third of releases had to be rolled back because interface incompatibility surfaced after publication.

What we changed

We assembled a single run: fixtures deploy the entire bundle in an isolated network, including an upgrade with preservation of accumulated storage, so not only the new module code is checked, but also compatibility with already recorded data.

We added invariant checks — sum of shares, role permissions, issuance limits — they are run after every operation, not once at the end. The scenario for rolling back to the previous version became part of the run, rather than an emergency procedure by instruction.

Deployment and execution are built on Hardhat: the same scenarios run for an engineer locally and in the build — there are no discrepancies between environments.

Metric Before After
Share of releases with rollback every third one in fourteen
Time for full bundle check two days manually about forty minutes
Time to roll back to previous version three to four hours up to ten minutes with one scenario

Now a release does not wait for a window at the end of the week: the run starts on every change and fails on incompatibility before publication. Rollback is no longer an incident with manual steps — it is the same scenario from the run executed on the production network.

What to do if the bundle is already deployed and running on mainnet?

A live system differs in that it cannot be stopped for testing: balances and storage change under your hands. Testing is built on a copy — a state snapshot at a fixed block is deployed in an isolated node, where any sequence of calls is reproduced without risk to mainnet.

This same snapshot serves as a comparison point: after the run, we compare balances, events, and storage with expected values.

The second loop is a shadow run: the new logic version accepts the same calls, but the result is not sent anywhere and is compared with the response of the running contract. A discrepancy in at least one scenario means incompatibility, and the build does not reach the network.

External dependencies — oracles, pools, partner contracts — are replaced with controllable stubs with predefined behavior, including failures and response delays.

Separately, we check backward compatibility of upgrades: old storage records must be readable by the new logic, and emitted events must remain understandable to indexers and wallets.

In upgradeable contracts, we fix the field order and storage slots in advance on OpenZeppelin schemes: a new field is added only at the end. This removes the class of failures in which balances are zeroed or access permissions break after an upgrade.

The release proceeds in stages: snapshot, shadow run on a copy, limited launch on a share of traffic with limits and instant rollback. Between stages, we keep metrics — share of successful calls and discrepancy with emulation.

If there are no discrepancies, the version is rolled out to the whole system; if there are, rollback takes one call, and users do not notice it.

Common technical doubts before starting

Before starting, clients ask the same set of questions, and almost all of them are about boundaries: what exactly we check, on which network, and how this affects the working product. Below are substantive answers to those that most often slow down the decision.

How is this different from testing individual modules?

Unit tests isolate one function. An integration scenario goes through the entire path: contract call, asset transfer, event recording, new state. This is where incorrect permissions, call order, and reentrancy surface — things that isolation does not see.

Is a real network needed?

We run the main volume on a local copy of the network state: the environment is spun up from configuration in minutes, and one scenario takes seconds. The real loop is needed for the final run — it confirms behavior with real addresses and limits.

How to understand that coverage is sufficient?

We count not lines but scenario branches: successful path, permission failure, limit failure, boundary amounts, rollback. If a branch can stop the withdrawal of funds, it is covered by a test.

How does this affect the working product?

The runs do not touch mainnet, and production keys are not used in them. Rare states — empty reserve, limit nearly exhausted, stuck transaction — are reproduced forcibly through a state snapshot, so tests run in parallel with the service's operation.

Let's discuss the task: where to start and what we need from you

The first call on integration testing of smart contracts is technical, not a presentation: we go over the protocol architecture, the boundaries between contracts and the external world, and the scenarios where an error costs the most.

An hour is usually enough to understand the scope — how many contracts are in the bundle, which of them are upgradeable, what is called from outside, and which roles are allowed to change state.

To keep the conversation substantive, materials should be prepared in advance. From the client, we need:

  • repository or archive of contracts together with existing tests, even partial ones;
  • protocol description: roles, access permissions, contract upgrade logic;
  • addresses of deployed contracts and test networks to which there is working access;
  • list of external integrations — oracles, bridges, pools, custody services — and their interfaces;
  • keys and nodes for test networks plus a build environment, if scenarios need to run in a pipeline;
  • incident and known defect log: what has already broken and under what conditions it reproduced;
  • acceptance requirements — by which scenarios we consider coverage sufficient.

The conversation ends not with "thanks, we'll think about it," but with a work plan: stages, composition of the local testbed and mainnet copies, a list of prioritized scenarios, report format, and scope estimate — by timeline and team load.

The project starts immediately after the plan is agreed. Describe the task — we will come back with a plan and estimate.