Module: Familia::Validation::TestHelpers

Included in:
FamiliaTestHelpers
Defined in:
lib/familia/validation/test_helpers.rb

Overview

Test helper methods for integrating Redis command validation with the tryouts testing framework. Provides easy-to-use assertion methods and automatic setup/cleanup for command validation tests.

Examples:

Basic usage in a try file

require_relative '../validation/test_helpers'
extend Familia::Validation::TestHelpers

## User save should execute expected Redis commands
user = TestUser.new(id: "123", name: "John")

assert_redis_commands do |expect|
  expect.hset("testuser:123:object", "name", "John")
        .hset("testuser:123:object", "id", "123")

  user.save
end
#=> true

Transaction validation

assert_atomic_operation do |expect|
  expect.transaction do |tx|
    tx.hset("account:123", "balance", "1000")
      .hset("account:456", "balance", "2000")
  end

  transfer_funds(from: "123", to: "456", amount: 500)
end
#=> true

Instance Method Summary collapse

Instance Method Details

#any_numberObject



347
348
349
# File 'lib/familia/validation/test_helpers.rb', line 347

def any_number
  ArgumentMatcher.new(:any_number)
end

#any_stringObject



343
344
345
# File 'lib/familia/validation/test_helpers.rb', line 343

def any_string
  ArgumentMatcher.new(:any_string)
end

#any_valueObject



351
352
353
# File 'lib/familia/validation/test_helpers.rb', line 351

def any_value
  ArgumentMatcher.new(:any_value)
end

#assert_atomic_operation(message = nil, &block) ⇒ Object

Assert that a block executes Redis commands atomically



51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
# File 'lib/familia/validation/test_helpers.rb', line 51

def assert_atomic_operation(message = nil, &block)
  validator = Validator.new(strict_atomicity: true)

  if block.arity == 1
    # Block expects expectations parameter
    result = validator.validate(&block)
  else
    # Block is just execution code - validate atomicity only
    result = validator.validate_atomicity(&block)
  end

  unless result.valid?
    error_msg = message || "Atomic operation validation failed"
    error_msg += "\n" + result.detailed_report
    raise ValidationError, error_msg
  end

  result.valid?
end

#assert_command_count(expected_count, &block) ⇒ Object

Assert that a specific number of commands were executed



106
107
108
109
110
111
112
113
114
115
116
117
118
# File 'lib/familia/validation/test_helpers.rb', line 106

def assert_command_count(expected_count, &block)
  CommandRecorder.start_recording
  block.call if block_given?
  commands = CommandRecorder.stop_recording

  actual_count = commands.command_count
  unless actual_count == expected_count
    error_msg = "Expected #{expected_count} Redis commands, but #{actual_count} were executed"
    raise ValidationError, error_msg
  end

  true
end

#assert_commands_executed(*expected_commands) ⇒ Object

Assert that specific commands were executed (flexible order)



72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
# File 'lib/familia/validation/test_helpers.rb', line 72

def assert_commands_executed(*expected_commands)
  validator = Validator.new

  CommandRecorder.start_recording
  yield if block_given?
  actual_commands = CommandRecorder.stop_recording

  result = validator.assert_commands_executed(expected_commands, actual_commands)

  unless result.valid?
    error_msg = "Expected commands were not executed as specified"
    error_msg += "\n" + result.detailed_report
    raise ValidationError, error_msg
  end

  result.valid?
end

#assert_efficient_commands(&block) ⇒ Object

Assert efficient command usage (no N+1 patterns)



175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
# File 'lib/familia/validation/test_helpers.rb', line 175

def assert_efficient_commands(&block)
  validator = Validator.new(performance_tracking: true)
  commands = capture_redis_commands(&block)

  analysis = validator.analyze_performance(commands)

  if analysis[:efficiency_score] < 70 # Threshold for acceptable efficiency
    error_msg = "Inefficient Redis command usage detected (score: #{analysis[:efficiency_score]})"

    if analysis[:potential_n_plus_one].any?
      error_msg += "\nPotential N+1 patterns:"
      analysis[:potential_n_plus_one].each do |pattern|
        error_msg += "\n  #{pattern[:command]}: #{pattern[:count]} calls - #{pattern[:suggestion]}"
      end
    end

    raise ValidationError, error_msg
  end

  true
end

#assert_no_redis_commands(&block) ⇒ Object

Assert that no Redis commands were executed



91
92
93
94
95
96
97
98
99
100
101
102
103
# File 'lib/familia/validation/test_helpers.rb', line 91

