patrick/0000755000176200001440000000000014322053322011703 5ustar liggesuserspatrick/NAMESPACE0000644000176200001440000000016614321606221013126 0ustar liggesusers# Generated by roxygen2: do not edit by hand export(cases) export(with_parameters_test_that) importFrom(dplyr,.data) patrick/README.md0000644000176200001440000000722214321646663013204 0ustar liggesusers [![R-CMD-check](https://github.com/google/patrick/actions/workflows/R-CMD-check.yaml/badge.svg)](https://github.com/google/patrick/actions/workflows/R-CMD-check.yaml) [![CRAN](https://www.r-pkg.org/badges/version/patrick)](https://cran.r-project.org/package=patrick) # Introducing patrick This package is an extension to `testthat` that enables parameterized unit testing in R. ## Installing The release version of `patrick` is available on CRAN. Install it in the usual manner: ``` install.packages("patrick") ``` The development version of `patrick` is currently only available on GitHub. Install it using `devtools`. ``` devtools::install_github("google/patrick") ``` To use `patrick` as a testing tool within your package, add it to your list of `Suggests` and `Remotes` within your package's `DESCRIPTION`. ``` Suggests: patrick ``` ## Use Many packages within R employ the following pattern when writing tests: ``` test_that("Data is a successfully converted: numeric", { input <- convert(numeric_data) expect_type(input, "double") }) test_that("Data is a successfully converted: character", { input <- convert(character_data) expect_type(input, "character") }) ``` While explicit, recycling a test pattern like this is prone to user error and other issues, as it is a violation of the classic DNRY rule (do not repeat yourself). `patrick` eliminates this problem by creating test parameters. ``` with_parameters_test_that("Data is successfully converted:", { input <- convert(test_data) expect_type(input, type) }, test_data = list(numeric_data, character_data), type = c("double", "character"), .test_name = type ) ``` Parameterized tests behave exactly the same as standard `testthat` tests. Per usual, you call all of your tests with `devtools::test`, and they'll also run during package checks. Each executes independently and then your test report will produce a single report. A complete name for each test will be formed using the initial test description and the strings in the `.test_name` parameter. Small sets of cases can be reasonably passed as parameters to `with_parameters_test_that`. This becomes less readable when the number of cases increases. To help mitigate this issue, `patrick` provides a case generator helper function. ``` with_parameters_test_that("Data is successfully converted:", { input <- convert(test_data) expect_type(input, type) }, cases( double = list(test_data = numeric_data, type = "double"), character = list(test_data = character_data, type = "character") ) ) ``` More complicated testing cases can be constructed using data frames. This is usually best handled within a helper function and in a `helper-.R` file. ``` make_cases <- function() { tibble::tribble( ~ .test_name, ~ expr, ~ numeric_value, "sin", sin(pi / 4), 1 / sqrt(2), "cos", cos(pi / 4), 1 / sqrt(2), "tan", tan(pi / 4), 1 ) } with_parameters_test_that( "trigonometric functions match identities", { testthat::expect_equal(expr, numeric_value) }, .cases = make_cases() ) ``` If you don't provide test names when generating cases, `patrick` will generate them automatically from the test data. ## Inspiration This package is inspired by parameterized testing packages in other languages, notably the [`parameterized`](https://github.com/wolever/parameterized) library in Python. ## Contributing Please read the [`CONTRIBUTING.md`](https://github.com/google/patrick/blob/master/CONTRIBUTING.md) for details on how to contribute to this project. ## Disclaimer This is not an officially supported Google product. patrick/man/0000755000176200001440000000000014321630513012460 5ustar liggesuserspatrick/man/patrick-package.Rd0000644000176200001440000000503614321630513016001 0ustar liggesusers% Generated by roxygen2: do not edit by hand % Please edit documentation in R/patrick-package.R \docType{package} \name{patrick-package} \alias{patrick} \alias{patrick-package} \title{Parameterized Unit Testing} \description{ `patrick` (parameterized testing in R is kind of cool!) is a `testthat` extension that lets you create reusable blocks of a test codes. Parameterized tests are often easier to read and more reliable, since they follow the DNRY (do not repeat yourself) rule. To do this, define tests with the function [with_parameters_test_that()]. Multiple approaches are provided for passing sets of cases. } \details{ This package is inspired by parameterized testing packages in other languages, notably the [`parameterized`](https://github.com/wolever/parameterized) library in Python. } \examples{ with_parameters_test_that("trigonometric functions match identities:", { testthat::expect_equal(expr, numeric_value) }, expr = c(sin(pi / 4), cos(pi / 4), tan(pi / 4)), numeric_value = c(1 / sqrt(2), 1 / sqrt(2), 1), .test_name = c("sin", "cos", "tan") ) # Run the same test with the cases() constructor with_parameters_test_that( "trigonometric functions match identities", { testthat::expect_equal(expr, numeric_value) }, cases( sin = list(expr = sin(pi / 4), numeric_value = 1 / sqrt(2)), cos = list(expr = cos(pi / 4), numeric_value = 1 / sqrt(2)), tan = list(expr = tan(pi / 4), numeric_value = 1) ) ) # If names aren't provided, they are automatically generated. with_parameters_test_that( "trigonometric functions match identities", { testthat::expect_equal(expr, numeric_value) }, cases( list(expr = sin(pi / 4), numeric_value = 1 / sqrt(2)), list(expr = cos(pi / 4), numeric_value = 1 / sqrt(2)), list(expr = tan(pi / 4), numeric_value = 1) ) ) # The first test case is named "expr=0.7071068, numeric_value="0.7071068" # and so on. # Or, pass a data frame of cases, perhaps using a helper function make_cases <- function() { tibble::tribble( ~.test_name, ~expr, ~numeric_value, "sin", sin(pi / 4), 1 / sqrt(2), "cos", cos(pi / 4), 1 / sqrt(2), "tan", tan(pi / 4), 1 ) } with_parameters_test_that( "trigonometric functions match identities", { testthat::expect_equal(expr, numeric_value) }, .cases = make_cases() ) } \seealso{ Useful links: \itemize{ \item \url{https://github.com/google/patrick} \item Report bugs at \url{https://github.com/google/patrick/issues} } } \author{ \strong{Maintainer}: Michael Quinn \email{msquinn@google.com} } \keyword{internal} patrick/man/with_parameters_test_that.Rd0000644000176200001440000000675514321630513020241 0ustar liggesusers% Generated by roxygen2: do not edit by hand % Please edit documentation in R/with_parameters.R \name{with_parameters_test_that} \alias{with_parameters_test_that} \alias{cases} \title{Execute a test with parameters.} \usage{ with_parameters_test_that( desc_stub, code, ..., .cases = NULL, .test_name = NULL ) cases(...) } \arguments{ \item{desc_stub}{A string scalar. Used in creating the names of the parameterized tests.} \item{code}{Test code containing expectations.} \item{...}{Named arguments of test parameters. All vectors should have the same length.} \item{.cases}{A data frame where each row contains test parameters.} \item{.test_name}{An alternative way for providing test names. If provided, the name will be appended to the stub description in `desc_stub`. If not provided, test names will be automatically generated.} } \description{ This function is an extension of [testthat::test_that()] that lets you pass a series of testing parameters. These values are substituted into your regular testing code block, making it reusable and reducing duplication. } \details{ You have a couple of options for passing parameters to you test. You can use named vectors/ lists. The function will assert that you have correct lengths before proceeding to test execution. Alternatively you can used a `data.frame` or list in combination with the splice unquote operator \code{\link[rlang]{!!!}}. Last, you can use the constructor `cases()`, which is similar to building a `data.frame` rowwise. If you manually build the data frame, pass it in the `.cases` argument. ## Naming test cases If the user passes a character vector as `.test_name`, each instance is combined with `desc_stub` to create the completed test name. Similarly, the named argument from `cases()` is combined with `desc_stub` to create the parameterized test names. When names aren't provided, they will be automatically generated using the test data. Names follow the pattern of "name=value, name=value" for all elements in a test case. } \examples{ with_parameters_test_that("trigonometric functions match identities:", { testthat::expect_equal(expr, numeric_value) }, expr = c(sin(pi / 4), cos(pi / 4), tan(pi / 4)), numeric_value = c(1 / sqrt(2), 1 / sqrt(2), 1), .test_name = c("sin", "cos", "tan") ) # Run the same test with the cases() constructor with_parameters_test_that( "trigonometric functions match identities", { testthat::expect_equal(expr, numeric_value) }, cases( sin = list(expr = sin(pi / 4), numeric_value = 1 / sqrt(2)), cos = list(expr = cos(pi / 4), numeric_value = 1 / sqrt(2)), tan = list(expr = tan(pi / 4), numeric_value = 1) ) ) # If names aren't provided, they are automatically generated. with_parameters_test_that( "trigonometric functions match identities", { testthat::expect_equal(expr, numeric_value) }, cases( list(expr = sin(pi / 4), numeric_value = 1 / sqrt(2)), list(expr = cos(pi / 4), numeric_value = 1 / sqrt(2)), list(expr = tan(pi / 4), numeric_value = 1) ) ) # The first test case is named "expr=0.7071068, numeric_value="0.7071068" # and so on. # Or, pass a data frame of cases, perhaps using a helper function make_cases <- function() { tibble::tribble( ~.test_name, ~expr, ~numeric_value, "sin", sin(pi / 4), 1 / sqrt(2), "cos", cos(pi / 4), 1 / sqrt(2), "tan", tan(pi / 4), 1 ) } with_parameters_test_that( "trigonometric functions match identities", { testthat::expect_equal(expr, numeric_value) }, .cases = make_cases() ) } patrick/DESCRIPTION0000644000176200001440000000162114322053322013411 0ustar liggesusersPackage: patrick Title: Parameterized Unit Testing Version: 0.2.0 Authors@R: person(given = "Michael", family = "Quinn", role = c("aut", "cre"), email = "msquinn@google.com") Description: This is an extension of the 'testthat' package that lets you add parameters to your unit tests. Parameterized unit tests are often easier to read and more reliable, since they follow the DNRY (do not repeat yourself) rule. License: Apache License 2.0 URL: https://github.com/google/patrick BugReports: https://github.com/google/patrick/issues Depends: R (>= 3.1) Imports: dplyr, purrr, rlang, testthat, tibble Config/testthat/edition: 3 Encoding: UTF-8 RoxygenNote: 7.2.1 NeedsCompilation: no Packaged: 2022-10-13 17:57:59 UTC; msquinn Author: Michael Quinn [aut, cre] Maintainer: Michael Quinn Repository: CRAN Date/Publication: 2022-10-13 18:20:02 UTC patrick/tests/0000755000176200001440000000000014321606222013047 5ustar liggesuserspatrick/tests/testthat/0000755000176200001440000000000014322053322014705 5ustar liggesuserspatrick/tests/testthat/test-with_parameters.R0000644000176200001440000001037414321630513021212 0ustar liggesusers# Copyright 2018 Google LLC # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. with_parameters_test_that( "Running tests:", { if (test_outcome == "success") { testthat::expect_success(testthat::expect_true(case)) } else { failure_message <- "`case` (isn't true|is not TRUE)" testthat::expect_failure(testthat::expect_true(case), failure_message) } }, test_outcome = c("success", "fail", "null"), case = list(TRUE, FALSE, NULL), .test_name = c("success", "fail", "null") ) with_parameters_test_that( "Names are added", { testthat::expect_identical(.test_name, "case=TRUE") }, case = TRUE ) with_parameters_test_that( "Names can be extracted from cases", { testthat::expect_identical( .test_name, "logical=FALSE, number=1, string=hello" ) }, .cases = data.frame( logical = FALSE, number = 1, string = "hello", stringsAsFactors = FALSE ) ) with_parameters_test_that( "Cases are correctly evaluated:", { testthat::expect_length(vec, len) }, cases( one = list(vec = 1, len = 1), ten = list(vec = 1:10, len = 10) ) ) with_parameters_test_that( "Cases are correctly evaluated with names added:", { testthat::expect_identical(.test_name, "vec=1, len=1") }, cases(list(vec = 1, len = 1)) ) with_parameters_test_that( "Data frames can be passed to cases:", { result <- rlang::as_function(FUN)(input) testthat::expect_equal(result, out) }, .cases = tibble::tribble( ~.test_name, ~FUN, ~input, ~out, "times", ~ .x * 2, 2, 4, "plus", ~ .x + 3, 3, 6 ) ) with_parameters_test_that("Patrick doesn't throw inappropriate warnings:", { testthat::expect_warning(fun(), regexp = message) }, cases( shouldnt_warn = list(fun = function() 1 + 1, message = NA), should_warn = list( fun = function() warning("still warn!"), message = "still warn" ) ) ) test_that("Patrick catches the right class of warning", { # TODO: Find a replacement for testthat's deprecated mock functions. testthat::local_mock( test_that = function(...) { rlang::warn("New warning", class = "testthat_braces_warning") }, .env = "testthat" ) testthat::expect_warning( with_parameters_test_that( "No more warnings:", { testthat::expect_true(truth) }, truth = TRUE ), regexp = NA ) }) # From testthat/tests/testthat/test-test-that.R # Use for checking that line numbers are still correct expectation_lines <- function(code) { srcref <- attr(substitute(code), "srcref") if (!is.list(srcref)) { stop("code doesn't have srcref", call. = FALSE) } results <- testthat::with_reporter("silent", code)$expectations() unlist(lapply(results, function(x) x$srcref[1])) - srcref[[1]][1] } test_that("patrick reports the correct line numbers", { lines <- expectation_lines({ # line 1 with_parameters_test_that("simple", { # line 2 expect_true(truth) # line 3 }, # line 4 cases( true = list(truth = TRUE), false = list(truth = FALSE) )) }) expect_equal(lines, c(3, 3)) }) test_that('patrick gives a deprecation warning for "test_name"', { testthat::expect_warning( with_parameters_test_that( "Warn about `test_name` argument:", { testthat::expect_true(truth) }, truth = TRUE, test_name = "true" ), regexp = "deprecated" ) testthat::expect_warning( with_parameters_test_that( "Warn about `test_name` column:", { testthat::expect_true(truth) }, .cases = tibble::tribble( ~test_name, ~truth, "true", TRUE ) ), regexp = "deprecated" ) }) patrick/tests/testthat.R0000644000176200001440000000117114321606222015032 0ustar liggesusers# Copyright 2018 Google LLC # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. library(patrick) library(testthat) test_check("patrick") patrick/R/0000755000176200001440000000000014321632430012106 5ustar liggesuserspatrick/R/patrick-package.R0000644000176200001440000000240314321606221015255 0ustar liggesusers# Copyright 2018 Google LLC # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. #' Parameterized Unit Testing #' #' `patrick` (parameterized testing in R is kind of cool!) is a `testthat` #' extension that lets you create reusable blocks of a test codes. Parameterized #' tests are often easier to read and more reliable, since they follow the DNRY #' (do not repeat yourself) rule. To do this, define tests with the function #' [with_parameters_test_that()]. Multiple approaches are provided for passing #' sets of cases. #' #' This package is inspired by parameterized testing packages in other #' languages, notably the #' [`parameterized`](https://github.com/wolever/parameterized) library in #' Python. #' @keywords internal #' @inherit with_parameters_test_that examples "_PACKAGE" patrick/R/with_parameters.R0000644000176200001440000001530714321632430015435 0ustar liggesusers# Copyright 2018 Google LLC # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. #' Execute a test with parameters. #' #' This function is an extension of [testthat::test_that()] that lets you pass #' a series of testing parameters. These values are substituted into your #' regular testing code block, making it reusable and reducing duplication. #' #' You have a couple of options for passing parameters to you test. You can #' use named vectors/ lists. The function will assert that you have correct #' lengths before proceeding to test execution. Alternatively you can used #' a `data.frame` or list in combination with the splice unquote operator #' \code{\link[rlang]{!!!}}. Last, you can use the constructor `cases()`, which #' is similar to building a `data.frame` rowwise. If you manually build the #' data frame, pass it in the `.cases` argument. #' #' ## Naming test cases #' #' If the user passes a character vector as `.test_name`, each instance is #' combined with `desc_stub` to create the completed test name. Similarly, the #' named argument from `cases()` is combined with `desc_stub` to create the #' parameterized test names. When names aren't provided, they will be #' automatically generated using the test data. #' #' Names follow the pattern of "name=value, name=value" for all elements in a #' test case. #' #' @param desc_stub A string scalar. Used in creating the names of the #' parameterized tests. #' @param code Test code containing expectations. #' @param ... Named arguments of test parameters. All vectors should have the #' same length. #' @param .cases A data frame where each row contains test parameters. #' @param .test_name An alternative way for providing test names. If provided, #' the name will be appended to the stub description in `desc_stub`. If not #' provided, test names will be automatically generated. #' @examples #' with_parameters_test_that("trigonometric functions match identities:", #' { #' testthat::expect_equal(expr, numeric_value) #' }, #' expr = c(sin(pi / 4), cos(pi / 4), tan(pi / 4)), #' numeric_value = c(1 / sqrt(2), 1 / sqrt(2), 1), #' .test_name = c("sin", "cos", "tan") #' ) #' #' # Run the same test with the cases() constructor #' with_parameters_test_that( #' "trigonometric functions match identities", #' { #' testthat::expect_equal(expr, numeric_value) #' }, #' cases( #' sin = list(expr = sin(pi / 4), numeric_value = 1 / sqrt(2)), #' cos = list(expr = cos(pi / 4), numeric_value = 1 / sqrt(2)), #' tan = list(expr = tan(pi / 4), numeric_value = 1) #' ) #' ) #' #' # If names aren't provided, they are automatically generated. #' with_parameters_test_that( #' "trigonometric functions match identities", #' { #' testthat::expect_equal(expr, numeric_value) #' }, #' cases( #' list(expr = sin(pi / 4), numeric_value = 1 / sqrt(2)), #' list(expr = cos(pi / 4), numeric_value = 1 / sqrt(2)), #' list(expr = tan(pi / 4), numeric_value = 1) #' ) #' ) #' # The first test case is named "expr=0.7071068, numeric_value="0.7071068" #' # and so on. #' #' # Or, pass a data frame of cases, perhaps using a helper function #' make_cases <- function() { #' tibble::tribble( #' ~.test_name, ~expr, ~numeric_value, #' "sin", sin(pi / 4), 1 / sqrt(2), #' "cos", cos(pi / 4), 1 / sqrt(2), #' "tan", tan(pi / 4), 1 #' ) #' } #' #' with_parameters_test_that( #' "trigonometric functions match identities", #' { #' testthat::expect_equal(expr, numeric_value) #' }, #' .cases = make_cases() #' ) #' @importFrom dplyr .data #' @export with_parameters_test_that <- function(desc_stub, code, ..., .cases = NULL, .test_name = NULL) { if (is.null(.cases)) { pars <- tibble::tibble(...) possibly_add_column <- purrr::possibly(tibble::add_column, otherwise = pars) all_pars <- possibly_add_column(pars, .test_name = .test_name) } else { all_pars <- .cases } # TODO: drop this once downstream users upgrade their version of patrick. if ("test_name" %in% names(all_pars)) { msg <- paste( 'The argument and cases column "test_name" is deprecated. Please use the', "new `.test_name` argument instead. See `?with_parameters_test_that`", "for more information" ) rlang::warn(msg, class = "patrick_test_name_deprecation") # It would be nicer to do this with rename(), but that function doesn't # support overwriting existing columns. all_pars <- dplyr::mutate( all_pars, .test_name = .data$test_name, test_name = NULL ) } if (!".test_name" %in% names(all_pars)) { all_pars$.test_name <- build_test_names(all_pars) } captured <- rlang::enquo(code) purrr::pmap(all_pars, build_and_run_test, desc = desc_stub, code = captured) invisible(TRUE) } #' Generate test names from cases, if none are provided. #' #' @param all_cases A tibble containing test cases. #' @return A character vector, whose length matches the number of rows in #' `all_cases`. #' @noRd build_test_names <- function(all_cases) { case_names <- names(all_cases) purrr::pmap_chr(all_cases, build_label, case_names = case_names) } build_label <- function(..., case_names) { row <- format(list(...)) toString(sprintf("%s=%s", case_names, row)) } build_and_run_test <- function(..., .test_name, desc, code, env) { completed_desc <- paste(desc, .test_name) args <- list(..., .test_name = .test_name) withCallingHandlers( testthat::test_that(completed_desc, rlang::eval_tidy(code, args)), testthat_braces_warning = function(cnd) { rlang::cnd_muffle(cnd) }, # Ensuring backwards compatibility # TODO: remove after new version of testthat releases warning = function(cnd) { if (cnd$message == paste( "The `code` argument to `test_that()` must be a braced expression", "to get accurate file-line information for failures." )) { rlang::cnd_muffle(cnd) } } ) } #' @rdname with_parameters_test_that #' @export cases <- function(...) { all_cases <- list(...) nested <- purrr::modify_depth(all_cases, 2L, list) dplyr::bind_rows( nested, .id = if (!is.null(names(nested))) ".test_name" ) } patrick/NEWS.md0000644000176200001440000000227314321630513013007 0ustar liggesusers# patrick 0.2.0 ## New features * Patrick will try to generate names automatically if not provided. This also works when cases are provided as a data frame. # patrick 0.1.0 Breaking changes: * Setting test names should now happen with `.test_name`, instead of the implicit `test_name` variable from before. This is now an explicit argument for the function `with_parameters_test_that()`, and the leading dot should help distinguish this from values passed as cases. # patrick 0.0.4 Update `patrick` for testthat 3e. * Catch warnings for code not being braced. We still produce the right code. * Make sure patrick uses the right line numbers. # patrick 0.0.3 * Add more examples and tests for how patrick works with data frames. * Update `with_parameters_test_that()` to use [data, dots, details](https://design.tidyverse.org/dots-position.html) * Modernize package files: DESCRIPTION and `R/patrick-package.R`. # patrick 0.0.2 * This is a minor update. Tests are compatible with the next version of `testthat`. # patrick 0.0.1 Welcome to `patrick`, a package for parameterizing tests within testthat. Check out the README.md file to learn more about this package. patrick/MD50000644000176200001440000000102314322053322012207 0ustar liggesusersea182bab84b795119b7ffa4cfc13cce8 *DESCRIPTION b31019ce5279d11dd34414e51867e25d *NAMESPACE 2b5807e634801ce5152c7c486bb6d041 *NEWS.md 374c72807ac9daa0b8df99f8f8410606 *R/patrick-package.R a779fce8d0c8fb552d3f903f5c68502a *R/with_parameters.R dce4a2cd8b8cebaba9768088771e62df *README.md 034c56f7d66401357d1403b43566bc52 *man/patrick-package.Rd 5a53b15d7be06199cfe31f431042cb16 *man/with_parameters_test_that.Rd 7d396051ec7db71be954bcac78dcd191 *tests/testthat.R 34059b874f1ba0d7a85974567c4e0a19 *tests/testthat/test-with_parameters.R