Class: Snails::Harness::IntegrationTest

Inherits:
Object
  • Object
show all
Defined in:
runtime/snails/harness.rb,
runtime/snails/harness.rbs

Overview

Base class for a case — an app's test classes inherit it, e.g. class UpSmokeTest < Snails::Harness::IntegrationTest. Not API-specific: it is the shared surface (real HTTP + assertions) any integration case uses.

base_url defaults to APP_BASE_URL (the runner's live server). Hooks let an app extend the base: setup (per-case fixtures/auth) and default_host (the Host header its routes expect) are reopened on the class, while a case class overrides tables with the tables to empty before each case.

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(base_url = nil, host: nil) ⇒ IntegrationTest

Returns a new instance of IntegrationTest.



121
122
123
124
125
126
127
128
# File 'runtime/snails/harness.rb', line 121

def initialize(base_url = nil, host: nil)
  @base_url = base_url.nil? ? ENV["APP_BASE_URL"] : base_url
  @host = host.nil? ? default_host : host
  @response = nil
  reset_tables
  reset_cache
  setup
end

Instance Attribute Details

#response ⇒ Object (readonly)

Returns the value of attribute response.



130
131
132
# File 'runtime/snails/harness.rb', line 130

def response
  @response
end

Class Method Details

.test(name, &block) ⇒ void

case declaration inside a subclass, written as test "description" do ... end. The case runs immediately in a fresh instance — spinel AOT cannot store a block's self — and name is the printed name. In a bundle process the require loads every file, but only the selected file's cases run (Snails::Harness.file_selected?), so each run prints only its own file's cases.

The selection gate wraps the body in an if rather than returning early: an early return in a method that stores &block corrupts a later stored block's exit under spinel AOT (instance_exec then raises TypeError after the body has finished).

Parameters:

  • name (String) —

    the case description printed with ok, FAIL, or skip



147
148
149
150
151
152
153
154
155
156
157
158
159
160
# File 'runtime/snails/harness.rb', line 147

def self.test(name, &block)
  if Snails::Harness.file_selected?
    instance = new
    begin
      instance.instance_exec(&block)
      puts "ok #{name}"
    rescue SkipSignal => e
      puts "skip #{name}: #{e.message}"
    rescue StandardError => e
      puts "FAIL #{name}: #{e.class}: #{e.message}"
      exit 1
    end
  end
end

Instance Method Details

#assert(value, message = nil) ⇒ void

pass when the value is truthy; the optional message rides on the failure line.

Parameters:

  • value (Object) —

    the truthy value under test

  • message (String, nil) (defaults to: nil) —

    extra context printed on failure

Raises:

  • (RuntimeError) —

    the value is false or nil



320
# File 'runtime/snails/harness.rb', line 320

def assert: (untyped value, ?untyped message) -> void

#assert_emails(count) ⇒ void

assert the block caused exactly count deliveries. The server is a separate process, so its ActionMailer::Base.deliveries is unreadable from here; the shared observation point is the gate's mail log (tmp/mail-capture.jsonl under the spin root, MAIL_LOG_PATH from this target's cwd — the file the server writes in test mode, where the stamped config/environment.rb registers the capture). The engine empties the file before boot, so the count is the block's before/after line delta, never an absolute. Without a live gate (a bare spin test <file>) the case skips — a mail assertion that cannot run is visibly skipped, not silently green.

Examples:

assert_emails 1 do
  post("/api/account/password", params: { email: "sue@example.com" }, as: :json)
end

Parameters:

  • count (Integer) —

    the deliveries the block must cause



420
421
422
423
424
425
426
427
428
429
430
431
# File 'runtime/snails/harness.rb', line 420

def assert_emails(count)
  if ENV["APP_BASE_URL"].to_s.empty?
    raise SkipSignal, "assert_emails (no live gate — run bin/test)"
  end

  before = mail_log_entries.length
  yield
  actual = mail_log_entries.length - before
  return if actual == count

  raise "assert_emails failed: expected #{count} mail(s), got #{actual}"
end

#assert_equal(expected, actual) ⇒ void

pass when the two values match; the failure names both sides.

Raises:

  • (RuntimeError) —

    the values differ



251
252
253
254
255
# File 'runtime/snails/harness.rb', line 251

def assert_equal(expected, actual)
  return if expected == actual

  raise "assert_equal: expected #{expected.inspect}, got #{actual.inspect}"
end

#assert_includes(collection, object) ⇒ void

