Skip to content

Use waldo::compare(list_as_map) in expect_mapequal() #2150

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

Merged
merged 3 commits into from
Aug 6, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions NEWS.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
# testthat (development version)

* Power `expect_mapequal()` with `waldo::compare(list_as_map = TRUE)` (#1521).
* On CRAN, `test_that()` now automatically skips if a package is not installed (#1585). Practically, this means that you no longer need to check that suggested packages are installed. (We don't do this in the tidyverse because we think it has limited payoff, but other styles advise differently.)
* `expect_snapshot()` no longer skips on CRAN, as that skips the rest of the test. Instead it just returns, neither succeeding nor failing (#1585).
* Interrupting a test now prints the test name. This makes it easier to tell where a very slow test might be hanging (#1464)
Expand Down
37 changes: 4 additions & 33 deletions R/expect-setequal.R
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,9 @@
#' (i.e. `y` is a subset of `x`).
#' * `expect_in(x, y)` tests every element of `x` is in `y`
#' (i.e. `x` is a subset of `y`).
#' * `expect_mapequal(x, y)` tests that `x` and `y` have the same names, and
#' that `x[names(y)]` equals `y`.
#' * `expect_mapequal(x, y)` treats lists as if they are mappings between names
#' and values. Concretely, this drops `NULL`s in both objects and sorts
#' named components.
#'
#' Note that `expect_setequal()` ignores names, and you will be warned if both
#' `object` and `expected` have them.
Expand Down Expand Up @@ -77,37 +78,7 @@ expect_mapequal <- function(object, expected) {
act <- quasi_label(enquo(object))
exp <- quasi_label(enquo(expected))

check_vector(object)
check_map_names(object)
check_vector(expected)
check_map_names(expected)

# Length-0 vectors are OK whether named or unnamed.
if (length(act$val) == 0 && length(exp$val) == 0) {
testthat_warn("`object` and `expected` are empty lists")
return(pass(act$val))
}

act_nms <- names(act$val)
exp_nms <- names(exp$val)
if (setequal(act_nms, exp_nms)) {
act <- labelled_value(act$val[exp_nms], act$lab)
return(expect_waldo_equal_("equal", act, exp))
}

act_miss <- setdiff(exp_nms, act_nms)
if (length(act_miss) > 0) {
vals <- paste0(encodeString(act_miss, quote = '"'), ", ")
return(fail(paste0("Names absent from `object`: ", vals)))
}

exp_miss <- setdiff(act_nms, exp_nms)
if (length(exp_miss) > 0) {
vals <- paste0(encodeString(exp_miss, quote = '"'), ", ")
return(fail(paste0("Names absent from `expected`: ", vals)))
}

pass(act$val)
expect_waldo_equal_("equal", act, exp, list_as_map = TRUE)
}

#' @export
Expand Down
5 changes: 3 additions & 2 deletions man/expect_setequal.Rd

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

37 changes: 0 additions & 37 deletions tests/testthat/_snaps/expect-setequal.md
Original file line number Diff line number Diff line change
Expand Up @@ -50,43 +50,6 @@
* Only in `expected`: 3, 4, 5, 6, 7, 8, 9, 10, 11, ...


# check inputs

Code
expect_mapequal(sum, named)
Condition
Error in `expect_mapequal()`:
! `object` must be a vector, not a primitive function.
Code
expect_mapequal(named, sum)
Condition
Error in `expect_mapequal()`:
! `expected` must be a vector, not a primitive function.
Code
expect_mapequal(unnamed, named)
Condition
Error in `expect_mapequal()`:
! All elements in `object` must have names.
x Empty names at position: 1
Code
expect_mapequal(named, unnamed)
Condition
Error in `expect_mapequal()`:
! All elements in `expected` must have names.
x Empty names at position: 1
Code
expect_mapequal(named, duplicated)
Condition
Error in `expect_mapequal()`:
! All elements in `expected` must have unique names.
x Duplicate names: "x"
Code
expect_mapequal(duplicated, named)
Condition
Error in `expect_mapequal()`:
! All elements in `object` must have unique names.
x Duplicate names: "x"

# expect_contains() gives useful message on failure

`x1` (`actual`) doesn't fully contain all the values in `x2` (`expected`).
Expand Down
40 changes: 12 additions & 28 deletions tests/testthat/test-expect-setequal.R
Original file line number Diff line number Diff line change
Expand Up @@ -53,10 +53,16 @@ test_that("ignores order", {
expect_success(expect_mapequal(list(a = 1, b = 2), list(b = 2, a = 1)))
})

test_that("ignores order recursively", {
x <- list(outer_1 = 1, outer_2 = list(inner_1 = 1, inner_2 = 2))
y <- list(outer_2 = list(inner_2 = 2, inner_1 = 1), outer_1 = 1)
expect_success(expect_mapequal(x, y))
})

test_that("error if any names are duplicated", {
expect_error(expect_mapequal(list(a = 1, b = 2, b = 3), list(b = 2, a = 1)))
expect_error(expect_mapequal(list(a = 1, b = 2), list(b = 3, b = 2, a = 1)))
expect_error(expect_mapequal(
expect_failure(expect_mapequal(list(a = 1, b = 2, b = 3), list(b = 2, a = 1)))
expect_failure(expect_mapequal(list(a = 1, b = 2), list(b = 3, b = 2, a = 1)))
expect_failure(expect_mapequal(
list(a = 1, b = 2, b = 3),
list(b = 3, b = 2, a = 1)
))
Expand All @@ -75,31 +81,9 @@ test_that("fails if values don't match", {
expect_failure(expect_mapequal(list(a = 1, b = 2), list(a = 1, b = 3)))
})

test_that("check inputs", {
unnamed <- list(1)
named <- list(a = 1)
duplicated <- list(x = 1, x = 2)

expect_snapshot(error = TRUE, {
expect_mapequal(sum, named)
expect_mapequal(named, sum)

expect_mapequal(unnamed, named)
expect_mapequal(named, unnamed)

expect_mapequal(named, duplicated)
expect_mapequal(duplicated, named)
})
})

test_that("succeeds if comparing empty named and unnamed vectors", {
x1 <- list()
x2 <- setNames(list(), character())

expect_warning(expect_success(expect_mapequal(x1, x1)))
expect_warning(expect_success(expect_mapequal(x1, x2)))
expect_warning(expect_success(expect_mapequal(x2, x1)))
expect_warning(expect_success(expect_mapequal(x2, x2)))
test_that("fails if unnamed values in different location if any unnamed values", {
expect_success(expect_mapequal(list(1, b = 2, c = 3), list(1, c = 3, b = 2)))

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should unnamed values be allowed at all in expect_mapequal()?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think waldo switches to "not a map" if there are any unnamed values, which seems reasonable to me. It will cause a failure, rather than an error, but I don't think that makes much practical difference.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So that should mean that this test should fail, right?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmmmm, ok that's not actually what waldo does — when it turns a list into a map, it removes all NULLs, and then reordered named elements, preserving the location of unnamed. That's a different choice but I think equally valid.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Perhaps so. Maybe this is moot because the use case I think of for mapequal is comparing something coming in from a JSON object, and {1, "b": 2} is not valid JSON. So I would never have a test like this line, and thus it doesn't matter what expect_mapequal() does in this case.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, makes sense.

expect_failure(expect_mapequal(list(1, b = 2, c = 3), list(b = 2, 1, c = 3)))
})

# contains ----------------------------------------------------------------
Expand Down
Loading