def assert_no_redis_commands(&block)
  CommandRecorder.start_recording
  block.call if block_given?
  commands = CommandRecorder.stop_recording

  unless commands.command_count == 0
    error_msg = "Expected no Redis commands, but #{commands.command_count} were executed:"
    commands.commands.each { |cmd| error_msg += "\n  #{cmd}" }
    raise ValidationError, error_msg
  end

  true
end

#assert_no_transaction_used(&block) ⇒ Object

Assert that commands were NOT executed within a transaction



135
136
137
138
139
140
141
142
143
144
145
146
# File 'lib/familia/validation/test_helpers.rb', line 135

def assert_no_transaction_used(&block)
  CommandRecorder.start_recording
  block.call if block_given?
  commands = CommandRecorder.stop_recording

  unless commands.transaction_count == 0
    error_msg = "Expected operations to NOT use transactions, but #{commands.transaction_count} were found"
    raise ValidationError, error_msg
  end

  true
end

#assert_performance_within(max_duration_ms, &block) ⇒ Object

Performance assertion - assert operations complete within time limit



156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
# File 'lib/familia/validation/test_helpers.rb', line 156

def assert_performance_within(max_duration_ms, &block)
  start_time = Time.now
  CommandRecorder.start_recording

  result = block.call if block_given?

  commands = CommandRecorder.stop_recording
  actual_duration_ms = (Time.now - start_time) * 1000

  if actual_duration_ms > max_duration_ms
    error_msg = "Operation took #{actual_duration_ms.round(2)}ms, " \
               "expected less than #{max_duration_ms}ms"
    raise ValidationError, error_msg
  end

  result
end

#assert_redis_commands(message = nil, &block) ⇒ Object

Assert that a block executes the expected Redis commands



37
38
39
40
41
42
43
44
45
46
47
48
# File 'lib/familia/validation/test_helpers.rb', line 37

def assert_redis_commands(message = nil, &block)
  validator = Validator.new
  result = validator.validate(&block)

  unless result.valid?
    error_msg = message || "Redis command validation failed"
    error_msg += "\n" + result.detailed_report
    raise ValidationError, error_msg
  end

  result.valid?
end

#assert_transaction_used(&block) ⇒ Object

Assert that commands were executed within a transaction



121
122
123
124
125
126
127
128
129
130
131
132
# File 'lib/familia/validation/test_helpers.rb', line 121

def assert_transaction_used(&block)
  CommandRecorder.start_recording
  block.call if block_given?
  commands = CommandRecorder.stop_recording

  unless commands.transaction_count > 0
    error_msg = "Expected operations to use transactions, but none were found"
    raise ValidationError, error_msg
  end

  true
end

#capture_redis_commands(&block) ⇒ Object

Capture and return Redis commands without validation



149
150
151
152
153
# File 'lib/familia/validation/test_helpers.rb', line 149

def capture_redis_commands(&block)
  CommandRecorder.start_recording
  block.call if block_given?
  CommandRecorder.stop_recording
end

#debug_print_commands(command_sequence = nil) ⇒ Object

Debugging helpers



273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
# File 'lib/familia/validation/test_helpers.rb', line 273

def debug_print_commands(command_sequence = nil)
  commands = command_sequence || capture_redis_commands { yield if block_given? }

  puts "Redis Commands Executed (#{commands.command_count} total):"
  puts "=" * 50

  commands.commands.each_with_index do |cmd, i|
    prefix = cmd.atomic_command? ? "[TX]" : "    "
    puts "#{prefix} #{i + 1}. #{cmd} (#{cmd.duration_us}µs)"
  end

  if commands.transaction_count > 0
    puts "\nTransactions (#{commands.transaction_count} total):"
    commands.transaction_blocks.each_with_index do |tx, i|
      puts "  #{i + 1}. #{tx.command_count} commands"
    end
  end

  puts "=" * 50
end

#debug_print_performance(command_sequence = nil) ⇒ Object



294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
# File 'lib/familia/validation/test_helpers.rb', line 294

