Skip to content
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)

* Expectations now consistently return the value of the first argument, regardless of whether the expectation succeeds or fails. The primary exception are `expect_message()` and friends which will return the condition. This shouldn't affect existing tests, but will make failures clearer when you chain together multiple expectations (#2246).
Copy link
Member

Choose a reason for hiding this comment

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

Suggested change
* Expectations now consistently return the value of the first argument, regardless of whether the expectation succeeds or fails. The primary exception are `expect_message()` and friends which will return the condition. This shouldn't affect existing tests, but will make failures clearer when you chain together multiple expectations (#2246).
* Expectations now consistently return the value of the first argument, regardless of whether the expectation succeeds or fails. The primary exceptions are `expect_message()` and friends which will return the condition. This shouldn't affect existing tests, but will make failures clearer when you chain together multiple expectations (#2246).

* `set_state_inspector()` gains `tolerance` argument and ignores minor FP differences by default (@mcol, #2237).
* `expect_vector()` fails, instead of erroring, if `object` is not a vector (@plietar, #2224).
* New `vignette("mocking")` explains mocking in detail (#1265).
Expand Down
69 changes: 40 additions & 29 deletions R/expect-comparison.R
Original file line number Diff line number Diff line change
Expand Up @@ -28,52 +28,60 @@ expect_compare_ <- function(
operator <- match.arg(operator)
op <- match.fun(operator)

actual_op <- switch(operator, "<" = ">=", "<=" = ">", ">" = "<=", ">=" = "<")

cmp <- op(act$val, exp$val)
if (length(cmp) != 1 || !is.logical(cmp)) {
cli::cli_abort(
"Result of comparison must be a single logical value.",
"Result of comparison must be `TRUE`, `FALSE`, or `NA`",
call = trace_env
)
} else if (!isTRUE(cmp)) {
msg <- failure_compare(act, exp, operator)
fail(msg, trace_env = trace_env)
} else {
pass()
}
if (!isTRUE(cmp)) {
Copy link
Member

Choose a reason for hiding this comment

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

I'm totally with you on no longer wanting to do an early return anymore

Like, I think seeing an if/else construct where one branch ends in a pass() and one branch ends in a fail() could be a defining feature of all of these expectations.

In other words, I prefer this version of expect_compare_() much more

expect_compare_ <- function(
  operator = c("<", "<=", ">", ">="),
  act,
  exp,
  trace_env = caller_env()
) {
  operator <- match.arg(operator)
  op <- match.fun(operator)

  actual_op <- switch(operator, "<" = ">=", "<=" = ">", ">" = "<=", ">=" = "<")

  cmp <- op(act$val, exp$val)
  if (length(cmp) != 1 || !is.logical(cmp)) {
    cli::cli_abort(
      "Result of comparison must be a single logical value.",
      call = trace_env
    )
  }

  if (isTRUE(cmp)) {
    pass()
  } else {
    diff <- act$val - exp$val
    msg_exp <- sprintf("Expected %s %s %s.", act$lab, operator, exp$lab)

    digits <- max(
      digits(act$val),
      digits(exp$val),
      min_digits(act$val, exp$val)
    )

    msg_act <- sprintf(
      "Actual comparison: %s %s %s",
      num_exact(act$val, digits),
      actual_op,
      num_exact(exp$val, digits)
    )

    if (is.na(diff)) {
      msg_diff <- NULL
    } else {
      msg_diff <- sprintf(
        "Difference: %s %s 0",
        num_exact(diff, digits),
        actual_op
      )
    }
    fail(c(msg_exp, msg_act, msg_diff), trace_env = trace_env)
  }

  invisible(act$val)
}

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, I was thinking that instead of an early return, pulling out a helper to generate the failure message would be the way to go.

Copy link
Member

Choose a reason for hiding this comment

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

yea everything except the fail(), i.e. this bit

    diff <- act$val - exp$val
    msg_exp <- sprintf("Expected %s %s %s.", act$lab, operator, exp$lab)

    digits <- max(
      digits(act$val),
      digits(exp$val),
      min_digits(act$val, exp$val)
    )

    msg_act <- sprintf(
      "Actual comparison: %s %s %s",
      num_exact(act$val, digits),
      actual_op,
      num_exact(exp$val, digits)
    )

    if (is.na(diff)) {
      msg_diff <- NULL
    } else {
      msg_diff <- sprintf(
        "Difference: %s %s 0",
        num_exact(diff, digits),
        actual_op
      )
    }

diff <- act$val - exp$val
msg_exp <- sprintf("Expected %s %s %s.", act$lab, operator, exp$lab)

digits <- max(
digits(act$val),
digits(exp$val),
min_digits(act$val, exp$val)
)
}

msg_act <- sprintf(
"Actual comparison: %s %s %s",
num_exact(act$val, digits),
actual_op,
num_exact(exp$val, digits)
)
failure_compare <- function(act, exp, operator) {
actual_op <- switch(operator, "<" = ">=", "<=" = ">", ">" = "<=", ">=" = "<")

if (is.na(diff)) {
msg_diff <- NULL
} else {
msg_diff <- sprintf(
"Difference: %s %s 0",
num_exact(diff, digits),
actual_op
)
}
return(fail(c(msg_exp, msg_act, msg_diff), trace_env = trace_env))
diff <- act$val - exp$val
msg_exp <- sprintf("Expected %s %s %s.", act$lab, operator, exp$lab)

digits <- max(
digits(act$val),
digits(exp$val),
min_digits(act$val, exp$val)
)

msg_act <- sprintf(
"Actual comparison: %s %s %s",
num_exact(act$val, digits),
actual_op,
num_exact(exp$val, digits)
)

if (is.na(diff)) {
msg_diff <- NULL
} else {
msg_diff <- sprintf(
"Difference: %s %s 0",
num_exact(diff, digits),
actual_op
)
}
pass(act$val)

c(msg_exp, msg_act, msg_diff)
}

#' @export
#' @rdname comparison-expectations
expect_lt <- function(object, expected, label = NULL, expected.label = NULL) {
act <- quasi_label(enquo(object), label)
exp <- quasi_label(enquo(expected), expected.label)

expect_compare_("<", act, exp)
invisible(act$val)
}

#' @export
Expand All @@ -83,6 +91,7 @@ expect_lte <- function(object, expected, label = NULL, expected.label = NULL) {
exp <- quasi_label(enquo(expected), expected.label)

expect_compare_("<=", act, exp)
invisible(act$val)
}

#' @export
Expand All @@ -92,6 +101,7 @@ expect_gt <- function(object, expected, label = NULL, expected.label = NULL) {
exp <- quasi_label(enquo(expected), expected.label)

expect_compare_(">", act, exp)
invisible(act$val)
}

#' @export
Expand All @@ -101,6 +111,7 @@ expect_gte <- function(object, expected, label = NULL, expected.label = NULL) {
exp <- quasi_label(enquo(expected), expected.label)

expect_compare_(">=", act, exp)
invisible(act$val)
}


Expand Down
32 changes: 21 additions & 11 deletions R/expect-condition.R
Original file line number Diff line number Diff line change
Expand Up @@ -143,9 +143,11 @@ expect_error <- function(
# Access error fields with `[[` rather than `$` because the
# `$.Throwable` from the rJava package throws with unknown fields
if (!is.null(msg)) {
return(fail(msg, info = info, trace = act$cap[["trace"]]))
fail(msg, info = info, trace = act$cap[["trace"]])
} else {
pass()
}
Comment on lines 145 to 149
Copy link
Member

Choose a reason for hiding this comment

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

My brain really wants

    if (is.null(msg)) {
      pass()
    } else {
      fail(msg, info = info, trace = act$cap[["trace"]])
    }

i.e. always pass() first then fail()

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'm actually starting to think it pass() should always be last because if there are multiple conditions that need to be satisfied in order to pass, you want to check each of them in turn.

Copy link
Member

Choose a reason for hiding this comment

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

I like seeing pass() in the if branch and fail() in the else branch (unless it is a series of fail, fail, fail, pass) and there are a lot of places we don't do that. Up to you.

yea i noted that as my exception here

pass(act$val %||% act$cap)
invisible(act$val %||% act$cap)
}
}

Expand Down Expand Up @@ -198,9 +200,11 @@ expect_warning <- function(
cond_type = "warnings"
)
if (!is.null(msg)) {
return(fail(msg, info = info))
fail(msg, info = info)
} else {
pass()
}
pass(act$val)
invisible(act$val)
}
}

Expand Down Expand Up @@ -236,9 +240,11 @@ expect_message <- function(
act <- quasi_capture(enquo(object), label, capture_messages)
msg <- compare_messages(act$cap, act$lab, regexp = regexp, all = all, ...)
if (!is.null(msg)) {
return(fail(msg, info = info))
fail(msg, info = info)
} else {
pass()
}
pass(act$val)
invisible(act$val)
}
}

