Module: GenevaDrive::TestHelpers

Defined in:
lib/geneva_drive/test_helpers.rb

Overview

Test helpers for testing workflows in your application.

Include this module in your test classes to get access to workflow testing utilities.

Examples:

Using in a test

class SignupWorkflowTest < ActiveSupport::TestCase
  include GenevaDrive::TestHelpers

  test "completes all steps" do
    workflow = SignupWorkflow.create!(hero: users(:one))
    speedrun_workflow(workflow)
    assert workflow.finished?
  end
end

Instance Method Summary collapse

Instance Method Details

#assert_cursor(workflow, expected_cursor) ⇒ void

This method returns an undefined value.

Asserts that a resumable step's cursor matches the expected value.

Examples:

Check cursor position

assert_cursor(workflow, 42)
assert_cursor(workflow, "page_token_abc")

Parameters:

  • workflow (GenevaDrive::Workflow) —

    the workflow to check

  • expected_cursor (Object) —

    the expected cursor value



272
273
274
275
276
277
278
279
# File 'lib/geneva_drive/test_helpers.rb', line 272

def assert_cursor(workflow, expected_cursor)
  workflow.reload
  execution = workflow.current_execution
  actual = execution&.cursor_value

  assert_equal expected_cursor, actual,
    "Expected cursor #{expected_cursor.inspect}, got #{actual.inspect}"
end

#assert_signal_state(workflow, signal_name, state) ⇒ void

This method returns an undefined value.

Asserts that a signal with the given name exists in the given state.

Examples:

Check that the signal was consumed

assert_signal_state(workflow, :payment_confirmed, :consumed)

Parameters:

  • workflow (GenevaDrive::Workflow) —

    the workflow to check

  • signal_name (String, Symbol) —

    the signal name

  • state (String, Symbol) —

    the expected signal state



338
339
340
341
342
343
344
# File 'lib/geneva_drive/test_helpers.rb', line 338

def assert_signal_state(workflow, signal_name, state)
  signal = workflow.signals.where(name: signal_name.to_s).order(:created_at, :id).last

  assert signal, "Expected a #{signal_name.inspect} signal on #{workflow.class.name}, but none was found"
  assert_equal state.to_s, signal.state,
    "Expected signal #{signal_name.inspect} to be #{state}, but was #{signal.state}"
end

#assert_step_executed(workflow, step_name, state: "completed") ⇒ void

This method returns an undefined value.

Asserts that a workflow has executed a specific step.

Examples:

Check that a step was executed

assert_step_executed(workflow, :send_email)
assert_step_executed(workflow, :optional_step, state: "skipped")

Parameters:

  • workflow (GenevaDrive::Workflow) —

    the workflow to check

  • step_name (String, Symbol) —

    the expected step name

  • state (String) (defaults to: "completed") —

    the expected step state (default: "completed")



154
155
156
157
158
159
160
161
162
# File 'lib/geneva_drive/test_helpers.rb', line 154

def assert_step_executed(workflow, step_name, state: "completed")
  step_execution = workflow.step_executions.find_by(
    step_name: step_name.to_s,
    state: state
  )

  assert step_execution,
    "Expected step '#{step_name}' to be #{state}, but it was not found"
end

#assert_step_has_successor(workflow, step_name) ⇒ void

This method returns an undefined value.

Asserts that a step completed and has a scheduled successor execution. Used to verify that a resumable step was interrupted and will continue.

Examples:

Check step has a scheduled successor

assert_step_has_successor(workflow, :bulk_process)

Parameters:

  • workflow (GenevaDrive::Workflow) —

    the workflow to check

  • step_name (String, Symbol) —

    the step name to check



291
292
293
294
295
296
297
298
299
# File 'lib/geneva_drive/test_helpers.rb', line 291

def assert_step_has_successor(workflow, step_name)
  workflow.reload
  completed = workflow.step_executions.find_by(step_name: step_name.to_s, state: "completed")
  assert completed, "Expected completed execution for step #{step_name}"

  successor = completed.successor
  assert successor, "Expected successor execution for step #{step_name}, but none found"
  assert successor.scheduled?, "Expected successor to be scheduled, but was #{successor.state}"
end

#assert_waiting_for_signal(workflow, signal_name = nil) ⇒ void

This method returns an undefined value.

Asserts that the workflow is parked waiting for a signal.

Examples:

Check that the workflow is waiting

assert_waiting_for_signal(workflow, :payment_confirmed)

Parameters:

  • workflow (GenevaDrive::Workflow) —

    the workflow to check

  • signal_name (String, Symbol, nil) (defaults to: nil) —

    the signal name the step waits for



312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
# File 'lib/geneva_drive/test_helpers.rb', line 312

def assert_waiting_for_signal(workflow, signal_name = nil)
  workflow.reload
  execution = workflow.step_executions.where(state: "waiting").first

  assert execution,
    "Expected #{workflow.class.name} to be waiting for a signal, but no parked step execution was found " \
    "(current execution: #{workflow.current_execution&.state.inspect})"

  return unless signal_name

  matcher = execution.step_definition&.signal_matcher
  assert_equal signal_name.to_s, matcher_name(matcher),
    "Expected step '#{execution.step_name}' to be waiting for #{signal_name.inspect}, " \
    "but it waits for #{matcher}"
end

#assert_workflow_state(workflow, expected_state) ⇒ void

This method returns an undefined value.

Asserts that a workflow is in a specific state.

Examples:

Check workflow state

assert_workflow_state(workflow, :finished)
assert_workflow_state(workflow, "paused")

Parameters:

  • workflow (GenevaDrive::Workflow) —

    the workflow to check

  • expected_state (String, Symbol) —

    the expected state



174
175
176
177
178
# File 'lib/geneva_drive/test_helpers.rb', line 174

def assert_workflow_state(workflow, expected_state)
  workflow.reload
  assert_equal expected_state.to_s, workflow.state,
    "Expected workflow to be #{expected_state}, but was #{workflow.state}"
end

#perform_next_step(workflow) ⇒ GenevaDrive::StepExecution?

Executes only the next pending step of a workflow.

Useful for testing step-by-step behavior and inspecting state between steps.

Examples:

Execute one step at a time

workflow = PaymentWorkflow.create!(hero: order)
perform_next_step(workflow)
assert_equal "charge", workflow.current_step_name
perform_next_step(workflow)
assert_equal "receipt", workflow.current_step_name

Parameters:

Returns:



79
80
81
82
83
84
85
86
87
88
89
# File 'lib/geneva_drive/test_helpers.rb', line 79

def perform_next_step(workflow)
  workflow.reload
  step_execution = workflow.current_execution
  return nil unless step_execution

  raise_if_parked!(step_execution, "perform_next_step")

  step_execution.execute!
  workflow.reload
  step_execution
end

#perform_step_inline(workflow, step_name = nil) ⇒ GenevaDrive::StepExecution

Executes a step inline, bypassing normal workflow progression.

Creates a step execution for the given step and executes it immediately. This is a testing shortcut that allows you to test individual steps without running through the entire flow.

When called without a step name, executes the current step.

Examples:

Execute the current step

workflow = OnboardingWorkflow.create!(hero: user)
perform_step_inline(workflow)

Execute a specific step directly

workflow = OnboardingWorkflow.create!(hero: user)
perform_step_inline(workflow, :send_welcome_email)

Parameters:

  • workflow (GenevaDrive::Workflow) —

    the workflow containing the step

  • step_name (String, Symbol, nil) (defaults to: nil) —

    the step to execute (defaults to current step)

Returns:

Raises:

  • (ArgumentError) —

    if the step is not defined in the workflow



112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
# File 'lib/geneva_drive/test_helpers.rb', line 112

def perform_step_inline(workflow, step_name = nil)
  workflow.reload
  step_name ||= workflow.next_step_name
  step_def = workflow.class.steps.find { |s| s.name == step_name.to_s }

  unless step_def
    available = workflow.class.steps.map(&:name).join(", ")
    raise ArgumentError,
      "Step '#{step_name}' is not defined in #{workflow.class.name}. " \
      "Available steps: #{available}"
  end

  # Cancel any existing scheduled or parked step executions to satisfy the
  # uniqueness constraint
  workflow.step_executions.where(state: %w[scheduled waiting]).update_all(
    state: "canceled",
    outcome: "canceled",
    canceled_at: Time.current
  )

  step_execution = workflow.step_executions.create!(
    step_name: step_def.name,
    state: "scheduled",
    scheduled_for: Time.current
  )

  step_execution.execute!
  workflow.reload
  step_execution
