RSpec 2.0 and before/after hooks
June 7, 2011As of RSpec 2, the configuration interface for RSpec changed dramatically. What used to look like:
Spec::Runner.configure do |config| config.prepend_before(:each, :type => :controller) do ... end end
Now looks more like:
RSpec::configure do |config| config.before(:each, :line => 153) do end end
One significant and interesting change is the way that before hooks are processed. Specifically, the #before, #after, and #around methods are now part of the Hooks module, which is included in both ExampleGroup and in Configuration, so you call configure.before in exactly the same way as you do within a describe block. Normally, you pass :each or :all, which sets the scope under which the hook will be called, but Hooks inspects the arguments for filtering metadata regardless of where you call it - I don't know that you'd want filter within an ExampleGroup, but you could...
Unfortunately, as cool as the metadata filtering capabilities are they aren't, as far as I can tell, very well documented. The process of extracting the metadata lives in it's own :nodoc: limbo, and the attachment of metadata to a particular example is scattered throughout the RSpec code. This, then, is an attempt to pick that apart.
Extracting Filters
When you call Hooks#before, for example (#after and #around work fundamentally the same way), the args are examined and two things are extracted:
A scope, which is the :each, :all, or :suite specification.
A metadata filter hash. Normally, you call #before(:each, {:hash => [:of, :metadata]}), but you can instead do something like before(:all, :symbol) which will result in a metadata filter like {:symbol => true}
Again, probably if you need to add metadata inside of a describe block, you are Doing Something Wrong, but maybe there's a good reason. The extreme (excessive?) flexibility of RSpec metadata and filtering does open up a lot of interesting possibilities.
Filter Matching
The metadata filter is used to decide if the hook should be run for a particular example block that it might apply to. As such, it's a remarkably powerful filtering system, although there's a lot of assumptions about it's format that you need to bear in mind.
The actual mechanics of the metadata filtering happen in RSpec::Core::Metadata#apply? and #apply_condition - there's a long chain of delegation and extra-meta-programming that leads there.
The upshot is that your metadata filter will be compared to the metadata on the example key/value pair by pair, like this:
- A regular expression in the filter will match against the appropriate value for the example.
- If you pass :line_number => 17, Rspec will check to see if the example includes line 17, much like running rspec filename_spec.rb:17
- Any other Fixnum will be compared with == to the value in the metadata
- Anything else gets compared with == to the value in the metadata, after both values have been converted to a string.
- A proc like
{|value| ... }will get the value of the key, and can return true for a match.
Filters can nest Hashes, which will be compared to nested Hashes in the metadata. In other words, if you want to be able to match for metadata like
{..., :example_group => {..., :full_description => "A very long winded example of the group", ...}, ...}
You can do something like:
before(:each, {:example_group =>{:full_description => /long winded/}})
it "should do something useful, someday", :pending => "Not this day, though"
Which is much faster than using the pending method call inside the block, and can be applied to a describe block to make the whole thing pending - especially handy when you have a before block inside that is causing problems.
In the same token, the example given in RSpec 2 documentation and announcement posts has been doing something like:
it "should not be taking this looooong", :slow => true
Since metadata can also be used to filter examples, you could use this to pull out the examples that take forever from your all-the-time specs, and run them only before a push, for instance.
What Metadata Does RSpec Give Us?
Probably the best way to figure that out is this very pragmatic approach.
A Useful Trick
Very useful for experimenting with metadata is that the proc form of the metadata has a special case: if the proc takes two arguments, the whole metadata hash will get passed into the proc, so you can inspect it at leisure. The snippet looks like:
require 'pp' before(:each, :bogus => proc{|val, all| pp all}) {}
{ :execution_result=>{:started_at=>Tue Jun 07 14:13:46 -0700 2011}, :type=>:controller, :full_description=>"UserSessionsController should be authorized", :description=>"should be authorized", :example_group=> { :full_description=>"UserSessionsController", :file_path=> "spec/controllers/user_sessions_controller_spec.rb", :describes=>UserSessionsController, :description=>"UserSessionsController", :block=> # , :line_number=>3, :caller=> [ ... the whole backtrace of the group ... ] }, :caller=> [ ... the backtrace of the example ...] }
One of the cool-but-problematic things about metadata in RSpec is that it get's added and updated all over the codebase, and constantly over the lifecycle of an example run and extensions (like Rspec-Rails) add their own fields and values, so it's very hard to have formal documentation for what you can match. Also, somewhat troubling, is that none of these fields are an explicit part of the RSpec API, and so might change with very little notice. It seems like the best way to manage working with the metadata is with the above pragmatic approach.
[...] 其中关键的就是“:example_group => { :file_path => /bspec/lib/warehouse/models/ }” , 这句话指定这个测试案例运行的条件,那就是这个spec文件的路径必须跟”/bspec/lib/warehouse/models/” 匹配上,才会开启和关闭事务,否则直接运行. 这个其实还满好用的,那现在我们只是用了:file_path这个过滤条件, 哪有没有更加强大的支持了? 有. 参考: RSpec 2.0 and before/after hooks [...]
Add A Comment