pass when the collection holds the object; whatever include? answers for that collection covers it (Array membership, String substring).

Examples:

assert_includes response.body, "created"

Parameters:

  • collection (Array, String) —

    the collection searched

  • object (Object) —

    the value that must be present

Raises:

  • (RuntimeError) —

    the collection does not hold the object



307
308
309
310
311
# File 'runtime/snails/harness.rb', line 307

def assert_includes(collection, object)
  return if collection.include?(object)

  raise "assert_includes: #{object.inspect} not included in #{collection.inspect}"
end

#assert_match(pattern, actual) ⇒ void

pass when the pattern matches the actual value. A String pattern is a literal substring (minitest parity: never a regexp source), so it needs no escaping pass.

Examples:

assert_match(/\d{4}/, response.body)

Parameters:

  • pattern (Regexp, String) —

    the pattern to match against

  • actual (String) —

    the value being matched

Raises:

  • (RuntimeError) —

    the pattern does not match



287
288
289
290
291
292
293
294
295
296
# File 'runtime/snails/harness.rb', line 287

def assert_match(pattern, actual)
  matched = if pattern.is_a?(String)
              actual.to_s.include?(pattern)
            else
              pattern.match?(actual.to_s)
            end
  return if matched

  raise "assert_match: expected #{actual.inspect} to match #{pattern.inspect}"
end

#assert_nil(value, message = nil) ⇒ void

pass when the value is nil; the failure names what was there instead.

Parameters:

  • value (Object) —

    the value under test

  • message (String, nil) (defaults to: nil) —

    extra context printed on failure

Raises:

  • (RuntimeError) —

    the value is not nil



332
# File 'runtime/snails/harness.rb', line 332

def assert_nil: (untyped value, ?untyped message) -> void

#assert_not(value, message = nil) ⇒ void

pass when the value is false or nil; the failure names what was truthy.

Parameters:

  • value (Object) —

    the value under test

  • message (String, nil) (defaults to: nil) —

    extra context printed on failure

Raises:

  • (RuntimeError) —

    the value is truthy



356
# File 'runtime/snails/harness.rb', line 356

def assert_not: (untyped value, ?untyped message) -> void

#assert_not_equal(expected, actual, message = nil) ⇒ void

pass when the two values differ; the failure names both sides, like assert_equal.

Parameters:

  • expected (Object) —

    the value the actual must differ from

  • actual (Object) —

    the value under test

  • message (String, nil) (defaults to: nil) —

    extra context printed on failure

Raises:

  • (RuntimeError) —

    the values match



370
# File 'runtime/snails/harness.rb', line 370

def assert_not_equal: (untyped expected, untyped actual, ?untyped message) -> void

#assert_not_nil(value, message = nil) ⇒ void

pass when the value is not nil; the failure names the nil it got.

Parameters:

  • value (Object) —

    the value under test

  • message (String, nil) (defaults to: nil) —

    extra context printed on failure

Raises:

  • (RuntimeError) —

    the value is nil



344
# File 'runtime/snails/harness.rb', line 344

def assert_not_nil: (untyped value, ?untyped message) -> void

#assert_operator(left, operator, right, message = nil) ⇒ void

pass when left <operator> right holds; the failure prints the comparison that did not.

Parameters:

  • left (Object) —

    the left-hand value

  • operator (Symbol) —

    one of :>=, :<=, :>, :<

  • right (Object) —

    the right-hand value

  • message (String, nil) (defaults to: nil) —

    extra context printed on failure

Raises:

  • (ArgumentError) —

    the operator is not one of the four

  • (RuntimeError) —

    the comparison does not hold



386
# File 'runtime/snails/harness.rb', line 386

def assert_operator: (untyped left, untyped operator, untyped right, ?untyped message) -> void

#assert_raises(error_class) ⇒ void

pass when the block raises the named exception (a subclass counts); nothing raised or a different exception fails, and the failure names what came instead.

Examples:

assert_raises(Sqlx::RecordNotFound) { User.find_by!(email: "sue@example.com") }

Parameters:

  • error_class (Class) —

    the exception the block must raise

Raises:

  • (RuntimeError) —

    nothing was raised, or another exception was



266
267
268
269
270
271
272
273
274
275
# File 'runtime/snails/harness.rb', line 266

def assert_raises(error_class)
  begin
    yield
  rescue error_class
    return
  rescue StandardError => e
    raise "assert_raises: expected #{error_class}, got #{e.class}: #{e.message}"
  end
  raise "assert_raises: #{error_class} not raised"
