Testing flash.now with RSpec
flash.now has always been a pain to test. The the traditional rails approach is to use assert_select and find it in your views. This clearly doesn’t work if you want to test your controller in isolation.
Other folks have found work arounds to the problem, including mocking out the flash or monkey patching it.
These solutions feel a bit like using a sledgehammer to me. If you’re going to monkey patch/mock something, you want it to be as discreet as possible so to minimize the chance of the implementation changing underneath you and also to reduce the affect on other areas of your application. Also, why duplicate perfectly good code that is provided elsewhere?
The real problem with testing flash.now is that it gets cleaned up (via #sweep) at the end of the action before you get to test anything. So let’s solve that problem and that problem only: disable sweeping of flash.now:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
# spec/spec_helper.rb module DisableFlashSweeping def sweep end end # A spec describe BogusController, "handling GET to #index" do it "sets flash.now[:message]" do @controller.instance_eval { flash.extend(DisableFlashSweeping) } get :index flash.now[:message].should_not be_nil end end |
instance_eval is used to access the flash, since it’s a protected method, and we extend with the minimum possible code to do what we want – blanking out the sweep method. This should not cause problems because sweeping is only relevant across multiple requests, which we shouldn’t be doing in our controller specs.
Unobtrusive live comment preview with jQuery
Live preview is shiny. First get your self a URL that renders a comment. In rails maybe something like the following.
1 2 3 4 5 6 7 8 9 |
def new @comment = Comment.build_for_preview(params[:comment]) respond_to do |format| format.js do render :partial => 'comment.html.erb' end end end |
Now you should have a form or div with an ID something like “new_comment”. Just drop in the following JS (you may need to customize the submit_url).
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 |
$(function() { // onload
var comment_form = $('#new_comment')
var input_elements = comment_form.find(':text, textarea')
var submit_url = '/comments/new'
var fetch_comment_preview = function() {
jQuery.ajax({
data: comment_form.serialize(),
url: submit_url,
timeout: 2000,
error: function() {
console.log("Failed to submit");
},
success: function(r) {
if ($('#comment-preview').length == 0) {
comment_form.after('<h2>Your comment will look like this:</h2><div id="comment-preview"></div>')
}
$('#comment-preview').html(r)
}
})
}
input_elements.keyup(function () {
fetch_comment_preview.only_every(1000);
})
if (input_elements.any(function() { return $(this).val().length > 0 }))
fetch_comment_preview();
})
|
The only_every function is they key to this piece – it ensures that an AJAX request will be sent at most only once a second so you don’t overload your server or your client’s connection.
Obviously you’ll need jQuery, less obviously you’ll also need these support functions
1 2 3 4 5 6 7 8 9 10 11 12 13 |
// Based on http://www.germanforblack.com/javascript-sleeping-keypress-delays-and-bashing-bad-articles
Function.prototype.only_every = function (millisecond_delay) {
if (!window.only_every_func)
{
var function_object = this;
window.only_every_func = setTimeout(function() { function_object(); window.only_every_func = null}, millisecond_delay);
}
};
// jQuery extensions
jQuery.prototype.any = function(callback) {
return (this.filter(callback).length > 0)
}
|
Viola, now you’re shimmering in awesomeness. Demo up soon, but it’s similar to what you see on this blog (though this blog is done with inline prototype).
AtomFeedHelper produces invalid feeds
Summary: atom_feed is broken until changeset 8529
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
# http://api.rubyonrails.org/classes/ActionView/Helpers/AtomFeedHelper.html#M000931 atom_feed do |feed| feed.title("My great blog!") feed.updated((@posts.first.created_at)) for post in @posts feed.entry(post) do |entry| entry.title(post.title) entry.content(post.body, :type => 'html') entry.author do |author| author.name("DHH") end end end end |
Produces the following feed (rails 2.0.2)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
<?xml version="1.0" encoding="UTF-8"?> <feed xml:lang="en-US" xmlns="http://www.w3.org/2005/Atom"> <id>tag:localhost:posts</id> <link type="text/html" rel="alternate" href="http://localhost:3000"/> <title>My great blog!</title> <updated>2007-12-23T04:23:07+11:00</updated> <entry> <id>tag:localhost:3000:Post1</id> <published>2007-12-23T04:23:07+11:00</published> <updated>2007-12-30T15:29:55+11:00</updated> <link type="text/html" rel="alternate" href="http://localhost:3000/posts/1"/> <title>First post</title> <content type="html">Check out the first post</content> <author> <name>DHH</name> </author> </entry> </feed> |
Let’s run that through the feed validator
1 2 3 |
line 3, column 25: id is not a valid TAG line 2, column 0: Missing atom:link with rel="self" line 8, column 32: id is not a valid TAG |
Oh dear. Not a happy result. Let’s fix it.
Problem the first is the feed ID tag. It doesn’t include a date, as per the Tag URI specification. This is a little bit tricky – you can’t just add Time.now.year as a default because that will change every year, and we need IDs to stay the same. We will provide an option to the user to specify the schema date, and produce a warning if they do not (as much as I’d like to just break it, the pragmatic side of me keeps backwards compatibility in).
The entry tag has the same problem, but you’ll also note it concatenates the class and the ID with no separator to create the ID. While it’s an edge case, this will break if you have a class name ending in a number, so we need to add in a separator. I vote for a slash. Also, the port in the tag URI is inconsistent with the feed URI (no port), so remove it.
For further reading, I recommend How to make a good ID in Atom.
The missing self link is just your garden variety bug – the documentation says it should be provided by default, but the code does not.
I went ahead and fixed these problems. Changeset 8529. The example above, when you change the call to atom_feed(:schema_date => 2008), looks like this.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
<?xml version="1.0" encoding="UTF-8"?> <feed xml:lang="en-US" xmlns="http://www.w3.org/2005/Atom"> <id>tag:localhost:/posts</id> <link type="text/html" rel="alternate" href="http://localhost:3000"/> <link type="application/atom+xml" rel="self" href="http://localhost:3000/posts.atom"/> <title>My great blog!</title> <updated>2007-12-23T04:23:07+11:00</updated> <entry> <id>tag:localhost:Post/1</id> <published>2007-12-23T04:23:07+11:00</published> <updated>2007-12-30T15:29:55+11:00</updated> <link type="text/html" rel="alternate" href="http://localhost:3000/posts/1"/> <title>First post</title> <content type="html">HOORAY. About ruby.</content> <author> <name>DHH</name> </author> </entry> </feed> |
mmm, semantic goodness
Test setup broken in Rails 2.0.2
Some changes went into rails 2.0.2 that mean the setup method in test subclasses won’t get called. Here’s how it went down:
You can see some code illustrating the problem in 8445. This affects two plugins that we’re using – helper_test and activemessaging.
For the helper test, the work around is to rename your helper test setup methods to setup_with_fixtures.
1 2 3 |
def setup_with_fixtures super end |
For activemessaging, add the following line to the setup of your functionals that are failing (from the mailing list):
1 |
ActiveMessaging.reload_activemessaging
|
Rails devs, reclaim your harddrive
1 2 |
cd code-dir find . | egrep "(development|test)\\.log" | grep -v .svn | xargs rm |
I’d forgotten to clear out my logs for a long while. This found me 9.5Gb!
Logging SQL statistics in rails
When your sysadmin comes to you whinging with a valid concern that your app is reading 60 gazillion records from the DB, you kinda wish you had a bit more information than % time spent in the DB. So I wrote a plugin that counts both the number of selects/updates/inserts/deletes and also the number of records affected.
1 |
git clone git://github.com/xaviershay/sql-counter.git vendor/plugins/sql_counter |
That does the counting, you need to decide how to log it. I am personally quite partial to adding it to the request log line, thus getting stats per request:
1 2 3 4 5 |
# vendor/rails/actionpack/lib/action_controller/benchmarking.rb:75 log_message << " | Select Records: #{ActiveRecord::Base.connection.select_record_count}" log_message << " | Selects: #{ActiveRecord::Base.connection.select_count}" ActiveRecord::Base.connection.reset_counters! |
Don’t forget the last line, otherwise you get cumulative numbers. That may be handy, but I doubt it. We’re only logging selects because that’s all we care about at the moment. I am sure this will change in time.
UPDATE: Moved to github, bzr repo is no longer available
Extending Rails
Previously, I extended rails by monkey patching stuff in lib/. This was good because it kept vendor/rails clean.
I have changed my mind!
I now just patch vendor/rails directly with a comment prefixed by RBEXT explaining why. This means that when I piston update rails, I get notified of any conflicts immediately, rather than having to remember what was in lib. It’s also much easier and quicker than monkey patching. Theoretically, I could also run the rails tests to make sure everything is still kosher, but I must confess I haven’t gotten around to patching the tests as well…
And the comments are ace because I can use this sweet rake task to see what rb-rails currently looks like:
1 2 3 4 |
desc "Show all RB extensions in vendor/" task :core_extensions do FileList["vendor/**/*.rb"].egrep(/RBEXT/) end |
How we use the Presenter pattern
FAKE EDIT: I wrote this article just after RailsConf but have just got around to publishing it. Jay has since written a follow up which is worthwhile reading.
I may have been zoning out during Jay Fields talk at RailsConf – not sleeping for a few days will do that to you – but I think I got the gist of his presentation: “Presenter” isn’t really a pattern because it’s use is to specific and there isn’t anything that be generalized from it. Now, I’m not going to argue with Jay, but I thought it may be helpful to give an example of how we’re using this “pattern” and how it is helpful for us at redbubble.
Uploading a piece of work to redbubble requires us to create two different models – a work and a storage, and associate them with each other. Initially, this logic was simply in the create method of one of our controllers. My problem with this was it obscured the intent of the controller. To my mind a controller is responsible for the flow of the application – the logic governing which page the user is directed to next – and kicking off any changes that need to happen at the model layer. In this case the controller was also dealing with the exact associations between the models, roll back conditions. Code that as we will see wasn’t actually specific to the controller. In addition, passing validation errors through to the views was hard because errors could exist on one or more of the models. So we introduced a psuedo-model that handled the aggregation of the models for us, it looks something like this:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 |
class UploadWorkPresenter < Presenter include Validatable attr_reader :storage attr_reader :work delegate_attributes :storage, :attributes => [:file] delegate_attributes :work, :attributes => [:description] include_validations_for :storage include_validations_for :work def initialize(work_type, user, attributes = {}) @work_type = work_type @work = work_type.new(:user => user, :publication_state => Work::PUBLISHED) @storage = work_type.storage_type.new initialize_from_hash(attributes) end def save return false if !self.valid? if @storage.save @work.storage = @storage if @work.save return true else @storage.destroy end end return false end end |
We have neatly encapsulated the logic of creating a work in a nice testable class that not only slims our controller, but can be reused. This came in handy when our UI guy thought it would be awesome if we could allow a user to signup and upload a work all on the same screen:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 |
class SignupWithImagePresenter < UploadWorkPresenter attr_reader :user delegate_attributes :user, :attributes => [:user_name, :email_address] include_validations_for :user def initialize(attributes) @user = User.new super(ImageWork, @user, attributes) end def save return false if !self.valid? begin User.transaction do raise(Validatable::RecordInvalid.new(self)) unless @user.save && super return true end rescue Validatable::RecordInvalid return false end end end |
So why does Jay think this is such a bad idea? I think it stems from a terminology issue. Presenters on Jay’s project were cloudy with their responsibilties – handling aggregation, helper functions, and navigation. As you can see, the Presenters we use solely deal with aggregation, keeping their responsibility narrow.
For reference, here is our base Presenter class:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 |
class Presenter extend Forwardable def initialize_from_hash(params) params.each_pair do |attribute, value| self.send :"#{attribute}=", value end unless params.nil? end def self.protected_delegate_writer(delegate, attribute, options) define_method "#{attribute}=" do |value| self.send(delegate).send("#{attribute}=", value) if self.send(options[:if]) end end def self.delegate_attributes(*options) raise ArgumentError("Must specify both a delegate and an attribute list") if options.size != 2 delegate = options[0] options = options[1] prefix = options[:prefix].blank? ? "" : options[:prefix] + "_" options[:attributes].each do |attribute| def_delegator delegate, attribute, "#{prefix}#{attribute}" def_delegator delegate, "#{attribute}=".to_sym, "#{prefix}#{attribute}=".to_sym def_delegator delegate, "#{attribute}?".to_sym, "#{prefix}#{attribute}?".to_sym end end end |
Object#send_with_default
Avoid those pesky whiny nils! send_with_default won’t complain.
1 2 3 |
"hello".send_with_default(:length, 0) # => 5 nil.send_with_default(:length, 0) # => 0 "hello".send_with_default(:index, -1, 'e') # => 1 |
So sending parameters is a little clunky, but I don’t reckon’ you’ll want to do that much. Here is the extension you want:
1 2 3 4 5 6 7 8 9 |
module CoreExtensions module Object def send_with_default(method, default, *args) !self.nil? && self.respond_to?(method) ? self.send(*args.unshift(method)) : default end end end Object.send(:include, CoreExtensions::Object) |
I'm a rails contributor
Allow me to gloat for a moment. Please turn your attention to changeset 7692 you’ll notice my name in the credits. So it’s not much, but there’s a certain amount of geek cred there.
Counting ActiveRecord associations: count, size or length?
Short answer: size. Here’s why.
length will fall through to the underlying array, which will force a load of the association
1 2 3 |
>> user.posts.length Post Load (0.620579) SELECT * FROM posts WHERE (posts.user_id = 1321) => 162 |
This is bad. You loaded 162 objects into memory, just to count them. The DB can do this for us! That’s what count does.
1 2 3 |
>> user.posts.count SQL (0.060506) SELECT count(*) AS count_all FROM posts WHERE (posts.user_id = 1321) => 162 |
Now we’re on to something. The problem is, count will always issue a count to the DB, which is kind of redundant if you’ve already loaded the association. That’s were size comes in. It’s got smarts. Observe!
1 2 3 4 5 6 7 |
>> User.find(1321).posts.size User Load (0.003610) SELECT * FROM users WHERE (users.id = 1321) SQL (0.000544) SELECT count(*) AS count_all FROM posts WHERE (posts.user_id = 1321) => 162 >> User.find(1321, :include => :posts).posts.size User Load Including Associations (0.124950) SELECT ... => 162 |
Notice it uses count, but if the association is already loaded (i.e. we already know how many objects there are), it uses length, for optimum DB usage.
But know that’s not all. There’s always more. If you also store the number of posts on the user object, as is common for performance reasons, size will use that also. Just make sure the column is named _association__count (i.e. posts_count).
1 2 3 4 5 |
>> User.columns.collect(&:name).include?("posts_count") => true >> User.find(1321).posts.size User Load (0.003869) SELECT * FROM users WHERE (users.id = 1321) => 162 |
The bad news
So now you’re all excited, I better tell you why this is only fantastic until you start using has_many :through.
Now, the situation is slightly different between 1.2.x (r4605) and edge (r7639), so I’ll start with stable. Now, they may look the same but a normal has_many association and one with the :through option are actually implememted by two entirely separate classes under the hood. And it so happens that the has_many :through version kind of, well, doesn’t have quite the same smarts. It loads up the association just as length does (then falls through to Array#size). Edge is sharp enough to use a This patch was added to edge in 7692count, but still doesn’t know about any caches you may be using. This was commited in r7237, so it’s pretty easy to patch in to stable. Or you can use this extension (on either branch – here is the trac ticket):
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
module CoreExtensions::HasManyThroughAssociation def size return @owner.send(:read_attribute, cached_counter_attribute_name) if has_cached_counter? return @target.size if loaded? return count end def has_cached_counter? @owner.attribute_present?(cached_counter_attribute_name) end def cached_counter_attribute_name "#{@reflection.name}_count" end end ActiveRecord::Associations::HasManyThroughAssociation.send(:include, CoreExtensions::HasManyThroughAssociation) |
How it doesn’t work
1 |
user.posts.find(:all, :conditions => ["reply_count > ?", 50]).size |
size normally works because assocations use a proxy – when I call user.posts it won’t actually load any posts until I call a method that requires them. So user.posts.size can work without ever loading the posts because they aren’t required for the operation. The above code won’t work well because find does not use a proxy – it will straight away load the requested posts from the DB, without size getting a chance to send a COUNT instead. You may be better off moving this finder logic into an association so that size will work as expected. This also has the benefit that if you decide to add a counter cache later on you won’t have to change any code to use it.
1 |
has_many :popular_posts, :class_name => "Post", :foreign_key => "post_id", :conditions => ["reply_count > ?", 50] |
So use size when counting associations unless you have a good reason not to. Most importantly thought, ensure you’re watching your development log so to be aware what SQL your app is generating.
UPDATE: Added link to my patch on trac
UPDATE 2: ... which is now closed, see r7692
Straight Sailing with Magellan
Magellan is a Ruby on Rails plugin that provides a framework for abstracting navigation logic out of your views and controllers, allowing you to write neater, more reusable code.
Table of Contents
Why should I use Magellan?
The short answer is you probably shouldn’t. Sorry, thanks for stopping by, please visit the gift shop. To elaborate, many applications don’t actually have complex navigational requirements. They are more generally of the type “go from page A to page B, then from there to page C”, and that’s that. While of course Magellan can neatly express these relationships, it adds a layer of complexity to your application for questionable benefit.
Where Magellan excels is in expressing more complex requirements: “go from page A to page B, unless it’s a Thursday, in which case go to page C. If we got to page C from page A, then go to page B, otherwise go to page A”. Urgh. Where do you put this logic in a traditional rails app? You don’t want this kind of logic in your views, and if you put it in your controllers you’ll end up duplicating code. You need a better solution.
You need Magellan.
Using Magellan
To use Magellan you need to understand three concepts:- Pages
- Links
- State
State is a more advanced topic, so we’ll go over that bit later on. You covered the first two in Web Coding 101, so I’ll go over them first. The only difference in Magellan’s usage of the terms “page” and “links” is a level of abstraction. Simply, a Magellan page represents a URL (rails or otherwise). Drop the following code into your environment.rb:
1 2 3 |
RHNH::Magellan::Navigator.draw do |map| map.add_page :home, {:controller => 'home', :action => 'list'} end |
Easy. To link to this page in a view, we use the nav_link_to helper in our .rhtml file instead of link_to. The first parameter is the name of the page we are currently on – in this case it is not strictly required and could be set to nil.
1 |
nav_link_to :current_page, :home |
That in of itself isn’t particularly exciting. Where things get tasty is when we start using links. Now, in basic usage a link acts the same way as a page1. We can create a next link that is different depending on which page you are on.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
RHNH::Magellan::Navigator.draw do |map| map.add_page :home1 do |p| p.url = { :controller => 'home1' } p.add_link :next, :home2 end map.add_page :home2 do |p| p.url = { controller => 'home2' } p.add_link :next, :home1 end end # Then in both home1.rhtml and home2.rhtml # @current_page is either :home1 or :home2 nav_link_to @current_page, :next |
As you can see we have de-coupled our navigation from the page itself. If we wanted to we could change the next link for home2 to home3 without having to change any of the code associated with home2. This makes our pages more modular and reusable, which is generally a Good Thing.
Dynamic Links
Let’s go back to our original example. I want the next link on page A to go to page B except on Thursdays, where it should go page C. The trick here is that in addition to just accepting a symbol for the link name (a “static link”), it can also accept a lambda block that is evaluated at runtime. This is a little bit more convoluted, the block needs to return not a link name, but the actual page we want to go to. While initially slightly unintuitive, it allows for more flexibility and less code than having to specify extra links.
1 2 3 4 5 6 7 8 9 10 11 |
RHNH::Magellan::Navigator.draw do |map| map.add_page :page_a do |p| p.add_link :back, lambda {|pages, state| # Thursday is the 4th day the of week Time.new.wday == 4 ? pages[:page_b] : pages[:page_c] } end map.add_page :page_b, { :controller => 'page_b' } map.add_page :page_c, { :controller => 'page_c' } end |
State
State is just like session storage for your navigation logic. In fact, it actually uses a subset of session storage2. The reason we differentiate it from normal session variables is simply to keep a neat separation between our navigation logic and other modules that may require the session. In typical usage, you modify the state in your controller (using set_nav_state, and then make a decision based on that state in your navigation logic (using the state parameter). A simple example is to have a dynamic back link depending on the previous page.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
# Both page A and B have a link to page C def page_a; set_nav_state :back_page => 'page_a'; end; def page_b; set_nav_state :back_page => 'page_b'; end; # Page C nav_link_to 'Back', :page_c, :back # environment.rb RHNH::Magellan::Navigator.draw do |map| map.add_page :page_a, { :controller => 'page_b' } map.add_page :page_b, { :controller => 'page_c' } map.add_page :page_c, { do |p| p.add_link :back, lambda {|pages, state| pages[state[:back_page]] } end end |
Testing your navigation
As with any code, it is important to test your navigation logic. There are many ways to do this, depending on the requirements and complexity of your application. I recommend at least one class of unit tests for your logic, and also to add code to your functional tests to ensure your controllers are setting the correct state. Magellan provides one helper function here – nav_state – which returns a hash of the current state.
1 2 3 4 5 6 7 8 9 10 11 12 |
class UnitTest < Test::Unit::TestCase def setup @nav = RHNH::Magellan::Navigator.instance end def test_back_link state = { :homepage => :home1 } expected = { :controller => 'example', :action => 'home1' } assert_equal expected, @nav.get_url(:page1, :back, state) end end |
1 2 3 4 5 6 7 8 9 |
class FunctionalTest < Test::Unit::TestCase # Standard functional test setup code... def test_index get 'index' assert_equal :home1, nav_state[:homepage] end end |
The tests included with the example that comes with Magellan provide a more complex example of navigation testing. I highly recommend you look over them.
Extra morsels
You can specify a default link by adding a link to the map rather than a page. For instance, to specify a default :back link:
1 2 3 4 |
RHNH::Magellan::Navigator.draw do |map| map.add_page :home, { controller => 'home' } map.add_link :back, :home end |
To be extra fancy, you can return extra parameters from your navigation logic that are added to the :params hash of the url. This is done by returning an array with both the page and the parameters in it.
1 2 3 4 5 6 |
RHNH::Magellan::Navigator.draw do |map| map.add_page :home, { controller => 'home' } map.add_link :back, lambda { |pages, state| [pages[:home], {:message => 'You just hit a default link'}] } end |
To conclude
Magellan is a great way of managing the complexity of larger projects. By abstracting navigation logic out of your controllers and views you make your project much more modular and reusable. It can even be introduced incrementally – all your old link_to calls will still work.
Footnotes
1 To be technically correct, a page acts like a link. Magellan creates default links to pages with the same name as the page. For instance, unless you specify otherwise, :home is actually a link to the page :home
2 Magellan uses session[:rhnh_navigator_state], so you may want to steer clear of that to avoid stepping on anyone’s toes.
Rails XHTML Validation with LibXML/HTML Tidy
I improved upon the XHTML validation technique I showed yesterday to add nicer error messages, and also support for local testing via HTML Tidy. HTML Tidy isn’t quite as good as W3C – for example it missed a label that was pointing to an invalid ID, but it runs hell fast. For W3C testing I’m now using libXML to parse the response to actually list the errors rather than just tell you they exist.
And it’s all customizable by setting the MARKUP_VALIDATOR environment variables. Options are: w3c, tidy, tidy_no_warnings. Tidy is the default.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 |
def assert_valid_markup(markup=@response.body) ENV['MARKUP_VALIDATOR'] ||= 'tidy' case ENV['MARKUP_VALIDATOR'] when 'w3c' # Thanks http://scottraymond.net/articles/2005/09/20/rails-xhtml-validation require 'net/http' response = Net::HTTP.start('validator.w3.org') do |w3c| query = 'fragment=' + CGI.escape(markup) + '&output=xml' w3c.post2('/check', query) end if response['x-w3c-validator-status'] != 'Valid' error_str = "XHTML Validation Failed:\n" parser = XML::Parser.new parser.string = response.body doc = parser.parse doc.find("//result/messages/msg").each do |msg| error_str += " Line %i: %s\n" % [msg["line"], msg] end flunk error_str end when 'tidy', 'tidy_no_warnings' require 'tidy' errors = [] Tidy.open(:input_xml => true) do |tidy| tidy.clean(markup) errors.concat(tidy.errors) end Tidy.open(:show_warnings=> (ENV['MARKUP_VALIDATOR'] != 'tidy_no_warnings')) do |tidy| tidy.clean(markup) errors.concat(tidy.errors) end if errors.length > 0 error_str = '' errors.each do |e| error_str += e.gsub(/\n/, "\n ") end error_str = "XHTML Validation Failed:\n #{error_str}" assert_block(error_str) { false } end end end |
Getting Tidy to work was an ordeal, the ruby documentation is rather lacking. It also behaves in weird ways – the call to errors returns a one element array, with all the errors bundled together in the one string.
LibXML was a little tricky – there’s no obvious way to parse an XML document in memory. You’d think XML::Document.new(xml) would do the trick, since there’s a XML::Document.file(filename) method, but that actually uses the entire XML document as the version string. Not so handy. Turns out you need to create an XML::Parser object instead, as I’ve done above. The docs don’t mention this (anywhere obvious, that is), I found a thread in the LibXML mailing list.
Testing rails
I was working on creating functional tests for some of my code today, a task made ridiculously easy by rails. To add extra value, I added an assertion (from Scott Raymond) to validate my markup against the w3c online validator:
1 2 3 4 5 6 7 8 9 10 |
def assert_valid_markup(markup=@response.body) if ENV["TEST_MARKUP"] require "net/http" response = Net::HTTP.start("validator.w3.org") do |w3c| query = "fragment=" + CGI.escape(markup) + "&output=xml" w3c.post2("/check", query) end assert_equal "Valid", response["x-w3c-validator-status"] end end |
The ENV test means it isn’t run by default since it slows down my tests considerably, but I don’t want to move markup checks out of the functional tests because that’s where they belong. Next step is to validate locally, which I’ve heard you can do with HTML Tidy.
Another problem is testing code that relies on DateTime.now, since this is a singleton call and not easily mockable.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
def pin_time time = DateTime.now DateTime.class_eval <<-EOS def self.now DateTime.parse("#{time}") end EOS yield time end # Usage pin_time do |test_time| assert_equal test_time, DateTime.now sleep 2 assert_equal test_time, DateTime.now end |
I haven’t found a neat way of resetting the behaviour of now. Using load 'date.rb' works but produces warnings for redefined constants. I couldn’t get either aliasing the original method, undefining the new one, or even just calling Date.now to work.
UPDATE: Ah, how young I was. A better way to do this is to use a library like mocha