background-image: url("img/logo_padded.001.jpeg") background-position: left background-size: 60% class: middle, center, .pull-right[ <br> ## .base_color[R Packages] ## .base_color[Testing] <br> #### .navy[Kelly McConville] #### .navy[ Stat 108 | Week 11 | Spring 2023] ] --- ## Announcements * My **Wed OH** this week will be **noon - 1 pm**. * No lecture next Wed, April 19th. * Don't forget to fill out: + By April 14th: [Project 1 Group Work Form](https://forms.gle/C2iFjTo3Ww1CMsFy5) + Topic preferences poll in Slack! ************************ ## Week's Goals .pull-left[ **Mon Lecture** * Testing ] .pull-right[ **Wed Lecture** * Extended documentation via vignettes * Package dissemination via `pkgdown` ] --- ## Recap Our Package: `camDogs` * Share the [Dogs of Cambridge dataset](https://data.cambridgema.gov/General-Government/Dogs-of-Cambridge/sckh-3xyx), along with documentation. * Includes, `top10()`, a function that outputs a dataset of the dogs with the 10 most common names. * Working copy can be found at [https://github.com/harvard-stat108s23/camDogs](https://github.com/harvard-stat108s23/camDogs) #### Other questions so far? --- ### Testing -- Informally #### Return to our first function ```r coef_of_var <- function(x, trim = 0, ...){ stopifnot(is.numeric(x)) sd(x, ...)/mean(x, trim = trim, ...) } ``` #### Ran so many tests! ```r coef_of_var(x = rnorm(n = 10000, mean = 1, sd = 4)) ``` ``` ## [1] 4.00025 ``` --- ### Testing -- Informally #### Ran so many tests! ```r library(pdxTrees) pdxTrees <- get_pdxTrees_parks() DBH_new <- c(pdxTrees$DBH, Inf) coef_of_var(DBH_new) ``` ``` ## [1] NaN ``` ```r coef_of_var(pdxTrees) ``` ``` ## Error in coef_of_var(pdxTrees): is.numeric(x) is not TRUE ``` --- ### Testing -- Informally #### Ran so many tests! ```r coef_of_var(pdxTrees$Condition) ``` ``` ## Error in coef_of_var(pdxTrees$Condition): is.numeric(x) is not TRUE ``` ```r coef_of_var(c(TRUE, FALSE, FALSE)) ``` ``` ## Error in coef_of_var(c(TRUE, FALSE, FALSE)): is.numeric(x) is not TRUE ``` --- <img src="img/testthat.png" width="10%" style="float:left; padding:25px" style="display: block; margin: auto;" /> <br> ### Testing our Package Functions <br> <br> #### Standard workflow in package development: 1. Write/revise the function in an `.R` script. + (Also write documentation using `roxygen2`) 2. Load the function with `devtools::load_all()` 3. Experiment with the function, giving it different inputs. 4. Repeat. Step 3 likely involves similar **informal** testing as we were doing for our functions outside an `R` package. Good to transition to **automated** testing (also called unit testing) using `testthat`! --- ### Why Automated Testing? * Fewer bugs. + Describe the behavior of your function in both the code and the test. Check against each other. + When adding a new feature to a function, you reduce the chances it breaks the existing features. * Can help motivate more refactoring of your code. + Functions that are easier to test are usually easier to understand. --- ### `testthat` Version 3 * Note: Version 3 behaves differently than earlier versions. * Within your package RStudio Project, run the following to get started: ```r usethis::use_testthat(3) ``` * Modifies your `DESCRIPTION` file: ```r Suggests: testthat (>= 3.0.0) Config/testthat/edition: 3 ``` * Creates the `tests/testthat` folder. * In particular, we now have a `testthat.R` file that will initiate all your tests every time you run `devtools::check()`. --- ### Test File Structure * For every `insert_function_name.R` function in the `R` folder that you want tests for, create an `test-insert_function_name.R` for storing your tests. + You can automate this process with: ```r usethis::use_test("top10") ``` * Each test file holds one or more `test_that()` tests for a particular function in your package. * Each test: + Describes what it is testing. + Has one or more expectations. .pull-left[ ```r library(testthat) test_that("multiplication works", { expect_equal(2 * 2, 4) }) ``` ``` ## Test passed 🥇 ``` ] .pull-right[ ```r test_that("multiplication works", { expect_equal(2 * 2, 5) }) ``` ``` ## ── Failure ('<text>:2'): multiplication works ────────────────────────────────── ## 2 * 2 not equal to 5. ## 1/1 mismatches ## [1] 4 - 5 == -1 ``` ``` ## Error in `reporter$stop_if_needed()`: ## ! Test failed ``` ] --- ### Expectations **Expectation**: Expected result of a computation * Start with `expect_---()` * Two main arguments: + 1st: Actual result + 2nd: What you expect .pull-left[ ```r expect_equal(2 * 2, 4) ``` ] .pull-right[ ```r expect_equal(2 * 2, 5) ``` ``` ## Error: 2 * 2 not equal to 5. ## 1/1 mismatches ## [1] 4 - 5 == -1 ``` ] * They automate the visual checking of results in the console. --- ### Expectations **Expectation**: Expected result of a computation #### Key Questions * Does it have the correct value(s) or dimensions? * Does it have the correct class? * Does it produce an error when it should? --- class: middle, center ## Let's explore some of the `expect_---()` functions in `testthat`! #### As we learn more functions, we will add them to `test-top10.R`. --- ### Testing Equality ```r expect_equal(2 * 2, 4) expect_equal(2 * 2, 4 + 0.0000001) ``` ``` ## Error: 2 * 2 not equal to 4 + 1e-07. ## 1/1 mismatches ## [1] 4 - 4 == -1e-07 ``` ```r expect_equal(2 * 2, 4 + 0.00000001) expect_identical(2 * 2, 4 + 0.0000001) ``` ``` ## Error: 2 * 2 not identical to 4 + 1e-07. ## 1/1 mismatches ## [1] 4 - 4 == -1e-07 ``` ```r expect_equal(2 * 2, 4L) expect_identical(2 * 2, 4L) ``` ``` ## Error: 2 * 2 not identical to 4L. ## Objects equal but not identical ``` --- ### Testing Equality ```r test_that("check all columns were kept", { expect_named(top10(camDogs, Dog_Name), names(camDogs)) }) ``` --- ### Testing Equality ```r test_that("check all columns were kept",{ expect_setequal(names(top10(camDogs, Dog_Name)), names(camDogs)) }) # This will also pass! test_that("check all columns were kept",{ expect_setequal(names(top10(camDogs, Dog_Name)), sample(names(camDogs))) }) ``` --- ### Testing Class Check the class of your output: ```r test_that("check class", { expect_s3_class(top10(camDogs, Dog_Name), "data.frame") }) ``` --- ### Testing Errors * Remember to use defensive coding in your function and then check them with your tests. ```r test_that("errors when it should", { expect_error(top10(camDogs, Latitude_masked)) }) ``` * There are similar functions for warnings and messages: `expect_warning()`, `expect_message()` --- ### Testing Guidelines * Focus on outputs and behaviors of your functions. + The defensive code in your functions focuses on the inputs. * Test each behavior in one test. * If you find a bug, write a test! * Check out the test coverage with `covr::report()`. --- ### Running Tests -- Three Options #### Option 1: Refine a specific test ```r # Load the function devtools::load_all() # Write the body test expect_equal(2 * 2, 4) # Run the test test_that("multiplication works", { expect_equal(2 * 2, 4) }) ``` --- ### Running Tests -- Three Options #### Option 2: Run all tests on a function ```r testthat::test_file("tests/testthat/test-top10.R") ``` #### Option 3: Run all tests on all functions ```r devtools::test() # OR devtools::check() ``` * Will error out if any of the tests don't pass.