Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

.setup= argument to allow re-usable (but hermetic) local variables? #13

Open
MichaelChirico opened this issue Oct 13, 2022 · 1 comment

Comments

@MichaelChirico
Copy link
Contributor

In plain {testthat} we can re-use local variables "hermetically" by running some code before expectations:

test_that("a test", {
  var1 <- 1
  var2 <- 2

  expect_equal(var1, var2 - 1)
  # ... more tests using var1,var2 ...
})

After "a test" runs, we don't have to worry about other tests "seeing" var1/var2, their scope is restricted.

It's not so easy to do this with {patrick}, because we want to declare just once variables used across all the .cases. We might also want to declare the .cases themselves.

The workaround that I've landed on is to wrap with_parameters_test_that() with local({}), but this is a tad clumsy and usage of local({}) is not so prevalent in R code, so it slightly harms readability IMO.

One solution would be to offer a .setup argument that encapsulates what local() is doing:

with_parameters_test_that(
  "a parametrized test",
  .setup = {
    var1 <- 1
    var2 <- 2

   cases <- expand.grid(p1 = 1:10, p2 = 10:1)
  },
  expect_equal(p1 + p2, 10 * var1 + var2 - 1),
  .cases = cases
)

.setup is run first, so var1 and var2 are available to code but also to the other arguments in the with_parameters_test_that call.

WDYT?

@michaelquinn32
Copy link
Member

My examples tend to focus on local variables that aren't expensive to compute. Your example is a bit like that too, and it shouldn't be a problem to run that code every time. Each patrick iteration should be in a clean environment.

I'd rewrite your lintr example like this, which is more in line with the docs:

generate_cases <- function() {
  # Note: cases indicated by "*" are whole numbers, but don't lint because the user has
  #   effectively declared "this is a double" much as adding '.0' is otherwise accepted.
  cases <- tibble::tribble(
            ~num_value_str, ~should_lint,
                     "Inf",        FALSE,
                     "NaN",        FALSE,
                    "TRUE",        FALSE,
                    # ... And many more cases ...
                    "8.0i",        FALSE
  )
  # for convenience of coercing these to string (since tribble doesn't support auto-conversion)
  int_max <- .Machine[["integer.max"]]  # largest number that R can represent as an integer
  cases_int_max <- tibble::tribble(
    ~num_value_str, ~should_lint,
    -int_max - 1.0,        FALSE,
          -int_max,         TRUE,
           int_max,         TRUE,
     int_max + 1.0,        FALSE           int_max,         TRUE,
     int_max + 1.0,        FALSE
  )
  )
  cases_int_max$num_value_str <- as.character(cases_int_max$num_value_str)
  cases <- rbind(cases, cases_int_max)
  cases$.test_name <- sprintf("num_value_str=%s, should_lint=%s", cases$num_value_str, cases$should_lint)
  cases
}

patrick::with_parameters_test_that(
  "single numerical constants are properly identified ",
  {
    linter <-  implicit_integer_linter()
    expect_lint(num_value_str, if (should_lint) "Integers should not be implicit", linter)
  },
  .cases = generate_cases()
)

But I can see some value here for cases where some of the values needed in the parameterized test are expensive to compute.
The closes parallel I see here is the difference between setup and setupClass in Python.
https://stackoverflow.com/questions/23667610/what-is-the-difference-between-setup-and-setupclass-in-python-unittest

When using parameterized tests, setUp is called every time. This is for a clean environment, like you said. You
use setUpClass when you want a (usually readonly) object that might take a lot of time. Adding data to a database
for the test is one good use.

I'll see what I can do here.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants