Skip to content

Commit 5216dfd

Browse files
authored
Merge pull request #103 from geeksam/add-testing-hooks
Add testing hooks
2 parents bd02ce1 + 081ba38 commit 5216dfd

5 files changed

Lines changed: 77 additions & 4 deletions

File tree

README.md

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -206,6 +206,8 @@ class MyExperiment
206206
end
207207
```
208208

209+
Note that the `#clean` method will discard the previous cleaner block if you call it again. If for some reason you need to access the currently configured cleaner block, `Scientist::Experiment#cleaner` will return the block without further ado. _(This probably won't come up in normal usage, but comes in handy if you're writing, say, a custom experiment runner that provides default cleaners.)_
210+
209211
### Ignoring mismatches
210212

211213
During the early stages of an experiment, it's possible that some of your code will always generate a mismatch for reasons you know and understand but haven't yet fixed. Instead of these known cases always showing up as mismatches in your metrics or analysis, you can tell an experiment whether or not to ignore a mismatch using the `ignore` method. You may include more than one block if needed:
@@ -491,6 +493,22 @@ science "various-ways", run: "first-way" do |e|
491493
end
492494
```
493495

496+
#### Providing fake timing data
497+
498+
If you're writing tests that depend on specific timing values, you can provide canned durations using the `fabricate_durations_for_testing_purposes` method, and Scientist will report these in `Scientist::Observation#duration` instead of the actual execution times.
499+
500+
```ruby
501+
science "absolutely-nothing-suspicious-happening-here" do |e|
502+
e.use { ... } # "control"
503+
e.try { ... } # "candidate"
504+
e.fabricate_durations_for_testing_purposes( "control" => 1.0, "candidate" => 0.5 )
505+
end
506+
```
507+
508+
`fabricate_durations_for_testing_purposes` takes a Hash of duration values, keyed by behavior names. (By default, Scientist uses `"control"` and `"candidate"`, but if you override these as shown in [Trying more than one thing](#trying-more-than-one-thing) or [No control, just candidates](#no-control-just-candidates), use matching names here.) If a name is not provided, the actual execution time will be reported instead.
509+
510+
_Like `Scientist::Experiment#cleaner`, this probably won't come up in normal usage. It's here to make it easier to test code that extends Scientist._
511+
494512
### Without including Scientist
495513

496514
If you need to use Scientist in a place where you aren't able to include the Scientist module, you can call `Scientist.run`:

doc/changelog.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,10 @@
11
# Changes
22

3+
## v1.2.1 (UNRELEASED)
4+
5+
- New: Add an accessor for the configured clean block
6+
- New: Add a hook to use fabricated durations instead of actual timing data.
7+
38
## v1.2.0 (5 July 2018)
49

510
- New: Use monotonic clock for duration calculations

lib/scientist/experiment.rb

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -96,6 +96,13 @@ def clean(&block)
9696
@_scientist_cleaner = block
9797
end
9898

99+
# Accessor for the clean block, if one is available.
100+
#
101+
# Returns the configured block, or nil.
102+
def cleaner
103+
@_scientist_cleaner
104+
end
105+
99106
# Internal: Clean a value with the configured clean block, or return the value
100107
# if no clean block is configured.
101108
#
@@ -213,7 +220,8 @@ def run(name = nil)
213220

214221
behaviors.keys.shuffle.each do |key|
215222
block = behaviors[key]
216-
observations << Scientist::Observation.new(key, self, &block)
223+
fabricated_duration = @_scientist_fabricated_durations && @_scientist_fabricated_durations[key]
224+
observations << Scientist::Observation.new(key, self, fabricated_duration: fabricated_duration, &block)
217225
end
218226

219227
control = observations.detect { |o| o.name == name }
@@ -290,4 +298,10 @@ def raise_on_mismatches?
290298
!!raise_on_mismatches
291299
end
292300
end
301+
302+
# Provide predefined durations to use instead of actual timing data.
303+
# This is here solely as a convenience for developers of libraries that extend Scientist.
304+
def fabricate_durations_for_testing_purposes(fabricated_durations = {})
305+
@_scientist_fabricated_durations = fabricated_durations
306+
end
293307
end

lib/scientist/observation.rb

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -23,19 +23,20 @@ class Scientist::Observation
2323
# The Float seconds elapsed.
2424
attr_reader :duration
2525

26-
def initialize(name, experiment, &block)
26+
def initialize(name, experiment, fabricated_duration: nil, &block)
2727
@name = name
2828
@experiment = experiment
2929
@now = Time.now
3030

31-
starting = Process.clock_gettime(Process::CLOCK_MONOTONIC, :float_second)
31+
starting = Process.clock_gettime(Process::CLOCK_MONOTONIC, :float_second) unless fabricated_duration
3232
begin
3333
@value = block.call
3434
rescue *RESCUES => e
3535
@exception = e
3636
end
3737

38-
@duration = Process.clock_gettime(Process::CLOCK_MONOTONIC, :float_second) - starting
38+
@duration = fabricated_duration ||
39+
Process.clock_gettime(Process::CLOCK_MONOTONIC, :float_second) - starting
3940

4041
freeze
4142
end

test/scientist/experiment_test.rb

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -239,6 +239,13 @@ def @ex.enabled?
239239
assert_equal 10, @ex.clean_value(10)
240240
end
241241

242+
it "provides the clean block when asked for it, in case subclasses wish to override and provide defaults" do
243+
assert_nil @ex.cleaner
244+
cleaner = ->(value) { value.upcase }
245+
@ex.clean(&cleaner)
246+
assert_equal cleaner, @ex.cleaner
247+
end
248+
242249
it "calls the configured clean block with a value when configured" do
243250
@ex.clean do |value|
244251
value.upcase
@@ -559,4 +566,32 @@ def @ex.enabled?
559566
refute before, "before_run should not have run"
560567
end
561568
end
569+
570+
describe "testing hooks for extending code" do
571+
it "allows a user to provide fabricated durations for testing purposes" do
572+
@ex.use { true }
573+
@ex.try { true }
574+
@ex.fabricate_durations_for_testing_purposes( "control" => 0.5, "candidate" => 1.0 )
575+
576+
@ex.run
577+
578+
cont = @ex.published_result.control
579+
cand = @ex.published_result.candidates.first
580+
assert_in_delta 0.5, cont.duration, 0.01
581+
assert_in_delta 1.0, cand.duration, 0.01
582+
end
583+
584+
it "returns actual durations if fabricated ones are omitted for some blocks" do
585+
@ex.use { true }
586+
@ex.try { sleep 0.1; true }
587+
@ex.fabricate_durations_for_testing_purposes( "control" => 0.5 )
588+
589+
@ex.run
590+
591+
cont = @ex.published_result.control
592+
cand = @ex.published_result.candidates.first
593+
assert_in_delta 0.5, cont.duration, 0.01
594+
assert_in_delta 0.1, cand.duration, 0.01
595+
end
596+
end
562597
end

0 commit comments

Comments
 (0)