def debug_print_performance(command_sequence = nil)
  commands = command_sequence || CommandRecorder.current_sequence
  validator = Validator.new(performance_tracking: true)
  analysis = validator.analyze_performance(commands)

  puts "Performance Analysis:"
  puts "=" * 30
  puts "Total Commands: #{analysis[:total_commands]}"
  puts "Total Duration: #{analysis[:total_duration_ms].round(2)}ms"
  puts "Average Command Time: #{analysis[:average_command_time_us].round(2)}µs"
  puts "Efficiency Score: #{analysis[:efficiency_score].round(1)}/100"

  if analysis[:slowest_commands].any?
    puts "\nSlowest Commands:"
    analysis[:slowest_commands].each do |cmd|
      puts "  #{cmd[:command]} (#{cmd[:duration_us]}µs)"
    end
  end

  if analysis[:potential_n_plus_one].any?
    puts "\nPotential N+1 Patterns:"
    analysis[:potential_n_plus_one].each do |pattern|
      puts "  #{pattern[:command]}: #{pattern[:count]} calls"
    end
  end

  puts "=" * 30
end

#expect_data_type_operation(class_name, identifier, type_name, operation, *args) ⇒ Object



265
266
267
268
269
270
# File 'lib/familia/validation/test_helpers.rb', line 265

def expect_data_type_operation(class_name, identifier, type_name, operation, *args)
  dbkey = "#{class_name.to_s.downcase}:#{identifier}:#{type_name}"

  expectations = CommandExpectations.new
  expectations.command(operation, dbkey, *args)
end

#expect_horreum_load(class_name, identifier, fields = []) ⇒ Object



250
251
252
253
254
255
256
257
258
259
260
261
262
263
# File 'lib/familia/validation/test_helpers.rb', line 250

def expect_horreum_load(class_name, identifier, fields = [])
  dbkey = "#{class_name.to_s.downcase}:#{identifier}:object"

  expectations = CommandExpectations.new
  if fields.empty?
    expectations.hgetall(dbkey)
  else
    fields.each do |field|
      expectations.hget(dbkey, field.to_s)
    end
  end

  expectations
end

#expect_horreum_save(class_name, identifier, fields = {}) ⇒ Object

Helper to create expectation builders for common patterns



239
240
241
242
243
244
245
246
247
248
# File 'lib/familia/validation/test_helpers.rb', line 239

def expect_horreum_save(class_name, identifier, fields = {})
  dbkey = "#{class_name.to_s.downcase}:#{identifier}:object"

  expectations = CommandExpectations.new
  fields.each do |field, value|
    expectations.hset(dbkey, field.to_s, value.to_s)
  end

  expectations
end

#match_command(cmd, *args) ⇒ Object

Matcher helpers for more readable tests



324
325
326
327
328
329
330
# File 'lib/familia/validation/test_helpers.rb', line 324

def match_command(cmd, *args)
  if args.empty?
    ->(recorded) { recorded.command == cmd.to_s.upcase }
  else
    ->(recorded) { recorded.command == cmd.to_s.upcase && recorded.args == args.map(&:to_s) }
  end
end

#match_pattern(pattern) ⇒ Object



332
333
334
335
336
337
338
339
340
341
# File 'lib/familia/validation/test_helpers.rb', line 332

def match_pattern(pattern)
  case pattern
  when Regexp
    ->(recorded) { pattern.match?(recorded.to_s) }
  when String
    ->(recorded) { recorded.to_s.include?(pattern) }
  else
    pattern
  end
end

#setup_validation_testObject

Setup and teardown helpers for validation tests



198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
# File 'lib/familia/validation/test_helpers.rb', line 198

def setup_validation_test
  # Ensure middleware is registered
  @original_middleware_state = CommandRecorder.recording?

  # Clear any existing state
  CommandRecorder.clear if CommandRecorder.recording?

  # Enable database logging for better debugging
  @original_logging_state = Familia.enable_database_logging
  Familia.enable_database_logging = true

  # Enable command counting
  @original_counter_state = Familia.enable_database_counter
  Familia.enable_database_counter = true

  DatabaseCommandCounter.reset
end

#teardown_validation_testObject



216
217
218
219
220
221
222
223
224
225
226
# File 'lib/familia/validation/test_helpers.rb', line 216

def teardown_validation_test
  # Stop recording if active
  CommandRecorder.stop_recording if CommandRecorder.recording?

  # Restore original states
  Familia.enable_database_logging = @original_logging_state if @original_logging_state
  Familia.enable_database_counter = @original_counter_state if @original_counter_state

  # Reset counters
  DatabaseCommandCounter.reset
end

#with_validation_test(&block) ⇒ Object

Wrapper for validation tests with automatic setup/teardown



229
230
231
232
233
234
235
236
# File 'lib/familia/validation/test_helpers.rb', line 229

def with_validation_test(&block)
  setup_validation_test
  begin
    block.call
  ensure
    teardown_validation_test
  end
end