module XSpec
Hello and welcome to XSpec
!
XSpec
is an rspec-inspired testing library that is designed to be highly modular and easy to extend. Let's dive right in.
XSpec
data structures are very dumb. They:
-
Only contain iteration, property, and creation logic.
-
Do not store recursive references (“everything flows downhill”).
Common DSL
functions are provided as a module so that they can be used in both top-level and nested contexts. The method names are modeled after rspec, and should behave roughly the same.
They delegate to method in the [current context](xspec.html#section-5) named in a way that more accurately represents XSpec
implementation details.
Evaluators are usually composed together into a stack. The final stack has a single API method `call`, which is sent the unit of work to be executed and must return an array of `Failure` objects. It should not allow code-level exceptions to be raised, though should not block system exceptions (`SignalException`, `NoMemoryError`, etc).
Without a notifier, there is no way for XSpec
to interact with the outside world. A notifier handles progress updates as tests are executed, and summarizing the run when it finished.
Schedulers are responsible for collecting all units of work to be run and scheduling them.
Constants
- CodeException
An exception is mostly handled the same way as a failure.
- ExecutedUnitOfWork
The result of executing a unit of work, including timing information. This is passed to notifiers for display or other processing.
- Failure
A test failure will be reported as a `Failure`, which includes contextual information about the failure useful for reporting to the user.
- NestedUnitOfWork
Units of work can be nested inside contexts. This is the main object that other components of the system work with.
- UnitOfWork
A unit of work, usually created by the `it`
DSL
method, is a labeled, indivisible code block that expresses an assertion about a property of the system under test. They are run by a scheduler.
Public Class Methods
# File lib/xspec/defaults.rb, line 25 def add_defaults(options = {}) # A notifier makes it possible to observe the state of the system, be that # progress or details of failing tests. options[:notifier] ||= XSpec::Notifier::DEFAULT # A unit of work will run as an instance method on the context it is # defined in, but in addition an assertion context will be added as well. # This is a module that is included as the final step in constructing a # context. Allows for different matchers and expectation frameworks to be # used. options[:evaluator] ||= Evaluator::DEFAULT options[:short_id] ||= XSpec.method(:default_short_id) # An scheduler is responsible for scheduling units of work and handing them # off to the assertion context. Any logic regarding threads, remote # execution or the like belongs in a scheduler. options[:scheduler] ||= Scheduler::DEFAULT options end
# File lib/xspec/defaults.rb, line 13 def default_short_id(uow) length = 3 base = 32 digest = Digest::SHA1.hexdigest(uow.full_name).hex bottom = base ** (length-1) top = base ** length shifted = digest % (top - bottom) + bottom shifted.to_s(base) end
The DSL
is the core of XSpec
. It dynamically generates a module that can be mixed into which ever context you choose (using `extend`), be that the top-level namespace or a specific class.
This enables different options to be specified per DSL
, which is at the heart of XSpec's modularity. It is easy to change every component to your liking.
# File lib/xspec.rb, line 15 def dsl(config = {}) config = XSpec.add_defaults(config) Module.new do # Each DSL provides a standard set of methods provided by the [DSL # module](dsl.html). include DSL # In addition, each DSL has its own independent context, which is # described in detail in the # [`data_structures.rb`](data_structures.html). def __xspec_context evaluator = __xspec_config.fetch(:evaluator) @__xspec_context ||= XSpec::Context.root(evaluator) end # Some meta-magic is needed to enable the config from local scope above # to be available inside the module. define_method(:__xspec_config) { config } # `run!` is where the magic happens. Typically called at the end of a # file (or by `autorun!`), this method takes all the data that was # accumulated by the DSL methods above and runs it through the scheduler. # # It takes an optional block that can be used to override any options # set in the initial `XSpec.dsl` call. def run!(&overrides) overrides ||= -> x { x } config = overrides.(__xspec_config) scheduler = config.fetch(:scheduler) scheduler.run(__xspec_context, config) end # It is often convenient to trigger a run after all files have been # processed, which is what `autorun!` sets up for you. Requiring # `xspec/autorun` does this automatically for you. def autorun! at_exit do exit 1 unless run! end end end end
Public Instance Methods
In addition, each DSL
has its own independent context, which is described in detail in the [`data_structures.rb`](data_structures.html).
# File lib/xspec.rb, line 26 def __xspec_context evaluator = __xspec_config.fetch(:evaluator) @__xspec_context ||= XSpec::Context.root(evaluator) end
It is often convenient to trigger a run after all files have been processed, which is what `autorun!` sets up for you. Requiring `xspec/autorun` does this automatically for you.
# File lib/xspec.rb, line 52 def autorun! at_exit do exit 1 unless run! end end
`run!` is where the magic happens. Typically called at the end of a file (or by `autorun!`), this method takes all the data that was accumulated by the DSL
methods above and runs it through the scheduler.
It takes an optional block that can be used to override any options set in the initial `XSpec.dsl` call.
# File lib/xspec.rb, line 41 def run!(&overrides) overrides ||= -> x { x } config = overrides.(__xspec_config) scheduler = config.fetch(:scheduler) scheduler.run(__xspec_context, config) end
Private Instance Methods
# File lib/xspec/defaults.rb, line 25 def add_defaults(options = {}) # A notifier makes it possible to observe the state of the system, be that # progress or details of failing tests. options[:notifier] ||= XSpec::Notifier::DEFAULT # A unit of work will run as an instance method on the context it is # defined in, but in addition an assertion context will be added as well. # This is a module that is included as the final step in constructing a # context. Allows for different matchers and expectation frameworks to be # used. options[:evaluator] ||= Evaluator::DEFAULT options[:short_id] ||= XSpec.method(:default_short_id) # An scheduler is responsible for scheduling units of work and handing them # off to the assertion context. Any logic regarding threads, remote # execution or the like belongs in a scheduler. options[:scheduler] ||= Scheduler::DEFAULT options end
# File lib/xspec/defaults.rb, line 13 def default_short_id(uow) length = 3 base = 32 digest = Digest::SHA1.hexdigest(uow.full_name).hex bottom = base ** (length-1) top = base ** length shifted = digest % (top - bottom) + bottom shifted.to_s(base) end
The DSL
is the core of XSpec
. It dynamically generates a module that can be mixed into which ever context you choose (using `extend`), be that the top-level namespace or a specific class.
This enables different options to be specified per DSL
, which is at the heart of XSpec's modularity. It is easy to change every component to your liking.
# File lib/xspec.rb, line 15 def dsl(config = {}) config = XSpec.add_defaults(config) Module.new do # Each DSL provides a standard set of methods provided by the [DSL # module](dsl.html). include DSL # In addition, each DSL has its own independent context, which is # described in detail in the # [`data_structures.rb`](data_structures.html). def __xspec_context evaluator = __xspec_config.fetch(:evaluator) @__xspec_context ||= XSpec::Context.root(evaluator) end # Some meta-magic is needed to enable the config from local scope above # to be available inside the module. define_method(:__xspec_config) { config } # `run!` is where the magic happens. Typically called at the end of a # file (or by `autorun!`), this method takes all the data that was # accumulated by the DSL methods above and runs it through the scheduler. # # It takes an optional block that can be used to override any options # set in the initial `XSpec.dsl` call. def run!(&overrides) overrides ||= -> x { x } config = overrides.(__xspec_config) scheduler = config.fetch(:scheduler) scheduler.run(__xspec_context, config) end # It is often convenient to trigger a run after all files have been # processed, which is what `autorun!` sets up for you. Requiring # `xspec/autorun` does this automatically for you. def autorun! at_exit do exit 1 unless run! end end end end