Skip to content

Request: expect_messages() or piped calls for matching multiple messages without the complexity of expect_snapshot() #2342

Description

@orgadish

Since capture_messages() was deprecated, the only way to match against multiple messages is either to use expect_snapshot() or to call the function twice, e.g. expect_message(f(), "message1") and expect_message(f(), "message2").

When running lots of tests that cover 1-2 messages, expect_snapshot seems to be a lot of overhead to add for all those different calls.

One option: expect_messages() with nomatch()

Claude has helped me build an expect_messages() function with a nomatch() helper, but it would be much better if it were built more deeply into testthat (see below).

e.g.

expect_message(my_function(), "message1", "message2", nomatch("should not match"))
Claude's expect_messages() and nomatch helpers
# Helpers to test multiple messages in sequence.
# Usage:
#   expect_messages(out <- f(), "message1", "message2", nomatch("message3"))
# Note:
#   Single-message tests should still use testthat::expect_message.
nomatch <- function(pattern) structure(pattern, excluded = TRUE)

expect_messages <- function(expr, ...) {
    actual_messages <- character()

    result <- withCallingHandlers(
        expr,
        message = function(m) {
            actual_messages <<- c(actual_messages, conditionMessage(m))
            invokeRestart("muffleMessage")
        }
    )

    for (pattern in list(...)) {
        is_excluded <- isTRUE(attr(pattern, "excluded"))
        matched     <- any(grepl(pattern, actual_messages))

        if (is_excluded && matched) {
            fail(sprintf(
                "Message matched excluded pattern '%s'.\nActual messages:\n%s",
                pattern,
                paste("-", encodeString(actual_messages), collapse = "\n")
            ))
        } else if (!is_excluded && !matched) {
            fail(sprintf(
                "No message matched pattern '%s'.\nActual messages:\n%s",
                pattern,
                paste("-", encodeString(actual_messages), collapse = "\n")
            ))
        }
    }

    invisible(result)
}

Another option: piped/nested calls

Another alternative would be piped/nested expect_message() calls, which currently don't seem to work. Potentially because when the first one succeeds it doesn't bubble up the other messages?

e.g.

expect_message(my_function(), "some_message") |>
  expect_message("some_other_message") |>
  expect_no_message("this message should not exist")  # Note that this can currently be run but will always succeed because expect_message swallows

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions