R/step.R

Defines functions assert_step get_steps clear_steps register_step .steps make_step step_regex

#' Creates a regex from step name and a description
#'
#' Allows whitespace before and after the description
#' @keywords internal
#' @noRd
step_regex <- function(description) {
  paste0("^\\s*", description, "\\s*$")
}

#' @keywords internal
#' @importFrom rlang exec
#' @importFrom checkmate assert_subset assert_string assert_function
make_step <- function(prefix) {
  function(description, implementation) {
    assert_string(description)
    assert_function(implementation)
    args <- formals(implementation)
    assert_subset("context", names(args[length(args)]))
    step <- structure(
      implementation,
      description = description,
      detect = step_regex(description),
      class = c("step", class(implementation))
    )
    register_step(step)
    invisible(step)
  }
}

#' Define a step
#'
#' Provide a description that matches steps in feature files and the implementation function that will be run.
#'
#' @name step
#' @param description A description of the step.
#'
#' Cucumber executes each step in a scenario one at a time, in the sequence you’ve written them in.
#' When Cucumber tries to execute a step, it looks for a matching step definition to execute.
#'
#' **Keywords are not taken into account when looking for a step definition.**
#' This means you cannot have a `Given`, `When`, `Then`, `And` or `But` step with the same text as another step.
#'
#' Cucumber considers the following steps duplicates:
#'
#' ```
#' Given there is money in my account
#' Then there is money in my account
#' ```
#'
#' This might seem like a limitation, but it forces you to come up with a less ambiguous, more clear domain language:
#'
#' ```
#' Given my account has a balance of £430
#' Then my account should have a balance of £430
#' ```
#'
#' To pass arguments, description can contain placeholders in curly braces.
#'
#' To match:
#'
#' ```
#' Given my account has a balance of £430
#' ```
#'
#' use:
#'
#' ```
#' given("my account has a balance of £{float}", function(balance, context) {
#'
#' })
#' ```
#'
#' If no step definition is found an error will be thrown.
#'
#' If multiple steps definitions for a single step are found an error will be thrown.
#'
#' @param implementation A function that will be run during test execution.
#'
#' The implementation function must always have the last parameter named `context`.
#' It holds the environment where test state can be stored to be passed to the next step.
#'
#' If a step has a description `"I have {int} cucumbers in my basket"` then the implementation
#' function should be `function(n, context)`. The `{int}` value will be passed to
#' `n`, this parameter can have any name.
#'
#' If a table or a docstring is defined for a step, it will be passed as an argument after placeholder parameters
#' and before `context`. The function should be a `function(n, table, context)`.
#' See
#' \href{https://github.com/jakubsob/cucumber/blob/main/inst/examples/table/tests/acceptance/setup-steps.R}{an example}
#' on how to write implementation that uses tables or docstrings.
#'
#' @examples
#' given("I have {int} cucumbers in my basket", function(n_cucumbers, context) {
#'   context$n_cucumbers <- n_cucumbers
#' })
#'
#' given("I have {int} cucumbers in my basket and a table", function(n_cucumbers, table, context) {
#'   context$n_cucumbers <- n_cucumbers
#'   context$table <- table
#' })
#'
#' when("I eat {int} cucumbers", function(n_cucumbers, context) {
#'   context$n_cucumbers <- context$n_cucumbers - n_cucumbers
#' })
#'
#' then("I should have {int} cucumbers in my basket", function(n_cucumbers, context) {
#'   expect_equal(context$n_cucumbers, n_cucumbers)
#' })
#'
#' @details
#' Placeholders in expressions are replaced with regular expressions that match values in the feature file.
#' Regular expressions are generated during runtime based on defined parameter types.
#'
#' The expression `"I have {int} cucumbers in my basket"` will be converted to
#' `"I have [+-]?(?<![.])[:digit:]+(?![.]) cucumbers in my basket"`. The extracted value of `{int}`
#' will be passed to the implementation function after being transformed with `as.integer`.
#'
#' To define your own parameter types use \code{\link{define_parameter_type}}.
#'
#' @md
#' @seealso [define_parameter_type()]
#' @return A function of class \code{step}, invisibly. Function should be called for side effects.
NULL

#' @rdname step
#' @export
given <- make_step("Given")

#' @rdname step
#' @export
when <- make_step("When")

#' @rdname step
#' @export
then <- make_step("Then")

#' @importFrom rlang list2
.steps <- function(...) {
  structure(list2(...), class = "steps")
}

#' @importFrom rlang `:=`
register_step <- function(step) {
  assert_step(step)
  steps <- getOption(getOption(".cucumber_steps_option"), default = .steps())
  steps <- .steps(!!!steps, !!attr(step, "description") := step)
  options(list2(!!getOption(".cucumber_steps_option") := steps))
  invisible(step)
}

clear_steps <- function() {
  options(list2(!!getOption(".cucumber_steps_option") := .steps()))
}

get_steps <- function() {
  getOption(getOption(".cucumber_steps_option"), default = .steps())
}

#' @importFrom checkmate assert_function assert_class assert_string
assert_step <- function(x) {
  assert_function(x)
  assert_class(x, "step")
  assert_string(attr(x, "description"))
  assert_string(attr(x, "detect"))
}

Try the cucumber package in your browser

Any scripts or data that you put into this service are public.

cucumber documentation built on June 8, 2025, 12:47 p.m.