Module: Hypothesis
- Extended by:
- Hypothesis
- Included in:
- Hypothesis, Possibilities, Possible
- Defined in:
- lib/hypothesis.rb,
lib/hypothesis/world.rb,
lib/hypothesis/engine.rb,
lib/hypothesis/errors.rb,
lib/hypothesis/possible.rb,
lib/hypothesis/testcase.rb,
lib/hypothesis/junkdrawer.rb
Overview
This is the main module for using Hypothesis. It is expected that you will include this in your tests, but its methods are also available on the module itself.
The main entry point for using this is the #hypothesis method. All of the other methods make sense only inside blocks passed to it.
Defined Under Namespace
Modules: Possibilities, World Classes: Engine, HypothesisError, MultipleExceptionError, MultipleExceptionErrorParent, Possible, Unsatisfiable, UsageError
Constant Summary collapse
- DEFAULT_DATABASE_PATH =
File.join(Dir.pwd, '.hypothesis', 'examples')
- @@setup_called =
rubocop:disable ClassVars
false
Class Method Summary collapse
- .included ⇒ Object
-
.setup_called ⇒ Object
rubocop:enable RuleByName.
Instance Method Summary collapse
-
#any(possible, name: nil, &block) ⇒ Object
Supplies a value to be used in your hypothesis.
-
#assume(condition) ⇒ Object
Specify an assumption of your test case.
-
#hypothesis(max_valid_test_cases: 200, phases: Phase.all, database: nil, &block) ⇒ Object
Run a test using Hypothesis.
Class Method Details
.included ⇒ Object
52 53 54 55 56 57 58 59 60 |
# File 'lib/hypothesis.rb', line 52 def self.included(*) if setup_called == false Rutie.new(:hypothesis_ruby_core).init( 'Init_rutie_hypothesis_core', __dir__ ) end @@setup_called = true end |
.setup_called ⇒ Object
rubocop:enable RuleByName
48 49 50 |
# File 'lib/hypothesis.rb', line 48 def self.setup_called @@setup_called == true end |
Instance Method Details
#any(possible, name: nil, &block) ⇒ Object
It is invalid to call this method outside of a hypothesis block.
Supplies a value to be used in your hypothesis.
257 258 259 260 261 262 263 264 265 |
# File 'lib/hypothesis.rb', line 257 def any(possible, name: nil, &block) if World.current_engine.nil? raise UsageError, 'Cannot call any outside of a hypothesis block' end World.current_engine.current_source.any( possible, name: name, &block ) end |
#assume(condition) ⇒ Object
It is invalid to call this method outside of a hypothesis block.
Try to use this only with “easy” conditions. If the condition is too hard to satisfy this can make your testing much worse, because Hypothesis will have to retry the test many times and will struggle to find “interesting” test cases. For example ‘assume(x != y)` is typically fine, and `assume(x == y)` is rarely a good idea.
Specify an assumption of your test case. Only test cases which satisfy their assumptions will treated as valid, and all others will be discarded.
280 281 282 283 284 285 |
# File 'lib/hypothesis.rb', line 280 def assume(condition) if World.current_engine.nil? raise UsageError, 'Cannot call assume outside of a hypothesis block' end World.current_engine.current_source.assume(condition) end |
#hypothesis(max_valid_test_cases: 200, phases: Phase.all, database: nil, &block) ⇒ Object
Run a test using Hypothesis.
For example:
“‘ruby hypothesis do
x = any integer
y = any integer(min: x)
expect(y).to be >= x
end “‘
The arguments to ‘any` are `Possible` instances which specify the range of value values for it to return.
Typically you would include this inside some test in your normal testing framework - e.g. in an rspec it block or a minitest test method.
This will run the block many times with integer values for x and y, and each time it will pass because we specified that y had a minimum value of x. If we changed it to ‘expect(y).to be > x` we would see output like the following:
“‘ Failure/Error: expect(y).to be > x
Given #1: 0 Given #2: 0 expected: > 0
got: 0
“‘
In more detail:
hypothesis calls its provided block many times. Each invocation of the block is a *test case*. A test case has three important features:
-
givens are the result of a call to self.any, and are the values that make up the test case. These might be values such as strings, integers, etc. or they might be values specific to your application such as a User object.
-
assumptions, where you call ‘self.assume(some_condition)`. If an assumption fails (`some_condition` is false), then the test case is considered invalid, and is discarded.
-
assertions are anything that will raise an error if the test case should be considered a failure. These could be e.g. RSpec expectations or minitest matchers, but anything that throws an exception will be treated as a failed assertion.
A test case which satisfies all of its assumptions and assertions is valid. A test-case which satisfies all of its assumptions but fails one of its assertions is failing.
A call to hypothesis does the following:
-
It first tries to reuse failing test cases for previous runs.
-
If there were no previous failing test cases then it tries to generate new failing test cases.
-
If either of the first two phases found failing test cases then it will shrink those failing test cases.
-
Finally, it will display the shrunk failing test case by the error from its failing assertion, modified to show the givens of the test case.
Reuse uses an internal representation of the test case, so examples from previous runs will obey all of the usual invariants of generation. However, this means that if you change your test then reuse may not work. Test cases that have become invalid or passing will be cleaned up automatically.
Generation consists of randomly trying test cases until one of three things has happened:
-
It has found a failing test case. At this point it will start shrinking the test case (see below).
-
It has found enough valid test cases. At this point it will silently stop.
-
It has found so many invalid test cases that it seems unlikely that it will find any more valid ones in a reasonable amount of time. At this point it will either silently stop or raise ‘Hypothesis::Unsatisfiable` depending on how many valid examples it found.
Shrinking is when Hypothesis takes a failing test case and tries to make it easier to understand. It does this by replacing the givens in the test case with smaller and simpler values. These givens will still come from the possible values, and will obey all the usual constraints. In general, shrinking is automatic and you shouldn’t need to care about the details of it. If the test case you’re shown at the end is messy or needlessly large, please file a bug explaining the problem!
226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 |
# File 'lib/hypothesis.rb', line 226 def hypothesis( max_valid_test_cases: 200, phases: Phase.all, database: nil, &block ) unless World.current_engine.nil? raise UsageError, 'Cannot nest hypothesis calls' end begin World.current_engine = Engine.new( hypothesis_stable_identifier, max_examples: max_valid_test_cases, phases: phases, database: database ) World.current_engine.run(&block) ensure World.current_engine = nil end end |