Module: Clickwrap::Testing
- Defined in:
- lib/clickwrap/testing.rb
Overview
Fault injection, for this gem's suite and for yours.
=========================================================================== The central promise of this gem is that required evidence and the protected database action commit together or not at all. A promise like that is worth exactly as much as your ability to prove it in a test, and you cannot prove it by reading the code — you prove it by making the evidence write fail on purpose and watching the account, the payout, or the withdrawal fail with it.
Clickwrap::Testing.fail_next_event_write do
assert_raises(Clickwrap::EventWriteFailed) { perform_signup }
end
assert_not User.exists?(email: "person@example.com")
assert_no_clickwrap_event :signup
The failure is injected INSIDE the Clickwrap::Event create, which means
inside the capture's transaction, which means the host's protected action
rolls back with it. Raising before the transaction opened would prove
nothing at all: of course the domain action does not happen if the capture
never started.
Everything here installs on entry and removes on exit, in an ensure, so a
failing assertion inside the block cannot leave a sabotage hook attached to
Clickwrap::Event for the rest of the suite. Nothing here is left resident
in a production process: the callbacks exist only while a block is running,
and this file is only loaded if something references Clickwrap::Testing.
Defined Under Namespace
Modules: FrozenClock Classes: DomainWriteFailed
Constant Summary collapse
- EVENT_WRITE_KEY =
:clickwrap_testing_fail_next_event_write- DOMAIN_WRITE_KEY =
:clickwrap_testing_fail_next_domain_write- FROZEN_TIME_KEY =
:clickwrap_testing_frozen_time- FAIL_EVENT_WRITE =
The injected callbacks, held as constants so
skip_callbackcan find the same object it was given. An anonymous lambda created per call would install fine and never come off. lambda do |_event| next unless Testing.consume_flag!(EVENT_WRITE_KEY) raise EventWriteFailed, "Clickwrap::Testing.fail_next_event_write made this evidence write fail on purpose. " \ "Whatever your protected action did in this transaction must roll back with it — " \ "that is the property this helper exists to let you assert." end
- FAIL_DOMAIN_WRITE =
lambda do |record| next if record.is_a?(Clickwrap::ApplicationRecord) next unless Testing.consume_flag!(DOMAIN_WRITE_KEY) raise DomainWriteFailed, "Clickwrap::Testing.fail_next_domain_write made this domain write fail on purpose. " \ "The Clickwrap evidence in the same transaction must roll back with it, so that a " \ "failed action never leaves behind a receipt saying it succeeded." end
Class Method Summary collapse
-
.consume_flag!(key) ⇒ Object
Reads a one-shot flag and clears it in the same breath, so the sabotage applies to exactly one write.
-
.fail_next_domain_write ⇒ Object
The mirror image: makes the next non-Clickwrap
ActiveRecordsave raise, so you can prove the other direction — that a domain action blowing up takes its evidence down with it, and never leaves a receipt describing something that did not happen. -
.fail_next_event_write ⇒ Object
Makes the NEXT
Clickwrap::Eventinsert raiseClickwrap::EventWriteFailed, from inside the create, and therefore from inside whatever transaction the capture is running in. -
.freeze_time_at(moment) ⇒ Object
Freezes the server clock Clickwrap records and evaluates expiry against, so a test about a declaration that expired last Tuesday does not have to sleep until next Tuesday.
- .frozen_time ⇒ Object
-
.reset! ⇒ Object
Clears every flag and detaches every injected callback.
Class Method Details
.consume_flag!(key) ⇒ Object
Reads a one-shot flag and clears it in the same breath, so the sabotage applies to exactly one write.
154 155 156 157 158 159 |
# File 'lib/clickwrap/testing.rb', line 154 def consume_flag!(key) return false unless Thread.current[key] Thread.current[key] = nil true end |
.fail_next_domain_write ⇒ Object
The mirror image: makes the next non-Clickwrap ActiveRecord save
raise, so you can prove the other direction — that a domain action
blowing up takes its evidence down with it, and never leaves a receipt
describing something that did not happen.
98 99 100 101 102 103 104 105 106 |
# File 'lib/clickwrap/testing.rb', line 98 def fail_next_domain_write install_domain_callback! Thread.current[DOMAIN_WRITE_KEY] = true yield ensure Thread.current[DOMAIN_WRITE_KEY] = nil remove_domain_callback! end |
.fail_next_event_write ⇒ Object
Makes the NEXT Clickwrap::Event insert raise
Clickwrap::EventWriteFailed, from inside the create, and therefore
from inside whatever transaction the capture is running in.
Only the next one: a capture that legitimately retries, or a test that goes on to record a control event afterwards, is not sabotaged twice.
84 85 86 87 88 89 90 91 92 |
# File 'lib/clickwrap/testing.rb', line 84 def fail_next_event_write install_event_callback! Thread.current[EVENT_WRITE_KEY] = true yield ensure Thread.current[EVENT_WRITE_KEY] = nil remove_event_callback! end |
.freeze_time_at(moment) ⇒ Object
Freezes the server clock Clickwrap records and evaluates expiry against, so a test about a declaration that expired last Tuesday does not have to sleep until next Tuesday.
It moves Clickwrap.now only. Time.now is left alone on purpose:
this gem's evidentiary time is the value it writes into
recorded_at_by_server, and a helper that quietly moved the whole
process clock would make it much harder to tell which of the two a test
actually depends on.
117 118 119 120 121 122 123 124 125 126 127 |
# File 'lib/clickwrap/testing.rb', line 117 def freeze_time_at(moment) moment = Time.parse(moment.to_s) if moment.is_a?(String) previous = Thread.current[FROZEN_TIME_KEY] install_clock! Thread.current[FROZEN_TIME_KEY] = moment.utc block_given? ? yield(moment.utc) : moment.utc ensure Thread.current[FROZEN_TIME_KEY] = previous end |
.frozen_time ⇒ Object
129 |
# File 'lib/clickwrap/testing.rb', line 129 def frozen_time = Thread.current[FROZEN_TIME_KEY] |
.reset! ⇒ Object
Clears every flag and detaches every injected callback. Safe to call
when nothing was ever installed, which is the point: a suite calls it
in setup and in teardown without having to know whether the test
that just ran used any of this.
135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 |
# File 'lib/clickwrap/testing.rb', line 135 def reset! Thread.current[EVENT_WRITE_KEY] = nil Thread.current[DOMAIN_WRITE_KEY] = nil Thread.current[FROZEN_TIME_KEY] = nil # Unconditional, and `raise: false` throughout: `reset!` is called from # `setup` and `teardown` in suites that mostly never touch fault # injection, and it must be a quiet no-op there rather than an # ArgumentError about a callback nobody installed. @event_depth = 0 @domain_depth = 0 Event.skip_callback(:create, :before, FAIL_EVENT_WRITE, raise: false) ::ActiveRecord::Base.skip_callback(:save, :before, FAIL_DOMAIN_WRITE, raise: false) self end |