Expand Down Expand Up @@ -285,9 +291,11 @@ expect_condition <- function(
cond_type = "condition"
)
if (!is.null(msg)) {
return(fail(msg, info = info, trace = act$cap[["trace"]]))
fail(msg, info = info, trace = act$cap[["trace"]])
} else {
pass()
}
pass(act$val %||% act$cap)
invisible(act$val %||% act$cap)
}
}

Expand Down Expand Up @@ -327,16 +335,18 @@ expect_condition_matching_ <- function(
# Access error fields with `[[` rather than `$` because the
# `$.Throwable` from the rJava package throws with unknown fields
if (!is.null(msg)) {
return(fail(
fail(
msg,
info = info,
trace = act$cap[["trace"]],
trace_env = trace_env
))
)
} else {
pass()
}
# If a condition was expected, return it. Otherwise return the value
# of the expression.
pass(if (expected) act$cap else act$val)
invisible(if (expected) act$cap else act$val)
}

# -------------------------------------------------------------------------
Expand Down
11 changes: 9 additions & 2 deletions R/expect-constant.R
Original file line number Diff line number Diff line change
Expand Up @@ -31,15 +31,19 @@ NULL
expect_true <- function(object, info = NULL, label = NULL) {
act <- quasi_label(enquo(object), label)
exp <- labelled_value(TRUE, "TRUE")

expect_waldo_constant_(act, exp, info = info, ignore_attr = TRUE)
invisible(act$val)
}

