Create your own
Lesson illustration

Test Coverage Analysis

Hello! Welcome to your tenth and final lesson in the "Modern Development & Testing Toolchain" module.

Introduction

In our previous lessons, you've built a formidable testing arsenal. You've written unit tests for happy paths, reverts, and events; used fuzzing to uncover edge cases; and constructed integration tests to validate complex multi-contract systems.

This raises a critical question: how do you know when you're done testing? How can you be confident that your test suite is truly comprehensive?

This is where test coverage analysis comes in. It's a quantitative measure of how much of your contract's code is actually executed by your tests. It provides a clear, data-driven way to find blind spots in your testing strategy.

Learning Outcome: By the end of this lesson, you will be able to perform test coverage analysis to ensure comprehensive test suites.

Today, we will cover:

  • The definition of test coverage and its key metrics.
  • How to generate and interpret coverage reports in both Hardhat and Foundry.
  • How to use these reports to identify and close gaps in your test suite.
  • The important limitations of test coverage as a measure of contract security.

1. What is Test Coverage?

At its core, test coverage answers the question: "Which lines of my code were run during my tests?" A coverage tool instruments your code, runs your entire test suite, and then generates a report detailing what was—and what was not—executed.

As this article explains, it's a measure of how much of your contract is covered by the tests you've written.

Tests coverage in foundry

Let's start with a simple definition of test coverage from this Medium article by 'Arc'.

Read the short section titled 'Coverage for tests' to get a concise definition.

Coverage is typically measured in a few ways:

  • Line/Statement Coverage: The percentage of executable lines/statements in your code that were run.
  • Function Coverage: The percentage of functions in your contract that were called.
  • Branch Coverage: For every conditional (if, require, ternary operator), this measures whether your tests executed both the true and false outcomes. This is often the most insightful metric.

Imagine this simple require statement:
require(value > 0, "Value must be positive");

A test suite that only ever passes a positive value might achieve 100% line coverage for this line, but it would only have 50% branch coverage because the revert path (when value <= 0) was never tested. Branch coverage forces you to test both the "happy path" and the failure conditions.

2. Test Coverage in Hardhat

Let's start with Hardhat, as its coverage tooling is seamlessly integrated. The previous lesson's auction example used Hardhat, and it's a great candidate for coverage analysis. Hardhat uses the solidity-coverage plugin, which is included by default in new projects.

The following tutorial from the Base documentation provides a perfect walkthrough of generating a report, interpreting it, and using it to improve a test suite.

Hardhat: Analyzing the test coverage of smart contracts

This tutorial demonstrates the entire test coverage workflow in Hardhat using a simple Lock contract.

Please read from the 'Objectives' section through to the end of 'Increasing test coverage'. Pay close attention to the following steps: Running Coverage: Note the command npx hardhat coverage. Analyzing the Report: Observe the initial console output, which shows 83.33% branch coverage. Identifying the Gap: The article points out that the require statement in the constructor is the source of the missing branch coverage. The HTML report (shown in a screenshot) makes this visually obvious by highlighting the untested code. Improving the Test: See how a new test is added specifically to trigger the constructor's require statement. Final Result: After adding the new test, the coverage report shows 100% across the board.

This workflow is fundamental:

  1. Run: npx hardhat coverage
  2. Analyze: Check the console and the HTML report in the coverage/ directory to find untested code (often highlighted in red).
  3. Improve: Write a new test that executes the specific line or branch that was missed.
  4. Verify: Rerun the coverage report to confirm the gap is now covered.

3. Test Coverage in Foundry

Foundry also has powerful, built-in coverage capabilities. The process is slightly different from Hardhat's, as generating the user-friendly HTML report involves an extra step with a standard tool called lcov.

The basic command is simple:

forge coverage

This command runs your tests and prints a coverage summary table directly to your console.

A typical console output from forge coverage, showing statement, branch, function, and line coverage percentages for each contract.

While the console output is useful for a quick check, the detailed HTML report is where you'll do your main analysis.

The article you read earlier provides a great guide on how to generate this report.

Tests coverage in foundry

This article explains how to generate a full HTML coverage report in Foundry using lcov.

Read the sections under 'Pro Coverage report' for 'Mac/Linux'. The key steps are: Install lcov: This is a prerequisite tool. On macOS, you can install it with Homebrew: brew install lcov. Run Coverage with Report Generation: The core Foundry command is forge coverage --report lcov. This creates a file named lcov.info. Generate HTML: The genhtml command (which comes with lcov) converts the lcov.info file into a navigable HTML report. The article provides a handy shell script to automate this. View the Report: Open the index.html file in the generated coverage directory to explore the report.

The resulting HTML report is very similar to Hardhat's. It allows you to drill down into each contract and file, with line-by-line highlighting that shows exactly what your tests executed. Red lines indicate untested code paths that need your attention.

4. The Limits of Coverage: A Necessary Warning

Achieving 100% test coverage feels great, and it's a worthy goal. It demonstrates discipline and significantly reduces the chance of simple bugs. However, it is crucial to understand what test coverage does not guarantee.

100% coverage does not mean your contract is 100% bug-free.

Coverage only confirms that a line of code was executed. It says nothing about whether the logic is correct or if your assertions are meaningful.

Consider this flawed function:

function add(uint a, uint b) public pure returns (uint) {
    return a - b; // Logical bug!
}

You could write a test:

function testAdd() public {
    uint result = myContract.add(5, 3);
    // No assertion!
}

This test would give you 100% coverage for the add function, but it completely fails to detect the critical bug because it doesn't assert the expected outcome.

Coverage is a tool to find untested code, not a proof of correctness. It's a powerful safety net to prevent you from forgetting to test entire functions or conditional branches. Always pair high coverage with strong, meaningful assertions and the diverse inputs from fuzz testing.

Conclusion

Congratulations on completing the "Modern Development & Testing Toolchain" module! You now have a comprehensive understanding of how to build, test, and analyze smart contracts using industry-standard tools like Hardhat and Foundry.

Key Takeaways:

  • Test coverage is a metric that shows what percentage of your codebase is executed by your test suite.
  • Branch coverage is a particularly important metric, as it ensures both outcomes of a conditional statement are tested.
  • In Hardhat, use npx hardhat coverage to generate a console summary and a detailed HTML report.
  • In Foundry, use forge coverage for a console report, and combine it with lcov (forge coverage --report lcov and genhtml) to create an HTML report.
  • The primary goal of coverage analysis is to identify and write tests for uncovered code paths.
  • High coverage is a sign of a good test suite but is not a guarantee of correctness. It must be combined with strong assertions.

Next Steps:

With a solid foundation in testing, you are now ready to tackle more advanced topics in secure and efficient smart contract development. In our next module, "Advanced Solidity & Secure Development," we will begin by exploring custom errors. You'll learn how to replace require strings with custom error types to create contracts that are both cheaper to deploy and easier for developers and tools to interact with.

Can't find a good explanation? Sign up and we'll make it for you

Sign up