Skip to Content
Course content

184: Code Coverage with covr

Click on the "Edit" button in the top corner of the screen to edit your slide content.

I’ve spent a lot of my career thinking that if my tests pass, my code is "done." It's a dangerous assumption. You can have a suite of 50 tests that all pass with flying colors, but if those tests only exercise the "happy path," you're essentially flying blind regarding the edge cases where the real bugs usually hide.

Let's look at a small package I've been tinkering with. It has a function designed to calculate a performance bonus for a sales team. I wrote a few tests, and they're all green. But I have a nagging feeling that I've missed a logic branch.

The "Perfect" Test Suite

Here is the function I'm working with:

calculate_bonus <- function(sales, tenure_years) {
  if (tenure_years < 1) {
    return(0) # New hires don't get bonuses
  }
  
  if (sales > 10000) {
    return(sales * 0.10) # High performers get 10%
  } else {
    return(sales * 0.02) # Everyone else gets 2%
  }
}

And here is the test I wrote for it in my tests/testthat/test-bonus.R file:

test_that("high performers get the correct bonus", {
  expect_equal(calculate_bonus(15000, 2), 1500)
})

I run devtools::test(), it says 1 test passed, and I'm tempted to call it a day. But this is where covr comes in. Instead of guessing, I want to see exactly which lines of code were actually executed during that test run.

Seeing the Gaps

I'll pull in the covr package and run the coverage report on my package. Since I'm developing this as a package, I can use the package_coverage() function.

# I'll run this from the console
covr::package_coverage()

When I run this, R generates a report (often opening in the viewer or a browser). Looking at the output, I see a percentage—likely around 40-50%—and when I click into the source code view, I see some lines highlighted in green and others in red. The red lines are the ones the tests never touched.

Wait, look at the tenure_years < 1 block. It's red. And the else block for the 2% bonus? Also red. My "passing" test only exercised the high-performer branch for experienced employees. I completely ignored new hires and average performers.

Filling the Holes

This is the "aha" moment. covr isn't telling me my code is wrong; it's telling me my tests are incomplete. I can't claim the function works for new hires if I've never actually run the code that handles new hires.

I'll go back to my test file and add cases to hit those red lines:

test_that("bonus logic handles all scenarios", {
  # Testing the new hire branch (the first red block)
  expect_equal(calculate_bonus(20000, 0.5), 0)
  
  # Testing the low-performance branch (the second red block)
  expect_equal(calculate_bonus(5000, 2), 100)
})

Now, I run covr::package_coverage() again. I refresh the report, and suddenly, the whole function is green. 100% coverage. Now I can actually sleep at night knowing that every logical branch in that function has been executed at least once.

The Trap of 100% Coverage

Now, a quick word of caution from someone who has crashed a few production servers: 100% coverage does not mean 100% correctness. Coverage tells you that a line was executed, not that it was executed with every possible weird input. For example, I haven't tested what happens if sales is a negative number or a string. covr tells me I've visited every room in the house, but it doesn't tell me if the furniture in those rooms is actually bolted down correctly.

Use covr to find the "dark corners" of your code that aren't being tested at all, but keep using your engineering intuition to write meaningful assertions.




📋 Practical Task

Closing the Coverage Gap in a Discount Calculator

You have a function called apply_discount that applies a discount based on customer loyalty and purchase amount. However, your current test suite is only hitting part of the logic.

apply_discount <- function(amount, is_loyal) {
  if (amount < 0) {
    stop("Amount cannot be negative")
  }
  
  if (is_loyal) {
    if (amount > 100) {
      return(amount * 0.80) # 20% off
    } else {
      return(amount * 0.90) # 10% off
    }
  } else {
    return(amount) # No discount for non-loyal customers
  }
}

Your current test is:

test_that("loyal customers with high spend get 20% off", {
  expect_equal(apply_discount(200, TRUE), 160)
})

Your Task: Use the covr philosophy to identify the uncovered branches. Write a set of additional test_that blocks that ensure 100% code coverage for the apply_discount function, including the error handling for negative amounts.

Rating
0 0

There are no comments for now.

to be the first to leave a comment.