#' @export
#' @rdname logical-expectations
expect_false <- function(object, info = NULL, label = NULL) {
act <- quasi_label(enquo(object), label)
exp <- labelled_value(FALSE, "FALSE")

expect_waldo_constant_(act, exp, info = info, ignore_attr = TRUE)
invisible(act$val)
}

#' Do you expect `NULL`?
Expand All @@ -59,7 +63,9 @@ expect_false <- function(object, info = NULL, label = NULL) {
expect_null <- function(object, info = NULL, label = NULL) {
act <- quasi_label(enquo(object), label)
exp <- labelled_value(NULL, "NULL")

expect_waldo_constant_(act, exp, info = info)
invisible(act$val)
}

expect_waldo_constant_ <- function(
Expand All @@ -82,7 +88,8 @@ expect_waldo_constant_ <- function(
"Differences:",
paste0(comp, collpase = "\n")
)
return(fail(msg, info = info, trace_env = trace_env))
fail(msg, info = info, trace_env = trace_env)
} else {
pass()
}
pass(act$val)
}
32 changes: 17 additions & 15 deletions R/expect-equality.R
Original file line number Diff line number Diff line change
Expand Up @@ -76,16 +76,18 @@ expect_equal <- function(
comp <- compare(act$val, exp$val, ...)
}

if (!comp$equal) {
if (comp$equal) {
pass()
} else {
msg <- c(
sprintf("Expected %s to equal %s.", act$lab, exp$lab),
"Differences:",
comp$message
)
return(fail(msg, info = info))
fail(msg, info = info)
}
pass(act$val)
}
invisible(act$val)
}


Expand All @@ -105,28 +107,25 @@ expect_identical <- function(
if (edition_get() >= 3) {
expect_waldo_equal_("identical", act, exp, info, ...)
} else {
ident <- identical(act$val, exp$val, ...)
if (ident) {
msg_act <- NULL
if (identical(act$val, exp$val, ...)) {
pass()
} else {
compare <- compare(act$val, exp$val)
if (compare$equal) {
msg_act <- "Objects equal but not identical"
} else {
msg_act <- compare$message
}
}

if (!ident) {
msg <- c(
sprintf("Expected %s to be identical to %s.", act$lab, exp$lab),
"Differences:",
msg_act
)
return(fail(msg, info = info))
fail(msg, info = info)
}
pass(act$val)
}
invisible(act$val)
}

expect_waldo_equal_ <- function(
Expand All @@ -144,15 +143,16 @@ expect_waldo_equal_ <- function(
x_arg = "actual",
y_arg = "expected"
)
if (length(comp) != 0) {
if (length(comp) == 0) {
pass()
} else {
msg <- c(
sprintf("Expected %s to be %s to %s.", act$lab, type, exp$lab),
"Differences:",
paste0(comp, collpase = "\n")
)
return(fail(msg, info = info, trace_env = trace_env))
fail(msg, info = info, trace_env = trace_env)
}
pass(act$val)
}

#' Is an object equal to the expected value, ignoring attributes?
Expand Down Expand Up @@ -203,7 +203,9 @@ expect_equivalent <- function(
exp$lab,
comp$message
)
return(fail(msg, info = info))
fail(msg, info = info)
} else {
pass()
Copy link
Member

Choose a reason for hiding this comment

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

flip pass fail order?

}
pass(act$val)
invisible(act$val)
}
Loading