end

#assert_response(type) ⇒ void

pass when the status matches. Four category symbols take a range or exact code: :success (2xx), :redirect (3xx), :error (5xx), :missing (404 exactly). Any other symbol resolves through the shared status table (Snails::Utils::SYMBOL_TO_STATUS_CODE), so every symbol the controller's status: accepts works here too; an Integer is compared as-is (assert_response 415).

Examples:

assert_response :bad_request
assert_response 415

Parameters:

  • type (Symbol, Integer) —

    a category symbol, any status symbol in the shared table, or the exact status code

Raises:

  • (RuntimeError) —

    the status does not match, or type names no category and is not in the shared status table



222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
# File 'runtime/snails/harness.rb', line 222

def assert_response(type)
  actual = @response.status
  ok = if type == :success
         actual >= 200 && actual <= 299
       elsif type == :redirect
         actual >= 300 && actual <= 399
       elsif type == :error
         actual >= 500 && actual <= 599
       elsif type == :missing
         actual == 404
       elsif type.is_a?(Integer)
         actual == type
       elsif type.is_a?(Symbol)
         code = Snails::Utils::SYMBOL_TO_STATUS_CODE[type]
         raise "unknown response type: #{type}" if code.nil?

         actual == code
       else
         raise "unknown response type: #{type}"
       end
  return if ok

  raise "assert_response #{type}: got #{actual} — #{@response.body.to_s[0, 200]}"
end

#default_host ⇒ String?

the Host header sent with every request when the instance is built without one. No-op (nil) here; an app reopens it with the host its routes expect.

Returns:

  • (String, nil) —

    the host, nil unless the app reopens



187
188
189
# File 'runtime/snails/harness.rb', line 187

def default_host
  nil
end

#delete(path, params: nil, headers: nil, as: nil) ⇒ Object



203
204
205
# File 'runtime/snails/harness.rb', line 203

def delete(path, params: nil, headers: nil, as: nil)
  @response = Snails::Harness.request(:delete, path, @base_url, params, with_host(headers), as)
end

#emails ⇒ Array[Hash]

the run's recorded deliveries so far, in send order: one hash per delivery with "to" (array), "from", and "subject". The gate engine empties the log before boot; a case reads emails.last after an assert_emails block rather than asserting absolutes.

Returns:

  • (Array[Hash]) —

    parsed log rows; empty without a gate



439
440
441
# File 'runtime/snails/harness.rb', line 439

def emails
  mail_log_entries
end

#get(path, params: nil, headers: nil) ⇒ Object



191
192
193
# File 'runtime/snails/harness.rb', line 191

def get(path, params: nil, headers: nil)
  @response = Snails::Harness.request(:get, path, @base_url, params, with_host(headers), nil)
end

#post(path, params: nil, headers: nil, as: nil) ⇒ Object



195
196
197
# File 'runtime/snails/harness.rb', line 195

def post(path, params: nil, headers: nil, as: nil)
  @response = Snails::Harness.request(:post, path, @base_url, params, with_host(headers), as)
end

#put(path, params: nil, headers: nil, as: nil) ⇒ Object



199
200
201
# File 'runtime/snails/harness.rb', line 199

def put(path, params: nil, headers: nil, as: nil)
  @response = Snails::Harness.request(:put, path, @base_url, params, with_host(headers), as)
end

#setup ⇒ void

per-case hook: runs once per fresh instance, before the case body and after per-case isolation (tables). No-op here; an app reopens it to load fixtures or seed state.



167
168
# File 'runtime/snails/harness.rb', line 167

def setup
end

#skip(message = "skipped") ⇒ void

end the case as skipped rather than failed; raises SkipSignal, the signal the test macro catches.

Parameters:

  • message (String) (defaults to: "skipped") —

    the reason printed after the case name; "skipped" by default

Raises:

  • (SkipSignal) —

    always — the case body ends here



450
451
452
# File 'runtime/snails/harness.rb', line 450

def skip(message = "skipped")
  raise SkipSignal, message
end

#tables ⇒ Array[String]

the table names emptied before each case, so a case never sees another case's writes. Default: none; a case class with DB state overrides this.

Examples:

def tables
  ["widgets"]
end

Returns:

  • (Array[String]) —

    the table names to empty, empty by default



178
179
180
# File 'runtime/snails/harness.rb', line 178

def tables
  []
end