Posted by Christopher Wojno
Fri, 16 May 2008 00:56:00 GMT
I hurriedly wrote an article this morning about my adaptation of fixtures I call sub-fixtures. While it seemed like a good idea in theory, it it beastly to implement in practice. It requires writing new fixtures for every unique test (if used correctly). When used incorrectly, the test names don’t describe the data configuration being tested against.
I sat down and thought about it a little longer. What I really need is a scenario. A database state poised for a particular test. Needless to say: such a scenario is not limited to a single table. That way, the scenario is describing the database state being tested against and can therefore be used logically in other tests as well.
Only one minor change is required. In the test directory, add the following code to the body of the Test::Unit::TestCase class in test_helper.rb:
def load_scenario( scenario )
directory = File.dirname(__FILE__)+'/fixtures/scenarios/'+scenario+'/'
files = Dir.entries( directory )
files.delete( '.' ); files.delete( '..' )
exp = /[^\.].*\.(yml|csv)$/
models = files.select{|f| f[exp] }
models = models.collect{|m| m.slice(0,m.length-4).to_sym}
Fixtures.create_fixtures( directory, models )
end
Again, it requires a special directory structure (maybe I’ll write a scenario generator later). in the test/fixtures I added a new directory: scenarios. Within that directory are other directories containing the scenarios fixture setups. For example, if I want to create a scenario where my list of weekly cookie recipes is missing a cookie and I want to test my code to see if it finds the correct missing cookie, I simply create the following directory:
test/fixtures/scenarios/missing_cookie
And in that directory, I create the following fixture files:
- cookies.yml
- users.yml
- user_cookies.yml
Note: it works for .csv files as well. Simply create the fixtures like you normally would do.
Now, in your unit test code, add the following line to the beginning of your test case method:
load_scenario( 'missing_cookie' )
Now, when your test runs, it will create the scenario in the database that you specify. It will automatically load all the fixture files in the directory.
Problems
Again, if you use normal fixtures, they will be loaded and unloaded (will make testing a little slower).
Posted in Rails Snippets | Tags fixture, scenario, testing, unit | 1 comment
Posted by Christopher Wojno
Thu, 15 May 2008 19:25:00 GMT
I’ve found that when testing model classes, it’s important to have a database that is setup with records such that something fails or is configured to produces results in a particular way. With the current rails Fixture framework, this is impossible. Let me explain what I’ve been trying to do to give you a better idea of what is lacking in Rails:
Example Problem Description Finding Something That’s “Missing”
Suppose you have a database with a table called “cookies” and another table “users” and another table called “user_cookies.” As you can see, you have a set of users and types of cookies. Maybe the cookies are chocolate or mint, etc. However, these are cookies of the month. Each month, a new cookie is released. To keep track of which month the cookie is for, we include a year and month field in the cookie table. Now, if we have users that want to track which cookies they’ve tried over the course of the years, we need to either keep a list of all the cookies they’ve eaten, or search for holes in the cookie order.
Even if we had a list, say we had a weekly and monthly cookie. If we wanted to know which monthly cookies we’re missing, the list will be of no use as well, as the list cannot differentiate between monthly or weekly. We’re forced to look for holes. Luckily, such a request is rare in our system, so a brute force search will be small (there are ways of making it faster, but that’s beyond the scope of this article).
Rails Testing Framework “Deficiency”
Don’t get me wrong, the testing framework for Rails is well done, easy to use, and mostly comprehensive. However, the Fixture support is a bit lacking. If you want to have multiple types of tests that act on the database records as a whole, you need custom fixtures. For the above problem, we need a fixture to test cookie lookups, but we also need a test to detect missing cookies.
So we need multiple fixtures we can load with different tests. Well, that’s not really possible with the standard testing framework. The class method “fixtures” for Test::Unit::TestCase (the testing base class) will only load up the standard fixtures. Bummer. I want to create subdirectories in the fixtures directory that are named after the model being tested. Within those directories, there will be more directories that name the test in progress. Within THAT directory will be the .yml files for custom database setups.
So the directory structure is of this form:
|-+BASE RAILS APPLICATION
| |-+ test
| | |-+ fixtures
| | | |-+ MODEL_NAMEs
| | | | |-+ test_find_missing_cookies
| | | | | |- users.yml
| | | | | |- cookies.yml
| | | | | |- user_cookies.yml
| | | | |-+ test_find_favorite_cookie
| | | | | |- user_cookies.yml
...
Not only can you deploy custom fixtures for models and more than just one, you can also specify tables to populate other than the model with which you’re working. This provides as much flexibility as I can fathom that I need.
But, alas, you need a way to load these fixtures up. Admittedly, my method is a bit messy and slow if you use the traditional Fixtures. You need to modify the test_helper.rb file. Add this to the Test::Unit::TestCase class:
@@model_name = nil
def self.fixture_model( m )
@@model_name = m
end
protected
def load_subfixtures( test_method, pfixture_table_names = [], pfixture_class_names = [] )
raise StandardError.new( 'load_subfixtures requires that you specify the model name in the class by calling the class method: fixture_model' ) if @@model_name.nil?
fixture_path = File.dirname(__FILE__) + '/fixtures/'+@@model_name.to_s+'/'+test_method+'/'
return super unless File.exists?( fixture_path )
oldpath = fixture_path
oldtables = fixture_table_names
oldclasses = fixture_class_names
@@fixture_path = fixture_path
@@fixture_table_names = pfixture_table_names
if @@fixture_table_names.empty?
@@fixture_table_names = fixture_table_names
else
@@fixture_table_names.push @@model_name
end
@@fixture_class_names = pfixture_class_names
@@fixture_class_names = fixture_class_names if @@fixture_class_names.empty?
oldret = load_fixtures
@@fixture_path = oldpath
@@fixture_table_names = oldtables
@@fixture_class_names = oldclasses
return oldret
end
As you can see, it does not override the fixture initializer for the unit tests. It merely calls it again. Here’s how you use it:
require File.dirname(__FILE__) + '/../test_helper'
class UserCookieTest < Test::Unit::TestCase
fixture_model :user_cookies
# Replace this with your real tests.
def test_find_missing_cookies
load_subfixtures( 'test_find_missing_cookies', [:users,:cookies] )
assert_equal 2, UserCookie.count
end
end
For the test: test_find_missing_cookies, it will load up the .yml files: user_cookies.yml, cookies.yml, and users.yml for use with that test.
You need to specify the fixture_model as this is the directory it will look in for additional sub-fixtures. If you don’t the call will fail with an exception.
Problems
Obviously, it’s not very smart. It can’t automatically detect the test name. Though, this allows you to re-use fixtures from other tests as well as mix and match the sub-fixtures loaded with the test. But it requires that you manually type in the test name for every test.
In addition, if the fixture has already been loaded using the standard class method call: fixtures, it will be loaded and overwritten by the test a second time. This will slow the testing process down.
I’m happy with it. I bet you’ve been waiting for something like this too. Enjoy!
Edit
An update to this article is posted here.
Posted in Rails Snippets | Tags fixture, test, unit | no comments
Posted by Christopher Wojno
Mon, 13 Aug 2007 18:37:00 GMT
Rails has a powerful form builder mechanism. However, if you want to make a list of things and have the data flow back to you without involving ActiveRecord, you’re in for some hurt; well, you were, unless you decide to use this module. Warning: this article is moderately advanced. I’m assuming you’re familiar with ActionControllers and the ActionView in my examples. If you’re familiar with how to use the basics of Rails, you should probably only read the first 1/2. Read the first code block to see what my code can do. But to see why this code is useful, read on to the examples.
Say I have a set of fox phrases. Now, if a certain fox is to have a list of catch phrases, and you don’t want to store them in a database, you can’t use Rails’ form helpers (text_field, hidden_field, etc.) and the params[] calls without a good deal of ugly, hack code (creating spoof instance variables to display data and creating a fake active record objects to receive that data from a form). To help beautify code, I created an additional way of accessing (setting and getting) elements in an array (valN). Behold the ArrayIterAccessors:
>> @catch_phrases = ['Addiction is like Pokemon!','Hey! There goes my pickup!','Chucky Bacon!']
=> ["Addiction is like Pokemon!", "Hey! There goes my pickup!", "Chucky Bacon!"]
>> @catch_phrases.val2
=> "Chucky Bacon!"
>> @catch_phrases.val2 = 'Chunky Bacon! Chunky Bacon! Chunky Bacon!'
=> "Chunky Bacon! Chunky Bacon! Chunky Bacon!"
>> @catch_phrases
=> ["Addiction is like Pokemon!", "Hey! There goes my pickup!", "Chunky Bacon! Chunky Bacon! Chunky Bacon!"]
“Now,” you say, “why the heck is he not just using the []!?” Well, I say, Try doing that within ActionView (rhtml template) for a text field:
<%= text_field 'catch_phrases', '[0]' %>
That will fail horribly. Depending on what you set @catch_phrases to, you’ll get:
undefined method `[0]' for ['Addiction is like Pokemon!','Hey! There goes my pickup!','Chucky Bacon!']:Array
And that doesn’t get you anywhere. So, supposin’ you say, oh, I dunno:
# My special module (should be a plugin but for demonstration
# purposes, I'm defining it inline
# Skip this code for now
class CatchPhrasesController < ApplicationController
module ArrayIterAccessors
alias :old_method_missing :method_missing
def method_missing( sym, *args, &block )
sym_s = sym.to_s; t = sym_s[/\d+/]
if t and sym_s[/\Aval\d+=?\Z/]
if sym_s[/=\Z/]
self[t.to_i] = args.first; return self[t.to_i]
else
return self[t.to_i]
end
else
return old_method_missing( sym, *args, &block )
end
end
end
# this is an action for your CatchPhrases Controller URL:
# http://site.example.com/catch_phrases/new
def new
Array.send( :include, ArrayIterAccessors )
@catch_phrases = ['Addiction is like Pokemon!','Hey! There goes my pickup!','Chucky Bacon!']
end
end
### in your view: new.rhtml ###
<% @catch_phrases.each_with_index do |item,index| %>
<%= text_field 'catch_phrases', "val#{index}" %>
<% end %>
So when your view renders, you’ll see 3 text fields, each with the catch phrase pre-filled (so you’ll have an addiction catch phrase, a pickup catch phrase, and a chunky bacon catch phrase). You can edit them and when you submit the form, you can reconstitute your array by:
@catch_phrases = params[:catch_phrases].keys.collect{|k| k.to_s[/\d+/] }.sort.collect{|k| params[:catch_phrases][('val'+k.to_s).to_sym] }
If you didn’t change the catch phrases, you’ll get the exact array: @catch_phrases, back. And if you DID change the phrases, you’ll get an array, in order, of the altered catch phrases!
Pitfalls
- No ActiveRecord (well, I claimed that as a plus above) means: no validation. That’s up to you, unless you include a validatable mixin (there are a few of those). This also means there is no way of reporting errors back automatically. I suppose I could write something later to do this.
- That last line is ugly and slow.
- The use example is for demonstration purposes and is awful: DO NOT USE IT. That will mix the module in EVERY time you render the “new” action. To do this correctly take the module and the
Array.send :include, ArrayIterAccessors
code out of your controller and:
- Put it in a plugin OR
- Create a new file in lib/array_iter_accessors.rb (in your Rails project) and put the module into that. Then, in your environment.rb file (at the bottom), put the
Array.send :include, ArrayIterAccessors
code. That way, the mixin will only be done once.
What I did
I overrided the method_missing method for the Array class (then mixed it in). I scan for any method calls for methods beginning with “val” and ending in a number. I also parse for an ’=’ in case you’d like to use it for assignment too. Then, I call the array’s [] operator to access the values at the desired position. And because I’m using the array’s built-in operator, you’ll see all the error messages and behavior you’re used to seeing without my module.
Why val?
Because it’s two letters shorter than “value.” What? You expected a profound reason? Sorry if this precludes your ability to keep track of your girlfriends.
You have enough here to write your own plug-in, or just to use it when you need it. Happy riding.
Posted in Ruby Snippets, Rails Snippets | Tags action, active, array, controller, module, plugin, rails, ruby, val, view | no comments
Posted by Christopher Wojno
Fri, 10 Aug 2007 23:48:00 GMT
Now, before you think “FUD Time!”, I preface this post with the following warning: Yes, this is a problem with not just Rails but all sessions; no, it’s not unsolvable. So, in light of the bad news, there is much good news.
The Problem
Rails applications are very useful. However, most require sessions in order to be useful. Sure, you can provide an ActionWebService or REST-based service to not use sessions, but for the majority of user services, you’re stuck with sessions.
A Session About Sessions
(Go ahead and skip this part if you’re already familiar with how sessions work)
A session is a way a server remembers who you are (in case you didn’t know). The most common way of allowing a server to ID you, without knowing who you are exactly, is by sharing a secret! Well, it’s supposed to be secret and we’ll come to that in a bit. When a new session is created, your session ID is generated (it’s random, hopefully) and saved in the server. The id can be associated with other data, such as your name, or permissions. When you request a page, the server will give you this ID. You almost never see that ID because it’s sent in the background as a “cookie.” You’ve probably heard of these before. From now on, every time you request a page from the server, your cookie data is automatically included in the request (your web browser takes care of this) so the server can figure out who you are. Done correctly, no personally identifiable information is sent over the Web. Just your (hopefully) random ID.
Sounds good so far. But if you’re not using HTTPS (SSL/TSL encryption), your secret contained in that cookie can be seen when it is first sent, and when you access any page thereafter (because you’re sending it each time remember). So, if someone has access to your traffic (if you’re using a shared WiFi or a hub or have a shady ISP), they can pull out your secret. Here’s an old session I sniffed using a program called snort:
GET /javascripts/prototype.js?1186461045 HTTP/1.1..
Host: christopher.wojno.com..
User-Agent: Mozilla/5.0 (X11; U; FreeBSD i386; en-US; rv:1.8.1.6) Gecko/20070810 Firefox/2.0.0.6..
Accept: text/xml,application/xml,application/xhtml+xml,text/html;q=0.9,text/plain;q=0.8,image/png,*/*;q=0.5..Accept-Language: en-us,en;q=0.5..
Accept-Encoding: gzip,deflate..
Accept-Charset: ISO-8859-1,utf-8;q=0.7,*;q=0.7..
Keep-Alive: 300..
Connection: keep-alive..
Cookie: _session_id=db392fa5b39125fa7c1e581b9c1ec71d; is_admin=yes....
There it is, the last line. “Cookie.” So when my browser was trying to download some javascript libraries (Prototype is quite good), it sent my session ID. Now, the javascript doesn’t care about my session ID, but it was sent any way. If someone were to see that sent over the Internet (at any point on it’s way to the server), he or she could create a fake cookie with that ID and login as me without my password! (don’t try it, I’ve already logged out and reset the session) They could then, go into the settings and lock me out of my own system (until I change the password in the ruby console… that’ll show ‘em?). In the meantime, the damage has been done and it can happen again.
SSL!
I won’t go into SSL, but that little piece of technology has enabled credit card purchases over the Internet for years and will hopefully continue to do so for years to come. Any way, in short, it’s encryption and it will encrypt cookies too!
So, we need to force rails to encrypt cookies. Not as simple as it sounds. First, you need to setup an SSL certificate on your server. Easier said than done, though, some Apache packages come with the tools to automatically generate one. For a confusing, yet interesting read, see X509 on Wikipedia.
I assume you installed your SSL certificate and have configured Apache1 to run with SSL (or lighty, if you’re using that instead (mongrel is not mentioned as it does not have SSL, though there is a way to use SSL and mongrel together)). If not, there are loads of tutorials. I warn you though, it’s fairly technical.
Hold onto your Cookies!
So, you have SSL and a Rails application running (I’m assuming). How do you tell Rails to make sure your cookies are only sent via SSL? Rails lets you specify it.
Tell Rails to use SSL Only
By default, Rails does NOT enforce SSL on cookies or sessions (that would be frustrating for development, wouldn’t it?). So you have to enable that enforcement yourself. If you want your session cookie to be sent over SSL site-wide (and generally, you do!), head into your rails application directory and open (in your favorite editor) app/controllers/application.rb Add the following anywhere in the ApplicationController class:
class ApplicationController < ActionController::Base
session :session_key => '_session_id', :session_secure => true
end
The ApplicationController class definition should already exist, don’t duplicate it. Also, make sure that session isn’t already specified. If it is, the important part here is ”:session_secure => true”. Rails will now tell the browser to only send the session cookie if the browser is using the https (SSL) protocol. This feature is poorly documented but hopefully this will help keep your applications that you write, safe. NOTE: You MUST use SSL if you enable this or your application will become extremely forgetful (Who are you? What are you doing in my kitchen?!).
If you’re interested in storing OTHER data (not overly recommended though due to this and another exploit) in other cookies, Rails offers cookie—manipulation (not really management, the API leaves something to be desired). You can tell individual cookies to only be sent over encrypted connections, just like the session cookie. The other exploit: if the computer is shared and the browser doesn’t clear out the cookies, the next person to sit at the computer can harvest the cookie information, so don’t store passwords, e-mail addresses… really anything in cookies, it’s just a bad idea. DO store worthless information you don’t want to save in your server, such as squirrel preferences.
Admittedly, generally the odds of someone having access to your network traffic (and your cookies, no! MY cookies!) directly is moderate. For the attacker to successfully pick out your cookie data from the slew of traffic wizzing by, he/she would have to be looking and looking for a specific cookie name. So make sure no one has it out for you.
I don’t mean to sound down on the Rails team. Dang-fine-job I say. Cookies aren’t really important and the session is cookie-based, so session security falls by the way-side. It’s up to all developers to keep his or her eyes open for potential pitfalls.
1_000.thank( Rails::DevTeam.members.collect{|m|m.email} )
1 Note: Apache doesn’t know how to run Ruby code as of this writing. You need to use an Apache’s mod_proxy. This will (after it’s been configured) then pass the requests from Apache, to mongrel, lighty, or… WEBrick (if you’re nuts)
Posted in Rails Snippets | Tags cookie, hijack, rails, ruby, secure_session, security, session, ssl | 2 comments
Posted by Christopher Wojno
Thu, 09 Aug 2007 21:30:00 GMT
Problems with Plurals
Trevor brought up a good point in my last Rails post concerning plurals.
So I decided to enlighten him/create something new.
Rails offers a mixin for Action View called (oddly enough) pluralize. It’s fairly simple, but unnecessarily limited. Here’s some syntax:
>> pluralize(1, 'person')
=> 1 person
>> pluralize(3, 'person')
=> 3 people
It’s annoying if you don’t want the number or are trying to construct complicated sentences. The other big problem is that it doesn’t work outside of an ActionView. Luckily, there’s pluralize So I complied with Trevor’s request but used the pluralize function:
>> module SmartPlural
>> def plural( count=0 )
>> return ( count != 1 ? self.pluralize : self.dup )
>> end
>> end
=> nil
>> String.send :include, SmartPlural
=> String
>> "Trevor".plural( 3 )
=> "Trevors"
>> "Trevor".plural( 1 )
=> "Trevor"
Sorry for the lack of a ! (bang) version. That requires writing C-code… and that means writing the pluralize function again… and I’m not doing that. Yes, you can call me lazy.
What happened here?
For those who are curious as to what I did: I created a module called SmartPlural, the name doesn’t really matter. After that, I created a new function called “plural” that will pluralize the string if the number is not 1 (0 apples, 1 apple, 2 apples, etc.)[1]. Finally, I install the new module into the String class as a mixin. Then I tested it on my friend here.
Why didn’t you do: (x.length == 1 ? ‘foo’ : ‘foos’)
Well, what if I don’t like foos (bad Mr. T joke here, I won’t suffer it upon you though)? Say I want to talk about octopus:
>> 'octopus'.pluralize
=> "octopi"
Rails already knows about quite a few irregular words and you don’t want to pluralize every word you’ll be pluralizing yourself; that’s just stupid and not DRY. Use the pluralize function that comes with Rails. Here’s yet another reason to do so. What if I like to juggle baby geese, you know, goslings? As in the previous example, octopus is a special word, it’s plural isn’t simply the original followed by an s. If I have:
>> 'goose'.pluralize
=> "gooses"
Rails is wrong. It doesn’t know about geese or goose. Now, I don’t expect the Rails team to think of every word, and neither did they. You can tell Rails how to plualize those special cases and it will be effective everywhere, now that’s DRY.
>> Inflector.inflections do |inflection|
>> inflection.irregular( 'goose', 'geese' )
>> end
=> [[/(g)eese$/i, "\\1oose"],... <snipped>
>> "goose".plural( 3 ) # make sure you have SmartPlural included in String
=> "geese"
Not all
One more thing: the default value out of String.plural is, well, the plural form:
>> "Trevor".plural
=> "Trevors"
So if you’re just interested in the plural form, and don’t care about numbers, you don’t have to use String.pluralize. Just a little bonus, a very small one.
The One-Liner
To use this code, just type (or preferably, paste):
module SmartPlural; def plural(count=0); return (count != 1 ? self.pluralize : self.dup ); end; end; String.send :include, SmartPlural
1 I like apples…
Posted in Rails Snippets | Tags line, one, plural, rails | 1 comment
Posted by Christopher Wojno
Tue, 07 Aug 2007 05:05:00 GMT
This missing feature of rails has really bugged me, but it’s so useful.
If you have a list of words such as: apples, oranges, and bananas as an array:
>> list = ['apples','oranges','bananas']
=> ["apples", "oranges", "bananas"]
You’d like to be able to have a variable length list and still have it look correct in the view. So a smaller list:
>> list = ['oranges','bananas']
=> ["oranges", "bananas"]
Should look like: “oranges and bananas”.
>> list = ['apples','oranges','bananas']
=> ["apples", "oranges", "bananas"]
>> and_or_list 'and', list
=> "apples, oranges, and bananas"
>> list.pop
=> "bananas"
>> and_or_list 'and', list
=> "apples and oranges"
>> list.pop
=> "oranges"
>> and_or_list 'and', list
=> "apples"
The following block of code will do just that:
def and_or_list( andor, list )
list = list.dup
comma = (list.size > 2 ? ',' : '')
list2 = list.pop if list.size > 1
s = list.join(', ')
s << comma+' '+andor+' ' + list2 if list2
s
end
Vioa!
Instant and easy listing of various things, in English. Your users will never know it’s generated.
I’ve wrapped it up in a neat little plug-in for you. Just install it in your vendor/plugins directory. It will automatically be available in your views.
AndOrList Module
Posted in Rails Snippets | Tags and, english, list, or, rails, ruby, sentence | 2 comments