end

#run_iterations(workflow, count:) ⇒ GenevaDrive::StepExecution

Runs the current (resumable) step for a limited number of iterations, then lets it interrupt and create a successor execution.

Useful for testing partial progress and cursor state of resumable steps.

Examples:

Run three iterations, then inspect the cursor

run_iterations(workflow, count: 3)
assert_cursor(workflow, 3)

Parameters:

  • workflow (GenevaDrive::Workflow) —

    the workflow containing the step

  • count (Integer) —

    the number of iterations to run before interrupting

Returns:



250
251
252
253
254
255
256
257
258
259
260
# File 'lib/geneva_drive/test_helpers.rb', line 250

def run_iterations(workflow, count:)
  workflow.reload
  step_execution = workflow.current_execution
  raise "No current step execution to run" unless step_execution

  step_execution.execute!(max_iterations: count)

  workflow.reload
  step_execution.reload
  step_execution
end

#speedrun_current_step(workflow, max_executions: 100) ⇒ GenevaDrive::StepExecution

Runs the current step to completion without advancing to the next step.

For regular steps, this is equivalent to perform_next_step. For resumable steps, this disables interruption checks and runs until the step completes (or fails/cancels/skips).

Examples:

Run current step to completion

workflow = BulkEmailWorkflow.create!(hero: campaign)
speedrun_current_step(workflow)
assert_step_executed(workflow, :send_emails)

Parameters:

  • workflow (GenevaDrive::Workflow) —

    the workflow containing the step

  • max_executions (Integer) (defaults to: 100) —

    safety limit on job executions (default: 100)

Returns:



197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
# File 'lib/geneva_drive/test_helpers.rb', line 197

def speedrun_current_step(workflow, max_executions: 100)
  workflow.reload
  step_execution = workflow.current_execution
  return nil unless step_execution

  raise_if_parked!(step_execution, "speedrun_current_step")

  executions = 0
  original_step_name = step_execution.step_name

  loop do
    step_execution.reload
    raise_if_parked!(step_execution, "speedrun_current_step")

    # Follow the execution chain: a completed execution that spawned a
    # successor means the step is still going (suspend! and skip_to! create
    # successors even with interruption checks disabled).
    if GenevaDrive::StepExecution.resumable_columns? &&
        step_execution.completed? && (successor = step_execution.successor)
      step_execution = successor
      next
    end

    break if step_execution.completed? || step_execution.failed? || step_execution.canceled? || step_execution.skipped?

    # interruptible: false so the step runs to completion
    step_execution.execute!(interruptible: false)
    executions += 1

    if executions >= max_executions
      raise "speedrun_current_step exceeded max_executions (#{max_executions}). " \
            "Step '#{original_step_name}' may be in an infinite loop."
    end
  end

  workflow.reload
  step_execution.reload
  step_execution
end

#speedrun_workflow(workflow, max_iterations: 100) ⇒ GenevaDrive::Workflow

Executes all steps of a workflow synchronously for testing.

This method runs through all scheduled steps, executing each one immediately regardless of wait times. Resumable steps are run to completion (interruption limits are disabled).

Examples:

Run a workflow to completion

workflow = OnboardingWorkflow.create!(hero: user)
speedrun_workflow(workflow)
assert_equal "finished", workflow.state

Run with custom iteration limit

speedrun_workflow(workflow, max_iterations: 10)

Parameters:

  • workflow (GenevaDrive::Workflow) —

    the workflow to run

  • max_iterations (Integer) (defaults to: 100) —

    maximum step executions (safety limit)

Returns:

Raises:

  • (RuntimeError) —

    if max_iterations is exceeded



39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
# File 'lib/geneva_drive/test_helpers.rb', line 39

def speedrun_workflow(workflow, max_iterations: 100)
  iterations = 0

  loop do
    workflow.reload
    break if %w[finished canceled paused].include?(workflow.state)

    step_execution = workflow.current_execution
    break unless step_execution

    raise_if_parked!(step_execution, "speedrun_workflow")

    # interruptible: false so resumable steps run to completion
    step_execution.execute!(interruptible: false)
    iterations += 1

    if iterations >= max_iterations
      raise "speedrun_workflow exceeded max_iterations (#{max_iterations}). " \
            "Workflow may be in an infinite loop."
    end
  end

  workflow.reload
end