For several months now I've been working on a system where the business rules are expressed in a Domain Specific Language. In fact, the majority of my focus these days goes towards working with Domain Specific Languages. However, I'm most fascinated by Domain Specific Languages that empower business subject matter experts.
One problem with DSLs is that almost anything can be considered a DSL. Everyone knows the common DSLs: YACC and spreadsheet macros. However, recently Rake and Ruby On Rails have gained much popularity as DSLs. Some even consider Cobol to be a DSL. And there lies the problem, you can make a case for almost anything to be considered a DSL.
Is that a problem? Well, to me it is. I enjoy writing, talking, and thinking about DSLs. And, when I can't be specific about what I'm discussing I find my conversations often get slowed down by describing what type of DSLs I'm not talking about. Or, worse, people dismiss the conversations because they don't believe that DSLs can work in the way described. This, unfortunately, is fairly common because of DSLs of the past that have failed.
By now I hope you can understand why I feel the need to classify the type of work I'm doing. For the past few months I've been referring to my work as Business Domain Specific Languages. Unfortunately, even if you prefix business on a topic all anyone really hears is DSL. Also, (as Jeremy Stell Smith pointed out) the DSLs I'm interested in are similar to natural language.
Therefore, in an attempt to focus the conversations and make them more productive I'm going to start describing the type of DSL work I'm doing as Business Natural Languages.
Thursday, June 29, 2006
Tuesday, June 27, 2006
Ruby on Rails Unit Tests
Updated following multiple comments requesting examples and questions on where to put AR::Base subclass tests.
Everyone (who reads this blog anyway) knows that you should not cross boundaries while unit testing. Unfortunately, Ruby on Rails seems to believe otherwise. This is evident by the fact that the
With a minor modification you can tell Rails not to run the
You'll need to add a .rake file to /lib/tasks. The file contains one line:
I included this test in my codebase to ensure expected behavior.
We have been using this style of testing for several months now and have over 130 tests at this point and our tests still run in less than a second.
This decision does carry some trade-offs though. First of all, it becomes a bit more work to test ActiveRecord::Base subclasses in your unit tests. I'm comfortable with the small amount of extra work since it results in a significantly faster running test suite.
Also, if you need to use a AR::Base class as a dependency for another class, you will need to mock or stub the AR::Base class. This generally requires using Dependency Injection or a framework such as Stubba. For example, if you have a method that returns an ActiveRecord::Base subclass you can mock the new call and return a stub instead.
For an example of what our unit tests look like here are some tests and the classes that the tests cover.
Everyone (who reads this blog anyway) knows that you should not cross boundaries while unit testing. Unfortunately, Ruby on Rails seems to believe otherwise. This is evident by the fact that the
test:units rake task has the pre-requisite db:test:prepare. Additionally, if you use script/generate to create a model, it creates a [model].yml fixture file and a unit test that includes a call to the fixtures class method. Rails may be opinionated, but that doesn't mean I have to agree with it.With a minor modification you can tell Rails not to run the
db:test:prepare task. You should also create a new test helper that doesn't load the additional frameworks that you will not need. I found some of the code for this from reading a great book, Rails Recipes, by Chad Fowler.You'll need to add a .rake file to /lib/tasks. The file contains one line:
Rake::Task[:'test:units'].prerequisites.clearAdditionally, you'll need to create a new helper file in /test. I named my file unit_test_helper.rb, but the file name is your choice.
ENV["RAILS_ENV"] = "test"As you can see, the unit_test_helper.rb only requires what is necessary; however, it also changes ActiveRecord::Base to throw an error if you attempt to access the connection from a unit test.
require File.expand_path(File.dirname(__FILE__) + "/../config/environment")
require 'application'
require 'test/unit'
require 'action_controller/test_process'
require 'breakpoint'
class UnitTest
def self.TestCase
class << ActiveRecord::Base
def connection
raise InvalidActionError, 'You cannot access the database from a unit test', caller
end
end
Test::Unit::TestCase
end
end
class InvalidActionError < StandardError
end
I included this test in my codebase to ensure expected behavior.
require File.dirname(__FILE__) + '/../unit_test_helper'Update (Again):
class AttemptToAccessDbThrowsExceptionTest < UnitTest.TestCase
def test_calling_the_db_causes_a_failure
assert_raise(InvalidActionError) { ActiveRecord::Base.connection }
end
end
We have been using this style of testing for several months now and have over 130 tests at this point and our tests still run in less than a second.
This decision does carry some trade-offs though. First of all, it becomes a bit more work to test ActiveRecord::Base subclasses in your unit tests. I'm comfortable with the small amount of extra work since it results in a significantly faster running test suite.
Also, if you need to use a AR::Base class as a dependency for another class, you will need to mock or stub the AR::Base class. This generally requires using Dependency Injection or a framework such as Stubba. For example, if you have a method that returns an ActiveRecord::Base subclass you can mock the new call and return a stub instead.
class AccountInformationPresenterIn the above code, mocking the new call on Account prevents an unnecesary database trip.
def account
Account.new
end
end
class AccountInformationPresenterTest
def test_account_returns_a_new_account
Account.expects(:new).returns(stub(:name=>'jay'))
AccountInformationPresenter.new.account
end
end
For an example of what our unit tests look like here are some tests and the classes that the tests cover.
require File.dirname(__FILE__) + '/../../../unit_test_helper'
class SelectTest < Test::Unit::TestCase
def test_select_with_single_column
assert_equal 'select foo', Select[:foo].to_sql
end
def test_select_with_multiple_columns
assert_equal 'select foo, bar', Select[:foo, :bar].to_sql
end
def test_date_time_literals_quoted
date = DateTime.new(2006, 1, 20, 13, 30, 54)
assert_equal "select to_timestamp('2006-01-20 13:30:54', 'YYYY-MM-DD HH24:MI:SS')", Select[date].to_sql
end
def test_select_with_symbol_and_literal_columns
assert_equal "select symbol, 'literal'", Select[:symbol, 'literal'].to_sql
end
def test_select_with_single_table
assert_equal 'select foo from foo', Select[:foo].from[:foo].to_sql
end
def test_select_with_multiple_tables
assert_equal 'select column from bar, foo',
Select[:column].from[:foo, :bar].to_sql
end
end
require File.dirname(__FILE__) + '/../../unit_test_helper'
class TimeTest < Test::Unit::TestCase
def test_to_sql_gives_quoted
t = Time.parse('2006/05/01')
assert_equal "to_timestamp('2006-05-01 00:00:00', 'YYYY-MM-DD HH24:MI:SS')", t.to_sql
end
def test_to_pretty_datetime
d = Time.parse("05/10/2006")
assert_equal "05-10-2006 12:00 AM", d.to_pretty_time
end
end
class Select < SqlStatement
class << self
def [](*columns)
unless columns.select { |column| column.nil? }.empty?
raise "Empty Column in #{columns.inspect}"
end
self.new("select #{columns.collect{ |column| column.to_sql }.join(', ')}")
end
end
def from
@to_sql += " from "
self
end
def [](*table_names)
@to_sql += table_names.collect{ |table| table.to_s }.sort.join(', ')
self
end
end
class Time
def to_sql
"to_timestamp('" + formatted + "', 'YYYY-MM-DD HH24:MI:SS')"
end
def to_pretty_time
self.strftime("%m-%d-%Y %I:%M %p")
end
private
def formatted
year.to_s + "-" + month.pad + "-" + day.pad + " " + hour.pad + ":" + min.pad + ":" + sec.pad
end
end
Ruby convert Time to Date
On more than one occasion I've needed to convert a Time object to a Date. Generally, I use the following code:
Date.parse(Time.now.strftime('%Y/%m/%d'))I have two questions.
- Is there a better way?
- Why doesn't Time have a to_date method?
Monday, June 26, 2006
Use erb to insert dynamic text
Tonight, I was writing up some new material and wanted to mix some examples with the text. I could have just pasted the code into the material; however, I wanted the material to update automatically if I updated the code. When working with Martin on his next book I got to experience the value in this practice first hand and it wasn't something I wanted to work without at this point.
So, I decided to use a similar tag system in my writing. When deciding on what syntax to use to delimit areas where I wanted code to execute I thought of the <% .. %> syntax common to .rhtml, .yaml, etc files. My first instinct was to use some type of description inside my delimiter and process the file based on the description. Then I realized, it would be easiest to simply express what I want to do by writing the valid ruby.
At this point I had a text file that contained small blocks of code surrounded by <%= %>. I could write a task that searches and evals all the code, but this seemed to be much better:
So, I decided to use a similar tag system in my writing. When deciding on what syntax to use to delimit areas where I wanted code to execute I thought of the <% .. %> syntax common to .rhtml, .yaml, etc files. My first instinct was to use some type of description inside my delimiter and process the file based on the description. Then I realized, it would be easiest to simply express what I want to do by writing the valid ruby.
At this point I had a text file that contained small blocks of code surrounded by <%= %>. I could write a task that searches and evals all the code, but this seemed to be much better:
erb INPUT > OUTPUTThat's it, my output is now generated from the static text and the dynamic code that executes. I love it when the simplest thing that could possibly work just works.
Friday, June 16, 2006
OpenStruct freeze behavior
While looking at OpenStruct, I noticed what I considered to be unexpected behavior.
irb(main):007:0> frozen = OpenStruct.new(:foo=>1).freezeThis behavior surprised me since freeze is defined as:
=> #
irb(main):008:0> frozen.foo
=> 1
irb(main):009:0> frozen.foo = 2
=> 2
irb(main):010:0> frozen.foo
=> 2
Prevents further modifications to obj. A TypeError will be raised if modification is attempted. There is no way to unfreeze a frozen object. See also Object#frozen?.To find out what was happening I opened ostruct.rb. The OpenStruct class defines methods based on the keys of the constructor hash parameter and stores the values in a hash.
a = [ "a", "b", "c" ]
a.freeze
a << "z"
produces:
prog.rb:3:in `<<': can't modify frozen array (TypeError)
from prog.rb:3
def initialize(hash=nil)A quick check shows that if you freeze an OpenStruct instance the value hash is not frozen.
@table = {}
if hash
for k,v in hash
@table[k.to_sym] = v
new_ostruct_member(k)
end
end
end
def new_ostruct_member(name)
name = name.to_sym
unless self.respond_to?(name)
meta = class << self; self; end
meta.send(:define_method, name) { @table[name] }
meta.send(:define_method, :"#{name}=") { |x| @table[name] = x }
end
end
irb(main):002:0> frozen = OpenStruct.new(:foo=>1).freezeTo fix this issue you could redefine freeze and delegate the call to the freeze to both the value hash as well as the object. The problem with this solution is that when the TypeError exception is raised it will return hash as the frozen object, not the OpenStruct.
=> #
irb(main):003:0> frozen.frozen?
=> true
irb(main):004:0> table = frozen.send :table
=> {:foo=>1}
irb(main):005:0> table.frozen?
=> false
irb(main):006:0> table.freezeAnother solution is to change the definition of new_ostruct_member.
=> {:foo=>1}
irb(main):007:0> frozen.foo = 2
TypeError: can't modify frozen hash
from /opt/local/lib/ruby/1.8/ostruct.rb:75:in `[]='
from /opt/local/lib/ruby/1.8/ostruct.rb:75:in `foo='
from (irb):7
from :0
class OpenStructThe above change will raise OpenStruct as the frozen class when a modification attempt is made. To verify the change works correctly I wrote the following test.
def new_ostruct_member(name)
name = name.to_sym
unless self.respond_to?(name)
meta = class << self; self; end
meta.send(:define_method, name) { @table[name] }
meta.send(:define_method, :"#{name}=") do |x|
raise TypeError, "can't modify frozen #{self.class}", caller(1) if self.frozen?
@table[name] = x
end
end
end
end
class OpenStructTest < Test::Unit::TestCase
def test_struct_does_not_modify_table_if_frozen
f = OpenStruct.new(:bar=>1).freeze
assert_raise(TypeError) { f.bar = 2 }
end
end
Subscribe to:
Posts (Atom)