R
Completed
-
Section 1: Getting Started
-
Section 2: Core Syntax
-
Section 3: Data Structures
-
Section 4: Data Manipulation
-
Section 5: Visualization and Statistics
-
Section 6: Advanced R
-
Section 7: Practical Projects
-
Section 8: Interview Practice
-
Section 9: More Practice Exercises
-
Section 10: Shiny Apps in Depth
-
Section 11: More Data Wrangling
-
Section 12: Tidyverse Deep Dive
-
Section 13: Statistical Modeling Deep Dive
-
Section 14: Machine Learning in R
-
Section 15: R Visualization Deep Dive
-
Section 16: R Package Development Deep Dive
-
Section 17: R for Reproducible Research
-
Section 18: R and Databases
-
Section 19: R Performance Optimization
-
Section 20: Bioinformatics and Specialized R
-
Section 21: More Shiny Practice
-
Section 22: More Practice Exercises
-
Section 23: R Data Structures Deep Dive
-
Section 24: More Interview and Review
-
Section 25: R for Business Analytics
-
Section 26: R Text Mining and NLP
-
Section 27: R Spatial Data Analysis
-
Section 28: R Deep Learning
-
Section 29: Advanced Statistical Techniques
-
Section 30: R Object Systems Deep Dive
-
Section 31: R Environments and Metaprogramming
-
Section 32: R for Finance
-
Section 33: R for Clinical and Health Data
-
Section 34: More Shiny Advanced Practice
-
Section 35: R Data Cleaning Deep Dive
-
Section 36: R Reporting Automation
-
Section 37: More Practical Projects Round 2
-
Section 38: R Ecosystem and Career
-
Section 39: More Visualization Practice
-
Section 40: R for Experimentation
-
Section 41: R for Genomics and Bioinformatics Deep Dive
-
Section 42: R for Marketing Analytics
-
Section 43: R Data Import/Export Deep Dive
-
Section 44: R String Processing Deep Dive
-
Section 45: R for Actuarial and Insurance Analytics
-
Section 46: R Testing and Quality Assurance Deep Dive
-
Section 47: R Version Control and Collaboration
-
Section 48: R Advanced Functional Programming
-
Section 49: R for Supply Chain and Operations
-
Section 50: More Practice Exercises Round 3
-
Section 51: R Dashboards and BI Integration
-
Section 52: R Data Governance and Ethics
-
Section 53: More Modeling Practice
-
Section 54: R Final Capstone Projects
-
Section 55: R for Sports Analytics
-
Section 56: More Interview Practice Round 2
-
Section 57: R Networking and APIs
-
Section 58: R for Environmental Science
-
Section 59: R Notebook and Documentation Practices
-
Section 60: More Data Wrangling Mastery
-
Section 61: R for A/B Testing at Scale
-
Section 62: R Package Ecosystem Deep Dive
70: Unit Testing with testthat
I've spent way too many hours of my career staring at a console, manually typing a function call, looking at the result, and saying, "Yeah, that looks right." The problem is that "looks right" is a dangerous metric. The moment I change one line of code to fix a bug in one area, I usually break three other things I forgot existed. That's why we're using testthat.
Let's look at a function I'm working on. I want to calculate a weighted average, but I want it to be robust enough to handle NA values by ignoring them—provided the corresponding weight is also handled.
calc_weighted_avg <- function(x, w) {
sum(x * w) / sum(w)
}
The illusion of correctness
At first glance, this is fine. I'll run a quick manual check:
calc_weighted_avg(c(10, 20), c(0.2, 0.8))
# [1] 18
Perfect. 18 is exactly what I expected. But here is where the "manual check" workflow fails. I'm only testing the "happy path." What happens if my data is messy? Let's try passing in some NA values.
calc_weighted_avg(c(10, NA), c(0.5, 0.5))
# [1] NA
That's not what I want. I want the function to ignore the NA and calculate the average based on the remaining valid data. Now, I could just add an na.rm = TRUE argument to the sums, but I don't want to just "guess and check" my way to a solution. I want a safety net.
Building the safety net
Instead of running lines of code in the console, I'm going to move this into a test_that block. This is the heart of the testthat package: we describe what we expect to happen, and the package tells us if we're lying to ourselves.
library(testthat)
test_that("calc_weighted_avg handles basic input", {
expect_equal(calc_weighted_avg(c(10, 20), c(0.2, 0.8)), 18)
})
test_that("calc_weighted_avg ignores NAs", {
expect_equal(calc_weighted_avg(c(10, NA), c(0.5, 0.5)), 10)
})
When I run this, the first test passes, but the second one fails spectacularly. It tells me Expected 10, but ACTUAL NA. Now I have a concrete failure to fix, and more importantly, a way to verify the fix without re-typing my test data every single time.
Adjusting the logic
I'll rewrite the function to handle those NAs. I'll use complete.cases to ensure we only use indices where both the value and the weight exist.
calc_weighted_avg <- function(x, w) {
valid <- complete.cases(x) & complete.cases(w)
sum(x[valid] * w[valid]) / sum(w[valid])
}
Running the tests again... both pass. But as a software engineer, I've learned that if you can think of a way to break it, the user will find it. What if the weights sum to zero? Or what if the input vectors are different lengths?
test_that("calc_weighted_avg handles edge cases", {
# Different lengths should probably trigger an error
expect_error(calc_weighted_avg(c(1, 2), c(1)))
# Zero weights should probably return NaN or an error, not crash
expect_true(is.nan(calc_weighted_avg(c(10, 20), c(0, 0))))
})
The "different lengths" test fails because R's recycling rules actually let c(1, 2) * c(1) happen without an error. That's a bug in my logic. I need to explicitly check that the lengths match.
Closing the loop
I'll add a check at the start of the function. This is the "Red-Green-Refactor" cycle in action: I wrote a test that failed (Red), I changed the code to make it pass (Green), and now I can clean up the implementation (Refactor).
calc_weighted_avg <- function(x, w) {
if (length(x) != length(w)) {
stop("x and w must be the same length")
}
valid <- complete.cases(x) & complete.cases(w)
sum(x[valid] * w[valid]) / sum(w[valid])
}
Now, when I run my test suite, everything is green. I can now change the internals of this function—maybe optimize it for huge datasets using matrixStats—and I can do so with total confidence. If I break the core logic, testthat will scream at me immediately.
📋 Practical Task
Fixing the `calculate_z_score` Utility
You have been handed a function designed to calculate the Z-score of a vector. However, it is currently fragile. Your task is to write a testthat suite to identify the bugs and then fix the function.
calculate_z_score <- function(x) {
(x - mean(x)) / sd(x)
}
Requirements:
- Create a test block that ensures a simple vector (e.g.,
c(1, 2, 3)) returns the expected Z-scores. - Create a test that checks how the function handles a vector of length 1. (Hint: What is the standard deviation of a single number?).
- Create a test that checks how the function handles a vector where all numbers are identical (e.g.,
c(5, 5, 5)). - Modify the
calculate_z_scorefunction so that it returns a vector ofNAs instead ofNaNs when the standard deviation is zero, and returns a helpful error message if the input is not numeric.
There are no comments for now.