<?xml version='1.0' encoding='UTF-8'?><?xml-stylesheet href="http://www.blogger.com/styles/atom.css" type="text/css"?><feed xmlns='http://www.w3.org/2005/Atom' xmlns:openSearch='http://a9.com/-/spec/opensearchrss/1.0/' xmlns:georss='http://www.georss.org/georss' xmlns:gd='http://schemas.google.com/g/2005' xmlns:thr='http://purl.org/syndication/thread/1.0'><id>tag:blogger.com,1999:blog-5573355675806579511</id><updated>2012-01-27T16:08:29.834-05:00</updated><category term='ruby testing OBA VSTO Excel'/><category term='JavaScript Mocha'/><title type='text'>Climb That Mountain</title><subtitle type='html'>Attila Domokos' blog</subtitle><link rel='http://schemas.google.com/g/2005#feed' type='application/atom+xml' href='http://www.adomokos.com/feeds/posts/default'/><link rel='self' type='application/atom+xml' href='http://www.blogger.com/feeds/5573355675806579511/posts/default?max-results=100'/><link rel='alternate' type='text/html' href='http://www.adomokos.com/'/><link rel='hub' href='http://pubsubhubbub.appspot.com/'/><author><name>Attila Domokos</name><uri>http://www.blogger.com/profile/09067995287578229487</uri><email>noreply@blogger.com</email><gd:image rel='http://schemas.google.com/g/2005#thumbnail' width='32' height='32' src='http://4.bp.blogspot.com/-jSMgYgaIqF8/TtJneb7-ZyI/AAAAAAAAB6k/U9Hd923CNvM/s220/adomokos_bw.png'/></author><generator version='7.00' uri='http://www.blogger.com'>Blogger</generator><openSearch:totalResults>20</openSearch:totalResults><openSearch:startIndex>1</openSearch:startIndex><openSearch:itemsPerPage>100</openSearch:itemsPerPage><entry><id>tag:blogger.com,1999:blog-5573355675806579511.post-2941531339952566070</id><published>2012-01-23T20:25:00.004-05:00</published><updated>2012-01-24T23:39:54.871-05:00</updated><category scheme='http://www.blogger.com/atom/ns#' term='JavaScript Mocha'/><title type='text'>JavaScript Testing with Mocha</title><content type='html'>&lt;p&gt;JavaScript is a neat and powerful language. Sure it has its flaws, but serious software can be developed with it. The way I prefer developing JavaScript applications is by test driving the code with some kind of testing tool. And I am not thinking about hitting the browser's refresh button. No, I mean executing the specs &lt;a href="http://www.adomokos.com/2010/11/running-jasmine-bdd-specs-in-terminal.html" target="_blank"&gt;right in the terminal&lt;/a&gt;.&lt;/p&gt;&lt;p&gt;I recently started playing with &lt;a href="https://github.com/visionmedia/mocha" target="_blank"&gt;Visionmedia's mocha&lt;/a&gt; testing framework. The code is well maintained and the authors are responding fairly fast to pull requests or issues.&lt;br&gt;I would recommend it as an alternative to &lt;a href="http://tryjasmine.com/" target="_blank"&gt;Jasmine&lt;/a&gt;.&lt;/p&gt;&lt;p&gt;This blog post will show you the first couple of steps you need to take to test drive your JavaScript code with mocha in the CLI. All my instructions are for OS X, but setting it up should be very similar on Linux and (maybe) on Windows as well.&lt;/p&gt;&lt;p&gt;First of all, you need &lt;a href="http://nodejs.org/" target="_blank"&gt;node.js&lt;/a&gt; to run JavaScript code in the terminal. You can download the source code and compile it yourself, but I'd recommend using &lt;a href="http://mxcl.github.com/homebrew/" target="_blank"&gt;Homebrew&lt;/a&gt; and let it do the job for you.&lt;/p&gt;&lt;pre class="ruby" name="code"&gt;&lt;br /&gt;$: brew install node&lt;br /&gt;&lt;/pre&gt;&lt;p&gt;At the time of this writing my current node version is 0.6.6. You can check your node.js version by running this command:  &lt;/p&gt;&lt;pre class="ruby" name="code"&gt;&lt;br /&gt;$: node -v&lt;br /&gt;v0.6.6&lt;br /&gt;&lt;/pre&gt;&lt;p&gt;Next you need &lt;a href="http://npmjs.org/" target="_blank"&gt;node's package management tool (npm)&lt;/a&gt;. Your version of node may include npm, I list this step here in case it does not. Installing it is super easy, just follow the instructions on their web site and in your terminal.&lt;/p&gt;&lt;p&gt;With these two steps you're ready to roll. Create the project directory, cd into it and start moving in. Create a "src" and a "test" directory. You need to install mocha and &lt;a href="https://github.com/visionmedia/should.js" target="_blank"&gt;should.js&lt;/a&gt; as npm packages. Having &lt;a href="https://github.com/cjohansen/Sinon.JS" target="_blank"&gt;sinon.js&lt;/a&gt; - an excellent spying framework - wouldn't hurt either. Create your spec and source file and you are ready to test drive your app with mocha.&lt;/p&gt;&lt;p&gt;I really wanted to help you - Dear Reader - so I created &lt;a href="https://raw.github.com/gist/1658930/17e51bef9a33e8f39560d118c9e4f1f49e6cabc3/mocha_sample_project_install.sh" target="_blank"&gt;this shell script&lt;/a&gt; to make your life easier.Create a directory, cd into it and run the command below in your terminal:&lt;/p&gt;&lt;p&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&lt;code style="background-color: #EFEFEF; padding: 3px;"&gt;curl -L http://git.io/setup_mocha_project | sh&lt;/code&gt;&lt;/p&gt;&lt;p&gt;If everything goes OK, you will see this:&lt;/p&gt;&lt;p&gt;&lt;div style="padding: 10px;"&gt;create the src directory...&lt;br&gt;create the test directory...&lt;br&gt;write the package.json file...&lt;br&gt;install npm packages...&lt;br&gt;&lt;br&gt;create a sample spec file...&lt;br&gt;create a sample src file...&lt;br&gt;run the spec with mocha...&lt;br&gt;&amp;nbsp;&amp;nbsp;&lt;span style="color: #090"&gt;.&lt;/span&gt;&lt;br&gt;&lt;br&gt;&amp;nbsp;&amp;nbsp;&lt;span style="color: #090"&gt;✔ 1 tests complete (1ms)&lt;/span&gt;&lt;br&gt;&lt;br&gt;run the spec with list reporter...&lt;br&gt;&lt;br&gt;&amp;nbsp;&amp;nbsp;&lt;span style="color: #090"&gt;✓&lt;/span&gt; Person should be able to say hello: 1ms&lt;br&gt;&lt;br&gt;&amp;nbsp;&amp;nbsp;&lt;span style="color: #090"&gt;✔ 1 tests complete (2ms)&lt;/span&gt;&lt;br&gt;&lt;/div&gt;&lt;/p&gt;&lt;p&gt;Let's add one more feature to our Person object. Open up the test/person_spec.js file - it was created by the shell script above - and add the "can say good night" spec:&lt;/p&gt;&lt;pre class="javascript" name="code"&gt;&lt;br /&gt;var should = require('should');&lt;br /&gt;var Person = require(__dirname + '/../src/person');&lt;br /&gt;&lt;br /&gt;describe('Person', function() {&lt;br /&gt;  it('should be able to say hello', function() {&lt;br /&gt;    var Person = global.theApp.Person();&lt;br /&gt;    var personInstance = new Person();&lt;br /&gt;    var message = personInstance.sayHelloTo('adomokos');&lt;br /&gt;&lt;br /&gt;    message.should.equal('Hello, adomokos!');&lt;br /&gt;  });&lt;br /&gt;&lt;br /&gt;  // Add this spec&lt;br /&gt;  it('can say good night', function() {&lt;br /&gt;    var Person = global.theApp.Person();&lt;br /&gt;    var personInstance = new Person();&lt;br /&gt;    var message = personInstance.sayGoodNight();&lt;br /&gt;&lt;br /&gt;    message.should.equal('Good night!');&lt;br /&gt;  });&lt;br /&gt;});&lt;br /&gt;&lt;/pre&gt;&lt;p&gt;Run the mocha specs with this command:&lt;/p&gt;&lt;pre class="ruby" name="code"&gt;&lt;br /&gt;$: ./node_modules/mocha/bin/mocha&lt;br /&gt;&lt;/pre&gt;&lt;p&gt;The error is obvious: the Person object does not yet have the method "sayGoodNight".&lt;/p&gt;&lt;p&gt;&amp;nbsp;&amp;nbsp;.&lt;span style="color: #900"&gt;.&lt;/span&gt;&lt;br&gt;&lt;br&gt;&amp;nbsp;&amp;nbsp;&lt;span style="color: #900"&gt;✖ 1 of 2 tests failed&lt;/span&gt;:&lt;br&gt;&lt;br&gt;&amp;nbsp;&amp;nbsp;1) Person can say good night:&lt;br&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&lt;span style="color: #900"&gt;TypeError: Object [object Object] has no method 'sayGoodNight'&lt;/span&gt;&lt;br&gt;&lt;/p&gt;&lt;p&gt;Let's fix it by adding the missing method to the Person object:&lt;/p&gt;&lt;pre class="javascript" name="code"&gt;&lt;br /&gt;global.theApp = {};&lt;br /&gt;&lt;br /&gt;global.theApp.Person = function() {&lt;br /&gt;&lt;br /&gt;  var Person = function() {&lt;br /&gt;   this.sayHelloTo = function(anotherPerson) {&lt;br /&gt;      return 'Hello, ' + anotherPerson + '!';&lt;br /&gt;    };&lt;br /&gt;&lt;br /&gt;   // Add this method&lt;br /&gt;   this.sayGoodNight = function() {&lt;br /&gt;     return 'Good night!';&lt;br /&gt;   };&lt;br /&gt;  };&lt;br /&gt;&lt;br /&gt;  return Person;&lt;br /&gt;&lt;br /&gt;};&lt;br /&gt;&lt;/pre&gt;&lt;p&gt;When I run the specs again, they all pass.&lt;/p&gt;&lt;p&gt;&amp;nbsp;&amp;nbsp;&lt;span style="color: #090"&gt;..&lt;/span&gt;&lt;br&gt;&lt;br&gt;&amp;nbsp;&amp;nbsp;&lt;span style="color: #090"&gt;✔ 2 tests complete (2ms)&lt;/span&gt;&lt;br&gt;&lt;/p&gt;&lt;p&gt;You can try other reporters as well. The "list" reporter will give you the documentation text:&lt;/p&gt;&lt;pre class="ruby" name="code"&gt;&lt;br /&gt;$: ./node_modules/mocha/bin/mocha -R list&lt;br /&gt;&lt;/pre&gt;&lt;p&gt;Try the &lt;a href="http://twitpic.com/88zi7h/full" target="_blank"&gt;landing reporter&lt;/a&gt;, I found its output unexpected but really cool!&lt;/p&gt;&lt;pre class="ruby" name="code"&gt;&lt;br /&gt;$: ./node_modules/mocha/bin/mocha -R landing&lt;br /&gt;&lt;/pre&gt;&lt;p&gt;&lt;span style="text-decoration: underline;"&gt;The steps once more:&lt;/span&gt;&lt;ol&gt;&lt;li&gt;Make sure you have node.js installed&lt;/li&gt;&lt;li&gt;Check for npm as well&lt;/li&gt;&lt;li&gt;Create your project directory and cd into it&lt;/li&gt;&lt;li&gt;Run this script $: &lt;code style="background-color: #EFEFEF; padding: 3px;"&gt;curl -L http://git.io/setup_mocha_project | sh&lt;/code&gt;&lt;/li&gt;&lt;li&gt;Execute the specs with $: &lt;code style="background-color: #EFEFEF; padding: 3px;"&gt;node_modules/mocha/bin/mocha&lt;/code&gt;&lt;/ol&gt;And I almost forgot: mocha will pick up CoffeeScript files as well.&lt;/p&gt;&lt;p&gt;Enjoy!&lt;/p&gt;&lt;p&gt;&amp;nbsp;&lt;/p&gt;&lt;p&gt;::: Update (01/24/2012):&lt;br&gt;I asked &lt;a href="http://tjholowaychuk.com/" target="_blank"&gt;TJ Holowaychuck&lt;/a&gt;, the author of Mocha of what thoughts he had on my blog post. He recommended adding a "test" script to the package.json file making it easier to run the specs. &lt;a href="https://gist.github.com/1658930" target="_blank"&gt;I made that change&lt;/a&gt;: &lt;code style="background-color: #EFEFEF; padding: 3px;"&gt;npm test&lt;/code&gt; executed in the terminal should run all your specs under the test directory.&lt;/p&gt;&lt;div class="blogger-post-footer"&gt;&lt;img width='1' height='1' src='https://blogger.googleusercontent.com/tracker/5573355675806579511-2941531339952566070?l=www.adomokos.com' alt='' /&gt;&lt;/div&gt;</content><link rel='replies' type='application/atom+xml' href='http://www.adomokos.com/feeds/2941531339952566070/comments/default' title='Post Comments'/><link rel='replies' type='text/html' href='http://www.adomokos.com/2012/01/javascript-testing-with-mocha.html#comment-form' title='3 Comments'/><link rel='edit' type='application/atom+xml' href='http://www.blogger.com/feeds/5573355675806579511/posts/default/2941531339952566070'/><link rel='self' type='application/atom+xml' href='http://www.blogger.com/feeds/5573355675806579511/posts/default/2941531339952566070'/><link rel='alternate' type='text/html' href='http://www.adomokos.com/2012/01/javascript-testing-with-mocha.html' title='JavaScript Testing with Mocha'/><author><name>Attila Domokos</name><uri>http://www.blogger.com/profile/09067995287578229487</uri><email>noreply@blogger.com</email><gd:image rel='http://schemas.google.com/g/2005#thumbnail' width='32' height='32' src='http://4.bp.blogspot.com/-jSMgYgaIqF8/TtJneb7-ZyI/AAAAAAAAB6k/U9Hd923CNvM/s220/adomokos_bw.png'/></author><thr:total>3</thr:total></entry><entry><id>tag:blogger.com,1999:blog-5573355675806579511.post-2421992650247272014</id><published>2012-01-02T22:04:00.029-05:00</published><updated>2012-01-02T22:52:49.760-05:00</updated><title type='text'>The Tic-Tac-Toe Game</title><content type='html'>&lt;p&gt;I've been pretty busy lately working on &lt;a href="https://github.com/adomokos/TicTacToe-Game" target="_blank"&gt;this Tic-Tac-Toe game&lt;/a&gt;. It all started as a project to learn CoffeeScript,  Backbone.js and turned into a big journey into JavaScript and node.js.&lt;/p&gt;&lt;p&gt;I am test-driving the code with &lt;a href="https://github.com/mhevery/jasmine-node" target="_blank"&gt;jasmine-node&lt;/a&gt;, a great node adapter to &lt;a href="http://pivotal.github.com/jasmine/" target="_blank"&gt;Jasmine BDD&lt;/a&gt;.&lt;br /&gt;The computer's moves are quite predictable, I will work on that in the future.&lt;/p&gt;&lt;div id="tic-tac-toe_game"&gt;&lt;style&gt;html, body {  /*height: 100%;*/  margin: 0;  padding: 0;}body { margin: 0; padding: 0;  font-family: Verdana;  font-size: 14px;}table {  padding-left: 20px;  padding-top: 20px;  line-height: normal;}td {  font-size: 16px;  width: 30px;  height: 30px;  margin: 1px;  background-color: Silver;  text-align: center;  vertical-align: middle;  padding-top: 4px;}#container {  /*overflow: hidden;*/  width: 250px;  margin: 0 auto;}#game_board {  float: left;  width: 120px;}#results {  float: right;  width: 100px;  padding-top: 20px;}#footer {  clear: both;  text-align: left;}#result {  padding-top: 10px;  margin-left: 20px;  /*float: left;*/}.result {  display: none;}.won {  color: #090;}.lost {  color: #900;}.tie {  color: #00C;}#restart_container {  display: none;  padding-top: 5px;  padding-left: 20px;}#restart_container a:link, a:visited, a:active {  text-decoration: none;  font-style: italic;  color: #686868;}#restart_container a:hover {  text-decoration: none;  color: #000;}&lt;/style&gt;&lt;script src="//ajax.googleapis.com/ajax/libs/jquery/1.7.1/jquery.min.js" type="text/javascript"&gt;&lt;/script&gt;&lt;script type='text/javascript'&gt;//&lt;![CDATA[// Underscore.js(function(){var l=this,z=l._,g={},f=Array.prototype,j=Object.prototype,i=f.slice,A=f.unshift,B=j.toString,n=j.hasOwnProperty,v=f.forEach,r=f.map,w=f.reduce,s=f.reduceRight,x=f.filter,t=f.every,y=f.some,p=f.indexOf,u=f.lastIndexOf,j=Array.isArray,q=Object.keys,a=function(a){return new m(a)};"undefined"!==typeof module&amp;&amp;module.exports?(module.exports=a,a._=a):l._=a;a.VERSION="1.1.4";var b=a.each=a.forEach=function(d,b,k){if(null!=d)if(v&amp;&amp;d.forEach===v)d.forEach(b,k);else if(a.isNumber(d.length))for(var c=0,e=d.length;c&lt;e&amp;&amp;!(b.call(k,d[c],c,d)===g);c++);else for(c in d)if(n.call(d,c)&amp;&amp;b.call(k,d[c],c,d)===g)break};a.map=function(a,o,c){var h=[];if(null==a)return h;if(r&amp;&amp;a.map===r)return a.map(o,c);b(a,function(a,d,b){h[h.length]=o.call(c,a,d,b)});return h};a.reduce=a.foldl=a.inject=function(d,o,c,h){var e=void 0!==c;null==d&amp;&amp;(d=[]);if(w&amp;&amp;d.reduce===w)return h&amp;&amp;(o=a.bind(o,h)),e?d.reduce(o,c):d.reduce(o);b(d,function(a,d,b){!e&amp;&amp;0===d?(c=a,e=!0):c=o.call(h,c,a,d,b)});if(!e)throw new TypeError("Reduce of empty array with no initial value");return c};a.reduceRight=a.foldr=function(d,b,c,h){null==d&amp;&amp;(d=[]);if(s&amp;&amp;d.reduceRight===s)return h&amp;&amp;(b=a.bind(b,h)),void 0!==c?d.reduceRight(b,c):d.reduceRight(b);d=(a.isArray(d)?d.slice():a.toArray(d)).reverse();return a.reduce(d,b,c,h)};a.find=a.detect=function(a,b,k){var h;c(a,function(a,d,c){if(b.call(k,a,d,c))return h=a,!0});return h};a.filter=a.select=function(a,c,k){var h=[];if(null==a)return h;if(x&amp;&amp;a.filter===x)return a.filter(c,k);b(a,function(a,d,b){c.call(k,a,d,b)&amp;&amp;(h[h.length]=a)});return h};a.reject=function(a,c,k){var h=[];if(null==a)return h;b(a,function(a,d,b){c.call(k,a,d,b)||(h[h.length]=a)});return h};a.every=a.all=function(d,c,k){var c=c||a.identity,h=!0;if(null==d)return h;if(t&amp;&amp;d.every===t)return d.every(c,k);b(d,function(a,d,b){if(!(h=h&amp;&amp;c.call(k,a,d,b)))return g});return h};var c=a.some=a.any=function(d,c,k){var c=c||a.identity,h=!1;if(null==d)return h;if(y&amp;&amp;d.some===y)return d.some(c,k);b(d,function(a,d,b){if(h=c.call(k,a,d,b))return g});return h};a.include=a.contains=function(a,b){var k=!1;if(null==a)return k;if(p&amp;&amp;a.indexOf===p)return-1!=a.indexOf(b);c(a,function(a){if(k=a===b)return!0});return k};a.invoke=function(d,b){var c=i.call(arguments,2);return a.map(d,function(a){return(b?a[b]:a).apply(a,c)})};a.pluck=function(d,b){return a.map(d,function(a){return a[b]})};a.max=function(d,c,k){if(!c&amp;&amp;a.isArray(d))return Math.max.apply(Math,d);var h={computed:-Infinity};b(d,function(a,d,b){d=c?c.call(k,a,d,b):a;d&gt;=h.computed&amp;&amp;(h={value:a,computed:d})});return h.value};a.min=function(d,c,k){if(!c&amp;&amp;a.isArray(d))return Math.min.apply(Math,d);var h={computed:Infinity};b(d,function(a,d,b){d=c?c.call(k,a,d,b):a;d&lt;h.computed&amp;&amp;(h={value:a,computed:d})});return h.value};a.sortBy=function(d,b,c){return a.pluck(a.map(d,function(a,d,e){return{value:a,criteria:b.call(c,a,d,e)}}).sort(function(a,d){var b=a.criteria,c=d.criteria;return b&lt;c?-1:b&gt;c?1:0}),"value")};a.sortedIndex=function(d,b,c){for(var c=c||a.identity,e=0,f=d.length;e&lt;f;){var g=e+f&gt;&gt;1;c(d[g])&lt;c(b)?e=g+1:f=g}return e};a.toArray=function(d){return!d?[]:d.toArray?d.toArray():a.isArray(d)?d:a.isArguments(d)?i.call(d):a.values(d)};a.size=function(d){return a.toArray(d).length};a.first=a.head=function(a,b,c){return b&amp;&amp;!c?i.call(a,0,b):a[0]};a.rest=a.tail=function(d,b,c){return i.call(d,a.isUndefined(b)||c?1:b)};a.last=function(a){return a[a.length-1]};a.compact=function(d){return a.filter(d,function(a){return!!a})};a.flatten=function(d){return a.reduce(d,function(d,b){if(a.isArray(b))return d.concat(a.flatten(b));d[d.length]=b;return d},[])};a.without=function(d){var b=i.call(arguments,1);return a.filter(d,function(d){return!a.include(b,d)})};a.uniq=a.unique=function(d,b){return a.reduce(d,function(d,c,e){if(0==e||(!0===b?a.last(d)!=c:!a.include(d,c)))d[d.length]=c;return d},[])};a.intersect=function(d){var b=i.call(arguments,1);return a.filter(a.uniq(d),function(d){return a.every(b,function(b){return 0&lt;=a.indexOf(b,d)})})};a.zip=function(){for(var d=i.call(arguments),b=a.max(a.pluck(d,"length")),c=Array(b),e=0;e&lt;b;e++)c[e]=a.pluck(d,""+e);return c};a.indexOf=function(d,b,c){if(null==d)return-1;if(c)return c=a.sortedIndex(d,b),d[c]===b?c:-1;if(p&amp;&amp;d.indexOf===p)return d.indexOf(b);for(var c=0,e=d.length;c&lt;e;c++)if(d[c]===b)return c;return-1};a.lastIndexOf=function(a,b){if(null==a)return-1;if(u&amp;&amp;a.lastIndexOf===u)return a.lastIndexOf(b);for(var c=a.length;c--;)if(a[c]===b)return c;return-1};a.range=function(a,b,c){for(var e=i.call(arguments),f=1&gt;=e.length,a=f?0:e[0],b=f?e[0]:e[1],c=e[2]||1,e=Math.max(Math.ceil((b-a)/c),0),f=0,g=Array(e);f&lt;e;)g[f++]=a,a+=c;return g};a.bind=function(a,b){var c=i.call(arguments,2);return function(){return a.apply(b||{},c.concat(i.call(arguments)))}};a.bindAll=function(d){var c=i.call(arguments,1);0==c.length&amp;&amp;(c=a.functions(d));b(c,function(b){d[b]=a.bind(d[b],d)});return d};a.memoize=function(b,c){var e={},c=c||a.identity;return function(){var a=c.apply(this,arguments);return a in e?e[a]:e[a]=b.apply(this,arguments)}};a.delay=function(a,b){var c=i.call(arguments,2);return setTimeout(function(){return a.apply(a,c)},b)};a.defer=function(b){return a.delay.apply(a,[b,1].concat(i.call(arguments,1)))};var e=function(a,b,c){var e;return function(){var f=this,g=arguments,m=function(){e=null;a.apply(f,g)};c&amp;&amp;clearTimeout(e);if(c||!e)e=setTimeout(m,b)}};a.throttle=function(a,b){return e(a,b,!1)};a.debounce=function(a,b){return e(a,b,!0)};a.wrap=function(a,b){return function(){var c=[a].concat(i.call(arguments));return b.apply(this,c)}};a.compose=function(){var a=i.call(arguments);return function(){for(var b=i.call(arguments),c=a.length-1;0&lt;=c;c--)b=[a[c].apply(this,b)];return b[0]}};a.keys=q||function(b){if(a.isArray(b))return a.range(0,b.length);var c=[],e;for(e in b)n.call(b,e)&amp;&amp;(c[c.length]=e);return c};a.values=function(b){return a.map(b,a.identity)};a.functions=a.methods=function(b){return a.filter(a.keys(b),function(c){return a.isFunction(b[c])}).sort()};a.extend=function(a){b(i.call(arguments,1),function(b){for(var c in b)a[c]=b[c]});return a};a.clone=function(b){return a.isArray(b)?b.slice():a.extend({},b)};a.tap=function(a,b){b(a);return a};a.isEqual=function(b,c){if(b===c)return!0;var e=typeof b;if(e!=typeof c)return!1;if(b==c)return!0;if(!b&amp;&amp;c||b&amp;&amp;!c)return!1;if(b._chain)b=b._wrapped;if(c._chain)c=c._wrapped;if(b.isEqual)return b.isEqual(c);if(a.isDate(b)&amp;&amp;a.isDate(c))return b.getTime()===c.getTime();if(a.isNaN(b)&amp;&amp;a.isNaN(c))return!1;if(a.isRegExp(b)&amp;&amp;a.isRegExp(c))return b.source===c.source&amp;&amp;b.global===c.global&amp;&amp;b.ignoreCase===c.ignoreCase&amp;&amp;b.multiline===c.multiline;if("object"!==e)return!1;if(b.length&amp;&amp;b.length!==c.length)return!1;var e=a.keys(b),f=a.keys(c);if(e.length!=f.length)return!1;for(var g in b)if(!(g in c)||!a.isEqual(b[g],c[g]))return!1;return!0};a.isEmpty=function(b){if(a.isArray(b)||a.isString(b))return 0===b.length;for(var c in b)if(n.call(b,c))return!1;return!0};a.isElement=function(a){return!!(a&amp;&amp;1==a.nodeType)};a.isArray=j||function(a){return"[object Array]"===B.call(a)};a.isArguments=function(a){return!(!a||!n.call(a,"callee"))};a.isFunction=function(a){return!(!a||!a.constructor||!a.call||!a.apply)};a.isString=function(a){return!!(""===a||a&amp;&amp;a.charCodeAt&amp;&amp;a.substr)};a.isNumber=function(a){return!!(0===a||a&amp;&amp;a.toExponential&amp;&amp;a.toFixed)};a.isNaN=function(a){return a!==a};a.isBoolean=function(a){return!0===a||!1===a};a.isDate=function(a){return!(!a||!a.getTimezoneOffset||!a.setUTCFullYear)};a.isRegExp=function(a){return!(!a||!a.test||!a.exec||!(a.ignoreCase||!1===a.ignoreCase))};a.isNull=function(a){return null===a};a.isUndefined=function(a){return void 0===a};a.noConflict=function(){l._=z;return this};a.identity=function(a){return a};a.times=function(a,b,c){for(var e=0;e&lt;a;e++)b.call(c,e)};a.mixin=function(c){b(a.functions(c),function(b){D(b,a[b]=c[b])})};var E=0;a.uniqueId=function(a){var b=E++;return a?a+b:b};a.templateSettings={evaluate:/&lt;%([\s\S]+?)%&gt;/g,interpolate:/&lt;%=([\s\S]+?)%&gt;/g};a.template=function(b,c){var e=a.templateSettings,e="var __p=[],print=function(){__p.push.apply(__p,arguments);};with(obj||{}){__p.push('"+b.replace(/\\/g,"\\\\").replace(/'/g,"\\'").replace(e.interpolate,function(a,b){return"',"+b.replace(/\\'/g,"'")+",'"}).replace(e.evaluate||null,function(a,b){return"');"+b.replace(/\\'/g,"'").replace(/[\r\n\t]/g," ")+"__p.push('"}).replace(/\r/g,"\\r").replace(/\n/g,"\\n").replace(/\t/g,"\\t")+"');}return __p.join('');",e=new Function("obj",e);return c?e(c):e};var m=function(a){this._wrapped=a};a.prototype=m.prototype;var C=function(b,c){return c?a(b).chain():b},D=function(b,c){m.prototype[b]=function(){var b=i.call(arguments);A.call(b,this._wrapped);return C(c.apply(a,b),this._chain)}};a.mixin(a);b("pop,push,reverse,shift,sort,splice,unshift".split(","),function(a){var b=f[a];m.prototype[a]=function(){b.apply(this._wrapped,arguments);return C(this._wrapped,this._chain)}});b(["concat","join","slice"],function(a){var b=f[a];m.prototype[a]=function(){return C(b.apply(this._wrapped,arguments),this._chain)}});m.prototype.chain=function(){this._chain=!0;return this};m.prototype.value=function(){return this._wrapped}})();(function(){var l=this,z=l.Backbone,g;g="undefined"!==typeof exports?exports:l.Backbone={};g.VERSION="0.5.1";var f=l._;if(!f&amp;&amp;"undefined"!==typeof require)f=require("underscore")._;var j=l.jQuery||l.Zepto;g.noConflict=function(){l.Backbone=z;return this};g.emulateHTTP=!1;g.emulateJSON=!1;g.Events={bind:function(a,b){var c=this._callbacks||(this._callbacks={});(c[a]||(c[a]=[])).push(b);return this},unbind:function(a,b){var c;if(a){if(c=this._callbacks)if(b){c=c[a];if(!c)return this;for(var e=0,f=c.length;e&lt;f;e++)if(b===c[e]){c[e]=null;break}}else c[a]=[]}else this._callbacks={};return this},trigger:function(a){var b,c,e,f,g=2;if(!(c=this._callbacks))return this;for(;g--;)if(b=g?a:"all",b=c[b])for(var i=0,j=b.length;i&lt;j;i++)(e=b[i])?(f=g?Array.prototype.slice.call(arguments,1):arguments,e.apply(this,f)):(b.splice(i,1),i--,j--);return this}};g.Model=function(a,b){var c;a||(a={});if(c=this.defaults)f.isFunction(c)&amp;&amp;(c=c()),a=f.extend({},c,a);this.attributes={};this._escapedAttributes={};this.cid=f.uniqueId("c");this.set(a,{silent:!0});this._changed=!1;this._previousAttributes=f.clone(this.attributes);if(b&amp;&amp;b.collection)this.collection=b.collection;this.initialize(a,b)};f.extend(g.Model.prototype,g.Events,{_previousAttributes:null,_changed:!1,idAttribute:"id",initialize:function(){},toJSON:function(){return f.clone(this.attributes)},get:function(a){return this.attributes[a]},escape:function(a){var b;if(b=this._escapedAttributes[a])return b;b=this.attributes[a];return this._escapedAttributes[a]=(null==b?"":""+b).replace(/&amp;(?!\w+;|#\d+;|#x[\da-f]+;)/gi,"&amp;amp;").replace(/&lt;/g,"&amp;lt;").replace(/&gt;/g,"&amp;gt;").replace(/"/g,"&amp;quot;").replace(/'/g,"&amp;#x27").replace(/\//g,"&amp;#x2F;")},has:function(a){return null!=this.attributes[a]},set:function(a,b){b||(b={});if(!a)return this;if(a.attributes)a=a.attributes;var c=this.attributes,e=this._escapedAttributes;if(!b.silent&amp;&amp;this.validate&amp;&amp;!this._performValidation(a,b))return!1;if(this.idAttribute in a)this.id=a[this.idAttribute];var g=this._changing;this._changing=!0;for(var i in a){var j=a[i];if(!f.isEqual(c[i],j))c[i]=j,delete e[i],this._changed=!0,b.silent||this.trigger("change:"+i,this,j,b)}!g&amp;&amp;!b.silent&amp;&amp;this._changed&amp;&amp;this.change(b);this._changing=!1;return this},unset:function(a,b){if(!(a in this.attributes))return this;b||(b={});var c={};c[a]=void 0;if(!b.silent&amp;&amp;this.validate&amp;&amp;!this._performValidation(c,b))return!1;delete this.attributes[a];delete this._escapedAttributes[a];a==this.idAttribute&amp;&amp;delete this.id;this._changed=!0;b.silent||(this.trigger("change:"+a,this,void 0,b),this.change(b));return this},clear:function(a){a||(a={});var b,c=this.attributes,e={};for(b in c)e[b]=void 0;if(!a.silent&amp;&amp;this.validate&amp;&amp;!this._performValidation(e,a))return!1;this.attributes={};this._escapedAttributes={};this._changed=!0;if(!a.silent){for(b in c)this.trigger("change:"+b,this,void 0,a);this.change(a)}return this},fetch:function(a){a||(a={});var b=this,c=a.success;a.success=function(e,f,g){if(!b.set(b.parse(e,g),a))return!1;c&amp;&amp;c(b,e)};a.error=q(a.error,b,a);return(this.sync||g.sync).call(this,"read",this,a)},save:function(a,b){b||(b={});if(a&amp;&amp;!this.set(a,b))return!1;var c=this,e=b.success;b.success=function(a,f,g){if(!c.set(c.parse(a,g),b))return!1;e&amp;&amp;e(c,a,g)};b.error=q(b.error,c,b);var f=this.isNew()?"create":"update";return(this.sync||g.sync).call(this,f,this,b)},destroy:function(a){a||(a={});if(this.isNew())return this.trigger("destroy",this,this.collection,a);var b=this,c=a.success;a.success=function(e){b.trigger("destroy",b,b.collection,a);c&amp;&amp;c(b,e)};a.error=q(a.error,b,a);return(this.sync||g.sync).call(this,"delete",this,a)},url:function(){var a=p(this.collection)||this.urlRoot||u();return this.isNew()?a:a+("/"==a.charAt(a.length-1)?"":"/")+encodeURIComponent(this.id)},parse:function(a){return a},clone:function(){return new this.constructor(this)},isNew:function(){return null==this.id},change:function(a){this.trigger("change",this,a);this._previousAttributes=f.clone(this.attributes);this._changed=!1},hasChanged:function(a){return a?this._previousAttributes[a]!=this.attributes[a]:this._changed},changedAttributes:function(a){a||(a=this.attributes);var b=this._previousAttributes,c=!1,e;for(e in a)f.isEqual(b[e],a[e])||(c=c||{},c[e]=a[e]);return c},previous:function(a){return!a||!this._previousAttributes?null:this._previousAttributes[a]},previousAttributes:function(){return f.clone(this._previousAttributes)},_performValidation:function(a,b){var c=this.validate(a);return c?(b.error?b.error(this,c,b):this.trigger("error",this,c,b),!1):!0}});g.Collection=function(a,b){b||(b={});if(b.comparator)this.comparator=b.comparator;f.bindAll(this,"_onModelEvent","_removeReference");this._reset();a&amp;&amp;this.reset(a,{silent:!0});this.initialize.apply(this,arguments)};f.extend(g.Collection.prototype,g.Events,{model:g.Model,initialize:function(){},toJSON:function(){return this.map(function(a){return a.toJSON()})},add:function(a,b){if(f.isArray(a))for(var c=0,e=a.length;c&lt;e;c++)this._add(a[c],b);else this._add(a,b);return this},remove:function(a,b){if(f.isArray(a))for(var c=0,e=a.length;c&lt;e;c++)this._remove(a[c],b);else this._remove(a,b);return this},get:function(a){return null==a?null:this._byId[null!=a.id?a.id:a]},getByCid:function(a){return a&amp;&amp;this._byCid[a.cid||a]},at:function(a){return this.models[a]},sort:function(a){a||(a={});if(!this.comparator)throw Error("Cannot sort a set without a comparator");this.models=this.sortBy(this.comparator);a.silent||this.trigger("reset",this,a);return this},pluck:function(a){return f.map(this.models,function(b){return b.get(a)})},reset:function(a,b){a||(a=[]);b||(b={});this.each(this._removeReference);this._reset();this.add(a,{silent:!0});b.silent||this.trigger("reset",this,b);return this},fetch:function(a){a||(a={});var b=this,c=a.success;a.success=function(e,f,g){b[a.add?"add":"reset"](b.parse(e,g),a);c&amp;&amp;c(b,e)};a.error=q(a.error,b,a);return(this.sync||g.sync).call(this,"read",this,a)},create:function(a,b){var c=this;b||(b={});a=this._prepareModel(a,b);if(!a)return!1;var e=b.success;b.success=function(a,f,g){c.add(a,b);e&amp;&amp;e(a,f,g)};a.save(null,b);return a},parse:function(a){return a},chain:function(){return f(this.models).chain()},_reset:function(){this.length=0;this.models=[];this._byId={};this._byCid={}},_prepareModel:function(a,b){if(a instanceof g.Model){if(!a.collection)a.collection=this}else{var c=a,a=new this.model(c,{collection:this});a.validate&amp;&amp;!a._performValidation(c,b)&amp;&amp;(a=!1)}return a},_add:function(a,b){b||(b={});a=this._prepareModel(a,b);if(!a)return!1;var c=this.getByCid(a)||this.get(a);if(c)throw Error(["Can't add the same model to a set twice",c.id]);this._byId[a.id]=a;this._byCid[a.cid]=a;this.models.splice(null!=b.at?b.at:this.comparator?this.sortedIndex(a,this.comparator):this.length,0,a);a.bind("all",this._onModelEvent);this.length++;b.silent||a.trigger("add",a,this,b);return a},_remove:function(a,b){b||(b={});a=this.getByCid(a)||this.get(a);if(!a)return null;delete this._byId[a.id];delete this._byCid[a.cid];this.models.splice(this.indexOf(a),1);this.length--;b.silent||a.trigger("remove",a,this,b);this._removeReference(a);return a},_removeReference:function(a){this==a.collection&amp;&amp;delete a.collection;a.unbind("all",this._onModelEvent)},_onModelEvent:function(a,b,c,e){("add"==a||"remove"==a)&amp;&amp;c!=this||("destroy"==a&amp;&amp;this._remove(b,e),b&amp;&amp;a==="change:"+b.idAttribute&amp;&amp;(delete this._byId[b.previous(b.idAttribute)],this._byId[b.id]=b),this.trigger.apply(this,arguments))}});f.each("forEach,each,map,reduce,reduceRight,find,detect,filter,select,reject,every,all,some,any,include,contains,invoke,max,min,sortBy,sortedIndex,toArray,size,first,rest,last,without,indexOf,lastIndexOf,isEmpty".split(","),function(a){g.Collection.prototype[a]=function(){return f[a].apply(f,[this.models].concat(f.toArray(arguments)))}});g.Router=function(a){a||(a={});if(a.routes)this.routes=a.routes;this._bindRoutes();this.initialize.apply(this,arguments)};var i=/:([\w\d]+)/g,A=/\*([\w\d]+)/g,B=/[-[\]{}()+?.,\\^$|#\s]/g;f.extend(g.Router.prototype,g.Events,{initialize:function(){},route:function(a,b,c){g.history||(g.history=new g.History);f.isRegExp(a)||(a=this._routeToRegExp(a));g.history.route(a,f.bind(function(e){e=this._extractParameters(a,e);c.apply(this,e);this.trigger.apply(this,["route:"+b].concat(e))},this))},navigate:function(a,b){g.history.navigate(a,b)},_bindRoutes:function(){if(this.routes){var a=[],b;for(b in this.routes)a.unshift([b,this.routes[b]]);b=0;for(var c=a.length;b&lt;c;b++)this.route(a[b][0],a[b][1],this[a[b][1]])}},_routeToRegExp:function(a){a=a.replace(B,"\\$&amp;").replace(i,"([^/]*)").replace(A,"(.*?)");return RegExp("^"+a+"$")},_extractParameters:function(a,b){return a.exec(b).slice(1)}});g.History=function(){this.handlers=[];f.bindAll(this,"checkUrl")};var n=/^#*/,v=/msie [\w.]+/,r=!1;f.extend(g.History.prototype,{interval:50,getFragment:function(a,b){if(null==a)if(this._hasPushState||b){var a=window.location.pathname,c=window.location.search;c&amp;&amp;(a+=c);0==a.indexOf(this.options.root)&amp;&amp;(a=a.substr(this.options.root.length))}else a=window.location.hash;return a.replace(n,"")},start:function(a){if(r)throw Error("Backbone.history has already been started");this.options=f.extend({},{root:"/"},this.options,a);this._wantsPushState=!!this.options.pushState;this._hasPushState=!(!this.options.pushState||!window.history||!window.history.pushState);var a=this.getFragment(),b=document.documentMode;if(b=v.exec(navigator.userAgent.toLowerCase())&amp;&amp;(!b||7&gt;=b))this.iframe=j('&lt;iframe src="javascript:0" tabindex="-1" /&gt;').hide().appendTo("body")[0].contentWindow,this.navigate(a);this._hasPushState?j(window).bind("popstate",this.checkUrl):"onhashchange"in window&amp;&amp;!b?j(window).bind("hashchange",this.checkUrl):setInterval(this.checkUrl,this.interval);this.fragment=a;r=!0;a=window.location;b=a.pathname==this.options.root;if(this._wantsPushState&amp;&amp;!this._hasPushState&amp;&amp;!b)this.fragment=this.getFragment(null,!0),window.location.replace(this.options.root+"#"+this.fragment);else if(this._wantsPushState&amp;&amp;this._hasPushState&amp;&amp;b&amp;&amp;a.hash)this.fragment=a.hash.replace(n,""),window.history.replaceState({},document.title,a.protocol+"//"+a.host+this.options.root+this.fragment);return this.loadUrl()},route:function(a,b){this.handlers.unshift({route:a,callback:b})},checkUrl:function(){var a=this.getFragment();a==this.fragment&amp;&amp;this.iframe&amp;&amp;(a=this.getFragment(this.iframe.location.hash));if(a==this.fragment||a==decodeURIComponent(this.fragment))return!1;this.iframe&amp;&amp;this.navigate(a);this.loadUrl()||this.loadUrl(window.location.hash)},loadUrl:function(a){var b=this.fragment=this.getFragment(a);return f.any(this.handlers,function(a){if(a.route.test(b))return a.callback(b),!0})},navigate:function(a,b){var c=(a||"").replace(n,"");if(!(this.fragment==c||this.fragment==decodeURIComponent(c))){if(this._hasPushState){var e=window.location;0!=c.indexOf(this.options.root)&amp;&amp;(c=this.options.root+c);this.fragment=c;window.history.pushState({},document.title,e.protocol+"//"+e.host+c)}else if(window.location.hash=this.fragment=c,this.iframe&amp;&amp;c!=this.getFragment(this.iframe.location.hash))this.iframe.document.open().close(),this.iframe.location.hash=c;b&amp;&amp;this.loadUrl(a)}}});g.View=function(a){this.cid=f.uniqueId("view");this._configure(a||{});this._ensureElement();this.delegateEvents();this.initialize.apply(this,arguments)};var w=/^(\S+)\s*(.*)$/,s="model,collection,el,id,attributes,className,tagName".split(",");f.extend(g.View.prototype,g.Events,{tagName:"div",$:function(a){return j(a,this.el)},initialize:function(){},render:function(){return this},remove:function(){j(this.el).remove();return this},make:function(a,b,c){a=document.createElement(a);b&amp;&amp;j(a).attr(b);c&amp;&amp;j(a).html(c);return a},delegateEvents:function(a){if(a||(a=this.events)){j(this.el).unbind(".delegateEvents"+this.cid);for(var b in a){var c=this[a[b]];if(!c)throw Error('Event "'+a[b]+'" does not exist');var e=b.match(w),g=e[1],e=e[2],c=f.bind(c,this),g=g+(".delegateEvents"+this.cid);""===e?j(this.el).bind(g,c):j(this.el).delegate(e,g,c)}}},_configure:function(a){this.options&amp;&amp;(a=f.extend({},this.options,a));for(var b=0,c=s.length;b&lt;c;b++){var e=s[b];a[e]&amp;&amp;(this[e]=a[e])}this.options=a},_ensureElement:function(){if(this.el){if(f.isString(this.el))this.el=j(this.el).get(0)}else{var a=this.attributes||{};if(this.id)a.id=this.id;if(this.className)a["class"]=this.className;this.el=this.make(this.tagName,a)}}});g.Model.extend=g.Collection.extend=g.Router.extend=g.View.extend=function(a,b){var c=y(this,a,b);c.extend=this.extend;return c};var x={create:"POST",update:"PUT","delete":"DELETE",read:"GET"};g.sync=function(a,b,c){var e=x[a],c=f.extend({type:e,dataType:"json",processData:!1},c);if(!c.url)c.url=p(b)||u();if(!c.data&amp;&amp;b&amp;&amp;("create"==a||"update"==a))c.contentType="application/json",c.data=JSON.stringify(b.toJSON());if(g.emulateJSON)c.contentType="application/x-www-form-urlencoded",c.processData=!0,c.data=c.data?{model:c.data}:{};if(g.emulateHTTP&amp;&amp;("PUT"===e||"DELETE"===e)){if(g.emulateJSON)c.data._method=e;c.type="POST";c.beforeSend=function(a){a.setRequestHeader("X-HTTP-Method-Override",e)}}return j.ajax(c)};var t=function(){},y=function(a,b,c){var e;e=b&amp;&amp;b.hasOwnProperty("constructor")?b.constructor:function(){return a.apply(this,arguments)};f.extend(e,a);t.prototype=a.prototype;e.prototype=new t;b&amp;&amp;f.extend(e.prototype,b);c&amp;&amp;f.extend(e,c);e.prototype.constructor=e;e.__super__=a.prototype;return e},p=function(a){return!a||!a.url?null:f.isFunction(a.url)?a.url():a.url},u=function(){throw Error('A "url" property or function must be specified');},q=function(a,b,c){return function(e){a?a(b,e,c):b.trigger("error",b,e,c)}}}).call(this);// Backbone.js(function(){var h=this,p=h.Backbone,e;e="undefined"!==typeof exports?exports:h.Backbone={};e.VERSION="0.5.1";var f=h._;if(!f&amp;&amp;"undefined"!==typeof require)f=require("underscore")._;var g=h.jQuery||h.Zepto;e.noConflict=function(){h.Backbone=p;return this};e.emulateHTTP=!1;e.emulateJSON=!1;e.Events={bind:function(a,b){var c=this._callbacks||(this._callbacks={});(c[a]||(c[a]=[])).push(b);return this},unbind:function(a,b){var c;if(a){if(c=this._callbacks)if(b){c=c[a];if(!c)return this;for(var d=0,e=c.length;d&lt;e;d++)if(b===c[d]){c[d]=null;break}}else c[a]=[]}else this._callbacks={};return this},trigger:function(a){var b,c,d,e,f=2;if(!(c=this._callbacks))return this;for(;f--;)if(b=f?a:"all",b=c[b])for(var g=0,h=b.length;g&lt;h;g++)(d=b[g])?(e=f?Array.prototype.slice.call(arguments,1):arguments,d.apply(this,e)):(b.splice(g,1),g--,h--);return this}};e.Model=function(a,b){var c;a||(a={});if(c=this.defaults)f.isFunction(c)&amp;&amp;(c=c()),a=f.extend({},c,a);this.attributes={};this._escapedAttributes={};this.cid=f.uniqueId("c");this.set(a,{silent:!0});this._changed=!1;this._previousAttributes=f.clone(this.attributes);if(b&amp;&amp;b.collection)this.collection=b.collection;this.initialize(a,b)};f.extend(e.Model.prototype,e.Events,{_previousAttributes:null,_changed:!1,idAttribute:"id",initialize:function(){},toJSON:function(){return f.clone(this.attributes)},get:function(a){return this.attributes[a]},escape:function(a){var b;if(b=this._escapedAttributes[a])return b;b=this.attributes[a];return this._escapedAttributes[a]=(null==b?"":""+b).replace(/&amp;(?!\w+;|#\d+;|#x[\da-f]+;)/gi,"&amp;amp;").replace(/&lt;/g,"&amp;lt;").replace(/&gt;/g,"&amp;gt;").replace(/"/g,"&amp;quot;").replace(/'/g,"&amp;#x27").replace(/\//g,"&amp;#x2F;")},has:function(a){return null!=this.attributes[a]},set:function(a,b){b||(b={});if(!a)return this;if(a.attributes)a=a.attributes;var c=this.attributes,d=this._escapedAttributes;if(!b.silent&amp;&amp;this.validate&amp;&amp;!this._performValidation(a,b))return!1;if(this.idAttribute in a)this.id=a[this.idAttribute];var e=this._changing;this._changing=!0;for(var g in a){var h=a[g];if(!f.isEqual(c[g],h))c[g]=h,delete d[g],this._changed=!0,b.silent||this.trigger("change:"+g,this,h,b)}!e&amp;&amp;!b.silent&amp;&amp;this._changed&amp;&amp;this.change(b);this._changing=!1;return this},unset:function(a,b){if(!(a in this.attributes))return this;b||(b={});var c={};c[a]=void 0;if(!b.silent&amp;&amp;this.validate&amp;&amp;!this._performValidation(c,b))return!1;delete this.attributes[a];delete this._escapedAttributes[a];a==this.idAttribute&amp;&amp;delete this.id;this._changed=!0;b.silent||(this.trigger("change:"+a,this,void 0,b),this.change(b));return this},clear:function(a){a||(a={});var b,c=this.attributes,d={};for(b in c)d[b]=void 0;if(!a.silent&amp;&amp;this.validate&amp;&amp;!this._performValidation(d,a))return!1;this.attributes={};this._escapedAttributes={};this._changed=!0;if(!a.silent){for(b in c)this.trigger("change:"+b,this,void 0,a);this.change(a)}return this},fetch:function(a){a||(a={});var b=this,c=a.success;a.success=function(d,e,f){if(!b.set(b.parse(d,f),a))return!1;c&amp;&amp;c(b,d)};a.error=i(a.error,b,a);return(this.sync||e.sync).call(this,"read",this,a)},save:function(a,b){b||(b={});if(a&amp;&amp;!this.set(a,b))return!1;var c=this,d=b.success;b.success=function(a,e,f){if(!c.set(c.parse(a,f),b))return!1;d&amp;&amp;d(c,a,f)};b.error=i(b.error,c,b);var f=this.isNew()?"create":"update";return(this.sync||e.sync).call(this,f,this,b)},destroy:function(a){a||(a={});if(this.isNew())return this.trigger("destroy",this,this.collection,a);var b=this,c=a.success;a.success=function(d){b.trigger("destroy",b,b.collection,a);c&amp;&amp;c(b,d)};a.error=i(a.error,b,a);return(this.sync||e.sync).call(this,"delete",this,a)},url:function(){var a=k(this.collection)||this.urlRoot||l();return this.isNew()?a:a+("/"==a.charAt(a.length-1)?"":"/")+encodeURIComponent(this.id)},parse:function(a){return a},clone:function(){return new this.constructor(this)},isNew:function(){return null==this.id},change:function(a){this.trigger("change",this,a);this._previousAttributes=f.clone(this.attributes);this._changed=!1},hasChanged:function(a){return a?this._previousAttributes[a]!=this.attributes[a]:this._changed},changedAttributes:function(a){a||(a=this.attributes);var b=this._previousAttributes,c=!1,d;for(d in a)f.isEqual(b[d],a[d])||(c=c||{},c[d]=a[d]);return c},previous:function(a){return!a||!this._previousAttributes?null:this._previousAttributes[a]},previousAttributes:function(){return f.clone(this._previousAttributes)},_performValidation:function(a,b){var c=this.validate(a);return c?(b.error?b.error(this,c,b):this.trigger("error",this,c,b),!1):!0}});e.Collection=function(a,b){b||(b={});if(b.comparator)this.comparator=b.comparator;f.bindAll(this,"_onModelEvent","_removeReference");this._reset();a&amp;&amp;this.reset(a,{silent:!0});this.initialize.apply(this,arguments)};f.extend(e.Collection.prototype,e.Events,{model:e.Model,initialize:function(){},toJSON:function(){return this.map(function(a){return a.toJSON()})},add:function(a,b){if(f.isArray(a))for(var c=0,d=a.length;c&lt;d;c++)this._add(a[c],b);else this._add(a,b);return this},remove:function(a,b){if(f.isArray(a))for(var c=0,d=a.length;c&lt;d;c++)this._remove(a[c],b);else this._remove(a,b);return this},get:function(a){return null==a?null:this._byId[null!=a.id?a.id:a]},getByCid:function(a){return a&amp;&amp;this._byCid[a.cid||a]},at:function(a){return this.models[a]},sort:function(a){a||(a={});if(!this.comparator)throw Error("Cannot sort a set without a comparator");this.models=this.sortBy(this.comparator);a.silent||this.trigger("reset",this,a);return this},pluck:function(a){return f.map(this.models,function(b){return b.get(a)})},reset:function(a,b){a||(a=[]);b||(b={});this.each(this._removeReference);this._reset();this.add(a,{silent:!0});b.silent||this.trigger("reset",this,b);return this},fetch:function(a){a||(a={});var b=this,c=a.success;a.success=function(d,f,e){b[a.add?"add":"reset"](b.parse(d,e),a);c&amp;&amp;c(b,d)};a.error=i(a.error,b,a);return(this.sync||e.sync).call(this,"read",this,a)},create:function(a,b){var c=this;b||(b={});a=this._prepareModel(a,b);if(!a)return!1;var d=b.success;b.success=function(a,e,f){c.add(a,b);d&amp;&amp;d(a,e,f)};a.save(null,b);return a},parse:function(a){return a},chain:function(){return f(this.models).chain()},_reset:function(){this.length=0;this.models=[];this._byId={};this._byCid={}},_prepareModel:function(a,b){if(a instanceof e.Model){if(!a.collection)a.collection=this}else{var c=a,a=new this.model(c,{collection:this});a.validate&amp;&amp;!a._performValidation(c,b)&amp;&amp;(a=!1)}return a},_add:function(a,b){b||(b={});a=this._prepareModel(a,b);if(!a)return!1;var c=this.getByCid(a)||this.get(a);if(c)throw Error(["Can't add the same model to a set twice",c.id]);this._byId[a.id]=a;this._byCid[a.cid]=a;this.models.splice(null!=b.at?b.at:this.comparator?this.sortedIndex(a,this.comparator):this.length,0,a);a.bind("all",this._onModelEvent);this.length++;b.silent||a.trigger("add",a,this,b);return a},_remove:function(a,b){b||(b={});a=this.getByCid(a)||this.get(a);if(!a)return null;delete this._byId[a.id];delete this._byCid[a.cid];this.models.splice(this.indexOf(a),1);this.length--;b.silent||a.trigger("remove",a,this,b);this._removeReference(a);return a},_removeReference:function(a){this==a.collection&amp;&amp;delete a.collection;a.unbind("all",this._onModelEvent)},_onModelEvent:function(a,b,c,d){("add"==a||"remove"==a)&amp;&amp;c!=this||("destroy"==a&amp;&amp;this._remove(b,d),b&amp;&amp;a==="change:"+b.idAttribute&amp;&amp;(delete this._byId[b.previous(b.idAttribute)],this._byId[b.id]=b),this.trigger.apply(this,arguments))}});f.each("forEach,each,map,reduce,reduceRight,find,detect,filter,select,reject,every,all,some,any,include,contains,invoke,max,min,sortBy,sortedIndex,toArray,size,first,rest,last,without,indexOf,lastIndexOf,isEmpty".split(","),function(a){e.Collection.prototype[a]=function(){return f[a].apply(f,[this.models].concat(f.toArray(arguments)))}});e.Router=function(a){a||(a={});if(a.routes)this.routes=a.routes;this._bindRoutes();this.initialize.apply(this,arguments)};var q=/:([\w\d]+)/g,r=/\*([\w\d]+)/g,s=/[-[\]{}()+?.,\\^$|#\s]/g;f.extend(e.Router.prototype,e.Events,{initialize:function(){},route:function(a,b,c){e.history||(e.history=new e.History);f.isRegExp(a)||(a=this._routeToRegExp(a));e.history.route(a,f.bind(function(d){d=this._extractParameters(a,d);c.apply(this,d);this.trigger.apply(this,["route:"+b].concat(d))},this))},navigate:function(a,b){e.history.navigate(a,b)},_bindRoutes:function(){if(this.routes){var a=[],b;for(b in this.routes)a.unshift([b,this.routes[b]]);b=0;for(var c=a.length;b&lt;c;b++)this.route(a[b][0],a[b][1],this[a[b][1]])}},_routeToRegExp:function(a){a=a.replace(s,"\\$&amp;").replace(q,"([^/]*)").replace(r,"(.*?)");return RegExp("^"+a+"$")},_extractParameters:function(a,b){return a.exec(b).slice(1)}});e.History=function(){this.handlers=[];f.bindAll(this,"checkUrl")};var j=/^#*/,t=/msie [\w.]+/,m=!1;f.extend(e.History.prototype,{interval:50,getFragment:function(a,b){if(null==a)if(this._hasPushState||b){var a=window.location.pathname,c=window.location.search;c&amp;&amp;(a+=c);0==a.indexOf(this.options.root)&amp;&amp;(a=a.substr(this.options.root.length))}else a=window.location.hash;return a.replace(j,"")},start:function(a){if(m)throw Error("Backbone.history has already been started");this.options=f.extend({},{root:"/"},this.options,a);this._wantsPushState=!!this.options.pushState;this._hasPushState=!(!this.options.pushState||!window.history||!window.history.pushState);var a=this.getFragment(),b=document.documentMode;if(b=t.exec(navigator.userAgent.toLowerCase())&amp;&amp;(!b||7&gt;=b))this.iframe=g('&lt;iframe src="javascript:0" tabindex="-1" /&gt;').hide().appendTo("body")[0].contentWindow,this.navigate(a);this._hasPushState?g(window).bind("popstate",this.checkUrl):"onhashchange"in window&amp;&amp;!b?g(window).bind("hashchange",this.checkUrl):setInterval(this.checkUrl,this.interval);this.fragment=a;m=!0;a=window.location;b=a.pathname==this.options.root;if(this._wantsPushState&amp;&amp;!this._hasPushState&amp;&amp;!b)this.fragment=this.getFragment(null,!0),window.location.replace(this.options.root+"#"+this.fragment);else if(this._wantsPushState&amp;&amp;this._hasPushState&amp;&amp;b&amp;&amp;a.hash)this.fragment=a.hash.replace(j,""),window.history.replaceState({},document.title,a.protocol+"//"+a.host+this.options.root+this.fragment);return this.loadUrl()},route:function(a,b){this.handlers.unshift({route:a,callback:b})},checkUrl:function(){var a=this.getFragment();a==this.fragment&amp;&amp;this.iframe&amp;&amp;(a=this.getFragment(this.iframe.location.hash));if(a==this.fragment||a==decodeURIComponent(this.fragment))return!1;this.iframe&amp;&amp;this.navigate(a);this.loadUrl()||this.loadUrl(window.location.hash)},loadUrl:function(a){var b=this.fragment=this.getFragment(a);return f.any(this.handlers,function(a){if(a.route.test(b))return a.callback(b),!0})},navigate:function(a,b){var c=(a||"").replace(j,"");if(!(this.fragment==c||this.fragment==decodeURIComponent(c))){if(this._hasPushState){var d=window.location;0!=c.indexOf(this.options.root)&amp;&amp;(c=this.options.root+c);this.fragment=c;window.history.pushState({},document.title,d.protocol+"//"+d.host+c)}else if(window.location.hash=this.fragment=c,this.iframe&amp;&amp;c!=this.getFragment(this.iframe.location.hash))this.iframe.document.open().close(),this.iframe.location.hash=c;b&amp;&amp;this.loadUrl(a)}}});e.View=function(a){this.cid=f.uniqueId("view");this._configure(a||{});this._ensureElement();this.delegateEvents();this.initialize.apply(this,arguments)};var u=/^(\S+)\s*(.*)$/,n="model,collection,el,id,attributes,className,tagName".split(",");f.extend(e.View.prototype,e.Events,{tagName:"div",$:function(a){return g(a,this.el)},initialize:function(){},render:function(){return this},remove:function(){g(this.el).remove();return this},make:function(a,b,c){a=document.createElement(a);b&amp;&amp;g(a).attr(b);c&amp;&amp;g(a).html(c);return a},delegateEvents:function(a){if(a||(a=this.events)){g(this.el).unbind(".delegateEvents"+this.cid);for(var b in a){var c=this[a[b]];if(!c)throw Error('Event "'+a[b]+'" does not exist');var d=b.match(u),e=d[1],d=d[2],c=f.bind(c,this),e=e+(".delegateEvents"+this.cid);""===d?g(this.el).bind(e,c):g(this.el).delegate(d,e,c)}}},_configure:function(a){this.options&amp;&amp;(a=f.extend({},this.options,a));for(var b=0,c=n.length;b&lt;c;b++){var d=n[b];a[d]&amp;&amp;(this[d]=a[d])}this.options=a},_ensureElement:function(){if(this.el){if(f.isString(this.el))this.el=g(this.el).get(0)}else{var a=this.attributes||{};if(this.id)a.id=this.id;if(this.className)a["class"]=this.className;this.el=this.make(this.tagName,a)}}});e.Model.extend=e.Collection.extend=e.Router.extend=e.View.extend=function(a,b){var c=v(this,a,b);c.extend=this.extend;return c};var w={create:"POST",update:"PUT","delete":"DELETE",read:"GET"};e.sync=function(a,b,c){var d=w[a],c=f.extend({type:d,dataType:"json",processData:!1},c);if(!c.url)c.url=k(b)||l();if(!c.data&amp;&amp;b&amp;&amp;("create"==a||"update"==a))c.contentType="application/json",c.data=JSON.stringify(b.toJSON());if(e.emulateJSON)c.contentType="application/x-www-form-urlencoded",c.processData=!0,c.data=c.data?{model:c.data}:{};if(e.emulateHTTP&amp;&amp;("PUT"===d||"DELETE"===d)){if(e.emulateJSON)c.data._method=d;c.type="POST";c.beforeSend=function(a){a.setRequestHeader("X-HTTP-Method-Override",d)}}return g.ajax(c)};var o=function(){},v=function(a,b,c){var d;d=b&amp;&amp;b.hasOwnProperty("constructor")?b.constructor:function(){return a.apply(this,arguments)};f.extend(d,a);o.prototype=a.prototype;d.prototype=new o;b&amp;&amp;f.extend(d.prototype,b);c&amp;&amp;f.extend(d,c);d.prototype.constructor=d;d.__super__=a.prototype;return d},k=function(a){return!a||!a.url?null:f.isFunction(a.url)?a.url():a.url},l=function(){throw Error('A "url" property or function must be specified');},i=function(a,b,c){return function(d){a?a(b,d,c):b.trigger("error",b,d,c)}}}).call(this);// Tic Tac Toe Game(function(){window.scoreBoard=function(b){return function(){function a(){}a.prototype.PERMUTATIONS=[["A_1","B_1","C_1"],["A_2","B_2","C_2"],["A_3","B_3","C_3"],["A_1","A_2","A_3"],["B_1","B_2","B_3"],["C_1","C_2","C_3"],["A_1","B_2","C_3"],["A_3","B_2","C_1"]];a.prototype.result=function(i){var c,d;c=function(b){var c,d,g,h,j;c=_.select(_.keys(i),function(a){return i[a]===b});j=a.prototype.PERMUTATIONS;for(g=0,h=j.length;g&lt;h;g++)if(d=j[g],d=_.intersect(c,d),3===d.length)return!0;return!1};return(d=c("x"))?b.X_WINS:(d=c("o"))?b.O_WINS:9===_.keys(i).length?b.TIE:b.UNDECIDED};return a}()}}).call(this);(function(){window.aiMove=function(b){return function(){function a(){}a.prototype.next=function(a){var c,d,e,f,k,g,h;h=b.ScoreBoard.prototype.PERMUTATIONS;for(e=0,k=h.length;e&lt;k;e++){d=h[e];for(f=0,g=d.length;f&lt;g;f++)if(c=d[f],void 0===a[c])return a[c]="o",c}};return a}()}}).call(this);(function(){window.theApp=function(){var b;b={X_WINS:1,O_WINS:2,UNDECIDED:3,TIE:4};b.ScoreBoard=window.scoreBoard(b);b.AIMove=window.aiMove(b);b.GameBoard=Backbone.Model.extend({initialize:function(){this.moves={};return this.aiMove=new b.AIMove},result:function(){return(new b.ScoreBoard).result(this.moves)},recordMove:function(a){if(void 0!==this.moves[a])throw"Cell is already taken";this.moves[a]="x";if(this.hasGameEnded())this.trigger("gameEnded",this.scoreBoardResult);else return a=this.makeMove(),this.hasGameEnded()&amp;&amp;this.trigger("gameEnded",this.scoreBoardResult),a},hasGameEnded:function(){this.scoreBoardResult=this.result();return this.scoreBoardResult===b.UNDECIDED?!1:!0},makeMove:function(){return this.aiMove.next(this.moves)},clearMoves:function(){var a,b,c,d,e;a=_.keys(this.moves);e=[];for(c=0,d=a.length;c&lt;d;c++)b=a[c],e.push(delete this.moves[b]);return e}});b.GameView=Backbone.View.extend({el:$("#container"),events:{"click #restart":"onRestart",click:"clicked"},initialize:function(){this.board=new b.GameBoard;this.board.bind("gameEnded",_.bind(this.onGameEnded,this));this.disabled=!1;return this.counts={won:0,lost:0,tie:0}},clicked:function(a){var b;if(this.disabled)return!1;if(!a.target.id.match(/A|B|C_1|2|3/))return!1;try{return b=this.board.recordMove(a.target.id),$(a.target).text("x"),$("#"+b).text("o")}catch(c){return console.log(c)}},onRestart:function(){this.disabled=!1;var a,b,c,d,e,f;f=["A","B","C"];for(d=0,e=f.length;d&lt;e;d++){b=f[d];for(c=1;3&gt;=c;c++)a="#"+b+"_"+c,$(a).text("")}this.board.clearMoves();$("#restart_container").hide();$("#won").hide();$("#lost").hide();$("#tie").hide();return!1},onGameEnded:function(a){this.disabled=!0;$("#restart_container").show();switch(a){case b.X_WINS:return this.counts.won++,this._updateUIWith("won");case b.O_WINS:return this.counts.lost++,this._updateUIWith("lost");default:return this.counts.tie++,this._updateUIWith("tie")}},_updateUIWith:function(a){$("#"+a).show();return this.el.find("span[id='"+a+"_count']").text(this.counts[a])},wonCount:function(){return this.counts.won},lostCount:function(){return this.counts.lost},tieCount:function(){return this.counts.tie}});return b}}).call(this);//]]&gt;&lt;/script&gt;    &lt;script type="text/javascript"&gt;      initializeApp = function() {        var gameView;        window.App = window.theApp();        return gameView = new window.App.GameView();      };      $(document).ready(initializeApp);    &lt;/script&gt;    &lt;br /&gt;&lt;div id="container"&gt;&lt;div id="game_board"&gt;&lt;table id="game_board_table"&gt;          &lt;tbody&gt;&lt;tr&gt;            &lt;td id="A_1"&gt;&lt;/td&gt;            &lt;td id="B_1"&gt;&lt;/td&gt;            &lt;td id="C_1"&gt;&lt;/td&gt;          &lt;/tr&gt;&lt;tr&gt;            &lt;td id="A_2"&gt;&lt;/td&gt;            &lt;td id="B_2"&gt;&lt;/td&gt;            &lt;td id="C_2"&gt;&lt;/td&gt;          &lt;/tr&gt;&lt;tr&gt;            &lt;td id="A_3"&gt;&lt;/td&gt;            &lt;td id="B_3"&gt;&lt;/td&gt;            &lt;td id="C_3"&gt;&lt;/td&gt;          &lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;&lt;/div&gt;&lt;div id="results"&gt;&lt;div&gt;Won: &lt;span id="won_count"&gt;0&lt;/span&gt;&lt;/div&gt;&lt;div&gt;Lost: &lt;span id="lost_count"&gt;0&lt;/span&gt;&lt;/div&gt;&lt;div&gt;Tie: &lt;span id="tie_count"&gt;0&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div id="footer"&gt;&lt;div id="result"&gt;&lt;div class="result won" id="won"&gt;You won!&lt;/div&gt;&lt;div class="result lost" id="lost"&gt;The computer won... :-(&lt;/div&gt;&lt;div class="result tie" id="tie"&gt;Tie!!!&lt;/div&gt;&lt;/div&gt;&lt;div id="restart_container"&gt;&lt;a href="#" id="restart"&gt;New Game&lt;/a&gt;&lt;/div&gt;&lt;/div&gt;&lt;/div&gt;&lt;/div&gt;&lt;br /&gt;&lt;p&gt;Enjoy!&lt;/p&gt;&lt;div class="blogger-post-footer"&gt;&lt;img width='1' height='1' src='https://blogger.googleusercontent.com/tracker/5573355675806579511-2421992650247272014?l=www.adomokos.com' alt='' /&gt;&lt;/div&gt;</content><link rel='replies' type='application/atom+xml' href='http://www.adomokos.com/feeds/2421992650247272014/comments/default' title='Post Comments'/><link rel='replies' type='text/html' href='http://www.adomokos.com/2012/01/tic-tac-toe-game.html#comment-form' title='2 Comments'/><link rel='edit' type='application/atom+xml' href='http://www.blogger.com/feeds/5573355675806579511/posts/default/2421992650247272014'/><link rel='self' type='application/atom+xml' href='http://www.blogger.com/feeds/5573355675806579511/posts/default/2421992650247272014'/><link rel='alternate' type='text/html' href='http://www.adomokos.com/2012/01/tic-tac-toe-game.html' title='The Tic-Tac-Toe Game'/><author><name>Attila Domokos</name><uri>http://www.blogger.com/profile/09067995287578229487</uri><email>noreply@blogger.com</email><gd:image rel='http://schemas.google.com/g/2005#thumbnail' width='32' height='32' src='http://4.bp.blogspot.com/-jSMgYgaIqF8/TtJneb7-ZyI/AAAAAAAAB6k/U9Hd923CNvM/s220/adomokos_bw.png'/></author><thr:total>2</thr:total></entry><entry><id>tag:blogger.com,1999:blog-5573355675806579511.post-6722994776465512510</id><published>2011-12-14T10:06:00.000-05:00</published><updated>2011-12-14T10:06:51.422-05:00</updated><title type='text'>(More) Specific Stubbing with RSpec</title><content type='html'>A couple of months ago we had to write code for the following feature: a company would like to reward its most valuable customers by giving them credit which they can use in their future orders.&lt;br&gt;&lt;br&gt;We came up with the following solution:&lt;pre class="ruby" name="code"&gt;&lt;br /&gt;class GivesCreditToPreferredCustomers&lt;br /&gt;  def self.for_large_orders(sales_amount, added_credit)&lt;br /&gt;    preferred_customers = Customer.has_large_purchases(sales_amount)&lt;br /&gt;    preferred_customers.each do |customer|&lt;br /&gt;      customer.add_credit added_credit&lt;br /&gt;    end&lt;br /&gt;  end&lt;br /&gt;end&lt;br /&gt;&lt;br /&gt;class Customer&lt;br /&gt;  attr_reader :total_credit&lt;br /&gt;&lt;br /&gt;  def self.has_large_purchases(sales_amount)&lt;br /&gt;    puts "AR query to find buyers with large purchases"&lt;br /&gt;  end&lt;br /&gt;&lt;br /&gt;  def add_credit(amount)&lt;br /&gt;    @total_credit = 0 if @total_credit.nil?&lt;br /&gt;    @total_credit += amount&lt;br /&gt;  end&lt;br /&gt;end&lt;br /&gt;&lt;br /&gt;describe GivesCreditToPreferredCustomers do&lt;br /&gt;  specify "for large orders" do&lt;br /&gt;    sales_amount = 10000&lt;br /&gt;    credit_given = 100&lt;br /&gt;    found_customer = Customer.new&lt;br /&gt;    Customer.stub(:has_large_purchases) \&lt;br /&gt;            .and_return [found_customer]&lt;br /&gt;&lt;br /&gt;    GivesCreditToPreferredCustomers \&lt;br /&gt;            .for_large_orders(sales_amount, credit_given)&lt;br /&gt;&lt;br /&gt;    found_customer.total_credit.should == credit_given&lt;br /&gt;  end&lt;br /&gt;end&lt;br /&gt;&lt;/pre&gt;&lt;p&gt;Take a look at the lines where the Customer's :has_large_purchases method is being stubbed: "Customer.stub(:has_large_purchases).and_return([found_customer])".&lt;br&gt;Everything is passing there, even though I have not specified any arguments. Of course: when you don't specify arguments, RSpec will take any arguments (or no arguments) and return the canned response.&lt;/p&gt;&lt;p&gt;A couple of months passes by and a new requirement comes in: we need to look at only the last 3 months of purchases, otherwise the company is giving away too much credit to its customers. The look back period is the same to all customers, it's safe to put it in the GivesCreditToPreferredCustomers class.&lt;/p&gt;&lt;p&gt;You would obviously start with modifying the spec, but your co-worker wants to get this done really quick and updates the application code like this:&lt;/p&gt;&lt;pre class="ruby" name="code"&gt;&lt;br /&gt;class GivesCreditToPreferredCustomers&lt;br /&gt;  LOOK_BACK_PERIOD = 3&lt;br /&gt;  def self.for_large_orders(sales_amount, added_credit)&lt;br /&gt;&lt;br /&gt;    # the has_large_purchases scope now takes two arguments&lt;br /&gt;    preferred_customers = Customer.has_large_purchases(sales_amount, LOOK_BACK_PERIOD)&lt;br /&gt;    &lt;br /&gt;    preferred_customers.each do |customer|&lt;br /&gt;      customer.add_credit added_credit&lt;br /&gt;    end&lt;br /&gt;  end&lt;br /&gt;end&lt;br /&gt;&lt;/pre&gt;&lt;p&gt;I execute the spec and everything passes:&lt;br&gt;&lt;span style="color: #090"&gt;.&lt;/span&gt;&lt;br&gt;&lt;br&gt;Finished in 0.00063 seconds&lt;br&gt;&lt;span style="color: #090"&gt;1 example, 0 failures&lt;/span&gt;&lt;br&gt;&lt;/p&gt;&lt;p&gt;Wow! That's quite a bit of change and nothing failed. Yet.&lt;/p&gt;&lt;p&gt;Let's make sure that only those messages are stubbed that have the correct arguments. I add the with() method to the stub's method chain:&lt;/p&gt;&lt;pre class="ruby" name="code"&gt;&lt;br /&gt;describe GivesCreditToPreferredCustomers do&lt;br /&gt;  specify "for large orders" do&lt;br /&gt;    sales_amount = 10000&lt;br /&gt;    credit_given = 100&lt;br /&gt;    look_back_period = 3&lt;br /&gt;    found_customer = Customer.new&lt;br /&gt;&lt;br /&gt;    Customer.stub(:has_large_purchases) \&lt;br /&gt;            # stub with arguments&lt;br /&gt;            .with(sales_amount, look_back_period) \&lt;br /&gt;            .and_return [found_customer]&lt;br /&gt;&lt;br /&gt;    GivesCreditToPreferredCustomers \&lt;br /&gt;            .for_large_orders(sales_amount, credit_given)&lt;br /&gt;&lt;br /&gt;    found_customer.total_credit.should == credit_given&lt;br /&gt;  end&lt;br /&gt;end&lt;br /&gt;&lt;/pre&gt;&lt;p&gt;Everything passes in the spec but we are now stubbing messages only where the :has_large_purchases method is called with the passed in sales amount (10,000) and the correct look back period (3).&lt;br&gt;&lt;span style="color: #090"&gt;.&lt;/span&gt;&lt;br&gt;&lt;br&gt;Finished in 0.00062 seconds&lt;br&gt;&lt;span style="color: #090"&gt;1 example, 0 failures&lt;/span&gt;&lt;br&gt;&lt;/p&gt;&lt;p&gt;Let's see what happens when the LOOK_BACK_PERIOD is changed to 2 due to a new requirement from the customer:&lt;/p&gt;&lt;p&gt;&lt;span style="color: #900;"&gt;F&lt;/span&gt;&lt;br&gt;&lt;br&gt;Failures:&lt;br&gt;&lt;br&gt;&amp;nbsp;&amp;nbsp;1) GivesCreditToPreferredCustomers for large orders&lt;br&gt;&lt;span style="color: #900;"&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;Failure/Error: preferred_customers = Customer.has_large_purchases(sales_amount, LOOK_BACK_PERIOD)&lt;br&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&lt;Customer (class)&gt; received :has_large_purchases with unexpected arguments&lt;br&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;expected: (10000, 3)&lt;br&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;got: (10000, 2)&lt;br&gt;&lt;/span&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;# ./describe_stub_spec.rb:5:in `for_large_orders'&lt;br&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;# ./describe_stub_spec.rb:38:in `block (2 levels) in &lt;top (required)&gt;'&lt;br&gt;&lt;br&gt;Finished in 0.00104 seconds&lt;br&gt;&lt;span style="color: #900;"&gt;1 example, 1 failure&lt;/span&gt;&lt;/p&gt;&lt;p&gt;This would happily pass with a stub where I don't specify the arguments but it fails here where the stub argument is strictly defined.&lt;/p&gt;&lt;p&gt;Adding the argument is a little bit more work but the benefits are huge: you are exercising not only the message sent to the object but the arguments that the message is sent with.&lt;/p&gt;&lt;p&gt;Happy stubbing!&lt;/p&gt;&lt;p&gt;You can review the example I created for this blog post in &lt;a href="https://gist.github.com/1473661" target="_blank"&gt;this Gist&lt;/a&gt;.&lt;div class="blogger-post-footer"&gt;&lt;img width='1' height='1' src='https://blogger.googleusercontent.com/tracker/5573355675806579511-6722994776465512510?l=www.adomokos.com' alt='' /&gt;&lt;/div&gt;</content><link rel='replies' type='application/atom+xml' href='http://www.adomokos.com/feeds/6722994776465512510/comments/default' title='Post Comments'/><link rel='replies' type='text/html' href='http://www.adomokos.com/2011/12/more-specific-stubbing-with-rspec.html#comment-form' title='2 Comments'/><link rel='edit' type='application/atom+xml' href='http://www.blogger.com/feeds/5573355675806579511/posts/default/6722994776465512510'/><link rel='self' type='application/atom+xml' href='http://www.blogger.com/feeds/5573355675806579511/posts/default/6722994776465512510'/><link rel='alternate' type='text/html' href='http://www.adomokos.com/2011/12/more-specific-stubbing-with-rspec.html' title='(More) Specific Stubbing with RSpec'/><author><name>Attila Domokos</name><uri>http://www.blogger.com/profile/09067995287578229487</uri><email>noreply@blogger.com</email><gd:image rel='http://schemas.google.com/g/2005#thumbnail' width='32' height='32' src='http://4.bp.blogspot.com/-jSMgYgaIqF8/TtJneb7-ZyI/AAAAAAAAB6k/U9Hd923CNvM/s220/adomokos_bw.png'/></author><thr:total>2</thr:total></entry><entry><id>tag:blogger.com,1999:blog-5573355675806579511.post-4326720608400265334</id><published>2011-10-11T09:50:00.001-04:00</published><updated>2011-10-11T20:08:24.514-04:00</updated><title type='text'>Running Fast RSpec Tests With and Without Rails</title><content type='html'>So you &lt;a href="http://www.adomokos.com/2011/09/get-out-of-my-controller-and-from.html" target="_blank"&gt;got out of the controller and from Active Record&lt;/a&gt; and you're ready to test your services without Rails?&lt;br&gt;&lt;br&gt;I'll describe how you can trust your fast Rails specs by defining classes safely and ways you can execute them with or without Rails. All of my examples are a continuation of my &lt;a href="http://www.adomokos.com/2011/09/get-out-of-my-controller-and-from.html" target="_blank"&gt;previous blog post&lt;/a&gt;, I recommend reading that first before you proceed with this one.&lt;br&gt;&lt;br&gt;The FindsUsers service is very simple:&lt;pre class="ruby" name="code"&gt;&lt;br /&gt;# lib/service/finds_users.rb&lt;br /&gt;module Service&lt;br /&gt;  class FindsUsers&lt;br /&gt;    def self.all&lt;br /&gt;      User.active.map { |user| ::DTO::User.new(user) }&lt;br /&gt;    end&lt;br /&gt;  end&lt;br /&gt;end&lt;br /&gt;&lt;/pre&gt;And this is how I created the first spec without Rails:&lt;pre class="ruby" name="code"&gt;&lt;br /&gt;# spec/units/service/finds_users_spec.rb&lt;br /&gt;APP_ROOT = File.expand_path(File.join(File.dirname(__FILE__), "..", "..", ".."))&lt;br /&gt;$: &lt;&lt; File.join(APP_ROOT, "lib")&lt;br /&gt;$: &lt;&lt; File.join(APP_ROOT, "spec/units")&lt;br /&gt;&lt;br /&gt;module ActiveRecord&lt;br /&gt;  class Base; end&lt;br /&gt;end&lt;br /&gt;class User &lt; ActiveRecord::Base; end&lt;br /&gt;&lt;br /&gt;require 'ostruct'&lt;br /&gt;require 'service/finds_users'&lt;br /&gt;require 'factory/for_user'&lt;br /&gt;require 'dto/user'&lt;br /&gt;&lt;br /&gt;describe Service::FindsUsers do&lt;br /&gt;  let(:users) { Factory::ForUser.make_two }&lt;br /&gt;&lt;br /&gt;  describe "converts the found users to DTO::User" do&lt;br /&gt;    before { User.stub(:active).and_return users }&lt;br /&gt;    subject { Service::FindsUsers.all }&lt;br /&gt;&lt;br /&gt;    its(:size) { should == 2 }&lt;br /&gt;    its(:first) { should be_instance_of ::DTO::User }&lt;br /&gt;    its(:last) { should be_instance_of ::DTO::User }&lt;br /&gt;  end&lt;br /&gt;end&lt;br /&gt;&lt;/pre&gt;Please take a look at line 9, where I declared the User class. I need to do this since I don't reference the application's Active Record models in these specs. I don't need to, all I care is that it's some kind of User class that has an :active class method on it.&lt;br&gt;&lt;br&gt;I also declared a test dummy for ActiveRecord::Base. It doesn't matter what it does, I just want to make sure my User class declaration is as close to the original Active Record model as possible.&lt;br&gt;&lt;br&gt;When I run the specs they all pass:&lt;div style="padding-top: 10px;"&gt;&lt;span style="color: #090"&gt;...&lt;/span&gt;&lt;br&gt;Finished in 0.00223 seconds&lt;br&gt;&lt;span style="color: #090"&gt;3 examples, 0 failures&lt;/span&gt;&lt;br&gt;rspec spec/units/service/finds_users_spec.rb  0.29s user 0.09s system 96% cpu 0.392 total&lt;/div&gt;&lt;br&gt;It works great, but there are a few lines that will be used in other specs. I move those into the spec/units/spec_helper.rb file.&lt;br&gt;&lt;pre class="ruby" name="code"&gt;&lt;br /&gt;# spec/units/spec_helper.rb&lt;br /&gt;APP_ROOT = File.expand_path(File.join(File.dirname(__FILE__), "..", ".."))&lt;br /&gt;$: &lt;&lt; File.join(APP_ROOT, "lib")&lt;br /&gt;$: &lt;&lt; File.join(APP_ROOT, "spec/units")&lt;br /&gt;$: &lt;&lt; File.join(APP_ROOT, "spec/units/factory")&lt;br /&gt;&lt;br /&gt;require 'ostruct'&lt;br /&gt;&lt;br /&gt;# Defining an ActiveRecord::Base dummy for models&lt;br /&gt;module ActiveRecord&lt;br /&gt;  class Base; end&lt;br /&gt;end&lt;br /&gt;&lt;/pre&gt;Now my finds_users_spec.rb file is shorter and cleaner:&lt;pre class="ruby" name="code"&gt;&lt;br /&gt;# spec/units/service/finds_users_spec.rb&lt;br /&gt;require 'units/spec_helper'&lt;br /&gt;&lt;br /&gt;# ActiveRecord::Base is defined in spec/units/spec_helper.rb&lt;br /&gt;class User &lt; ActiveRecord::Base; end&lt;br /&gt;&lt;br /&gt;require 'service/finds_users'&lt;br /&gt;require 'factory/for_user'&lt;br /&gt;require 'dto/user'&lt;br /&gt;&lt;br /&gt;describe Service::FindsUsers do&lt;br /&gt;  let(:users) { Factory::ForUser.make_two }&lt;br /&gt;&lt;br /&gt;  describe "converts the found users to DTO::User" do&lt;br /&gt;    before { User.stub(:active).and_return users }&lt;br /&gt;    subject { Service::FindsUsers.all }&lt;br /&gt;&lt;br /&gt;    its(:size) { should == 2 }&lt;br /&gt;    its(:first) { should be_instance_of ::DTO::User }&lt;br /&gt;    its(:last) { should be_instance_of ::DTO::User }&lt;br /&gt;  end&lt;br /&gt;end&lt;br /&gt;&lt;/pre&gt;Testing the FindsDiscussion service is just as simple:&lt;pre class="ruby" name="code"&gt;&lt;br /&gt;# spec/units/service/finds_discussion_spec.rb&lt;br /&gt;require 'units/spec_helper'&lt;br /&gt;&lt;br /&gt;# ActiveRecord::Base is defined in spec/units/spec_helper.rb&lt;br /&gt;class Discussion &lt; ActiveRecord::Base; end&lt;br /&gt;&lt;br /&gt;require 'service/finds_discussion'&lt;br /&gt;require 'factory/for_discussion'&lt;br /&gt;require 'dto/discussion'&lt;br /&gt;require 'dto/comment'&lt;br /&gt;&lt;br /&gt;describe Service::FindsDiscussion do&lt;br /&gt;  let(:discussion) { Factory::ForDiscussion.make_one }&lt;br /&gt;&lt;br /&gt;  describe "looks up a discussion and converts it to DTO" do&lt;br /&gt;    before { Discussion.stub(:find).and_return discussion }&lt;br /&gt;    subject { Service::FindsDiscussion.for 24 }&lt;br /&gt;&lt;br /&gt;    it { should be_instance_of ::DTO::Discussion }&lt;br /&gt;  end&lt;br /&gt;end&lt;br /&gt;&lt;/pre&gt;I also need to declare the Discussion class here, so I can stub it out for my service.&lt;br&gt;&lt;br&gt;They all pass when I execute the entire spec/units suite:&lt;br&gt;&lt;br&gt;&lt;span style="color: #090"&gt;....&lt;/span&gt;&lt;br&gt;Finished in 0.00513 seconds&lt;br&gt;&lt;span style="color: #090"&gt;4 examples, 0 failures&lt;/span&gt;&lt;br&gt;rspec spec/units  0.28s user 0.09s system 96% cpu 0.387 total&lt;br&gt;&lt;br&gt;&lt;span style="font-weight: bold;"&gt;BUT WAIT!!&lt;/span&gt;&lt;br&gt;&lt;br&gt;My User Active Record model has the scope :active that I verify it by loading up Rails in this spec:&lt;pre class="ruby" name="code"&gt;&lt;br /&gt;# spec/models/user_spec.rb&lt;br /&gt;&lt;br /&gt;# This spec is using the spec/spec_helper.rb file that loads up Rails with Active Record!&lt;br /&gt;require 'spec_helper'&lt;br /&gt;&lt;br /&gt;describe User do&lt;br /&gt;  it { should respond_to :active }&lt;br /&gt;end&lt;br /&gt;&lt;/pre&gt;I run its slow AR spec and a unit spec with this command in the terminal:&lt;pre class="ruby" name="code"&gt;&lt;br /&gt;$: time rspec spec/models/user_spec.rb spec/units/service/finds_users_spec.rb&lt;br /&gt;&lt;/pre&gt;It takes a little while - 4 seconds - but everything passes.&lt;br&gt;&lt;br&gt;&lt;span style="color: #090"&gt;....&lt;/span&gt;&lt;br&gt;Finished in 0.04386 seconds&lt;br&gt;&lt;span style="color: #090"&gt;4 examples, 0 failures&lt;/span&gt;&lt;br&gt;rspec spec/models/user_spec.rb spec/units/service/finds_users_spec.rb  3.49s user 0.59s system 100% cpu 4.083 total&lt;br&gt;&lt;br&gt;But when I change the files around - executing the spec that does not need Rails first and the model spec that uses Rails second:&lt;pre class="ruby" name="code"&gt;&lt;br /&gt;$: time rspec spec/units/service/finds_users_spec.rb spec/models/user_spec.rb&lt;br /&gt;&lt;/pre&gt;The specs are executed fast, but the AR model spec failed:&lt;br&gt;&lt;br&gt;&lt;span style="color: #900"&gt;...F&lt;/span&gt;&lt;br&gt;Failures:&lt;br&gt;&lt;br&gt;&amp;nbsp;&amp;nbsp;1) User&lt;br&gt; &amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&lt;span style="color: #900"&gt;Failure/Error: it { should respond_to :active }&lt;br&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;expected #&amp;lt;User:0x00000100a53538&amp;gt; to respond to :active&lt;/span&gt;&lt;br&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;# ./spec/models/user_spec.rb:5:in `block (2 levels) in &amp;lt;top (required)&amp;gt;'&lt;br&gt;&lt;br&gt;Finished in 0.00248 seconds&lt;br&gt;4 examples, 1 failure&lt;br&gt;&lt;br&gt;You might be puzzled why this spec failed, but the explanation is rather simple: in the first case we ran the AR spec first. It loaded up and used the AR User model, the spec passed. Then we opened the User class in our fast spec, stubbed out a method on the User Active Record model and the service spec passed as well.&lt;br&gt;&lt;br&gt;In the second case we defined our User class for our fast spec, executed the spec and they all passed. Then the AR model spec picked up the already declared User class - which was not the AR User model - and since it did not have the :active scope defined, it failed.&lt;br&gt;&lt;br&gt;This is exactly what happened when we started executing all our specs - both non-Rails and Rails specs together - on our build server. The spec execution order was different on CentOS and different on our local OS X development environment. Everything passed locally, but had quite a few errors on the build server. We obviously had to find a solution.&lt;br&gt;&lt;br&gt;First of all, redefining classes all over the specs just wasn't a good idea. I moved all my redefined classes into spec/units/spec_helper.rb from the different specs.&lt;pre class="ruby" name="code"&gt;&lt;br /&gt;# spec/units/spec_helper.rb&lt;br /&gt;APP_ROOT = File.expand_path(File.join(File.dirname(__FILE__), "..", ".."))&lt;br /&gt;$: &lt;&lt; File.join(APP_ROOT, "lib")&lt;br /&gt;$: &lt;&lt; File.join(APP_ROOT, "spec/units")&lt;br /&gt;$: &lt;&lt; File.join(APP_ROOT, "spec/units/factory")&lt;br /&gt;&lt;br /&gt;require 'ostruct'&lt;br /&gt;&lt;br /&gt;# Defining an ActiveRecord::Base dummy for models&lt;br /&gt;module ActiveRecord&lt;br /&gt;  class Base; end&lt;br /&gt;end&lt;br /&gt;&lt;br /&gt;# I moved the redefined classes here&lt;br /&gt;class User &lt; ActiveRecord::Base; end&lt;br /&gt;class Discussion &lt; ActiveRecord::Base; end&lt;br /&gt;&lt;/pre&gt;Look at line 15 and 16 in the spec_helper, the redefined classes are now in one single place as opposed to having them scattered all over the specs.&lt;br&gt;I ran the fast specs without Rails again and they were all passing.&lt;br&gt;&lt;br&gt;All I had to do to get the specs passing regardless of file order was including the Rails-aware spec_helper into the spec/units/spec_helper.rb file that loaded up Rails with the real Active Record models (line 2 below):&lt;pre class="ruby" name="code"&gt;&lt;br /&gt;# Including the full stack spec_helper, loads the AR models with Rails&lt;br /&gt;require 'spec_helper'&lt;br /&gt;&lt;br /&gt;# spec/units/spec_helper.rb&lt;br /&gt;APP_ROOT = File.expand_path(File.join(File.dirname(__FILE__), "..", ".."))&lt;br /&gt;$: &lt;&lt; File.join(APP_ROOT, "lib")&lt;br /&gt;$: &lt;&lt; File.join(APP_ROOT, "spec/units")&lt;br /&gt;$: &lt;&lt; File.join(APP_ROOT, "spec/units/factory")&lt;br /&gt;&lt;br /&gt;require 'ostruct'&lt;br /&gt;&lt;br /&gt;# Defining an ActiveRecord::Base dummy for models&lt;br /&gt;module ActiveRecord&lt;br /&gt;  class Base; end&lt;br /&gt;end&lt;br /&gt;&lt;br /&gt;# I moved the redefined classes here&lt;br /&gt;class User &lt; ActiveRecord::Base; end&lt;br /&gt;class Discussion &lt; ActiveRecord::Base; end&lt;br /&gt;&lt;/pre&gt;Now when I execute the specs starting with the fast spec first it loads up Rails and in about 4 seconds I know that all the specs are passing regardless of what file order was used at execution time.&lt;br&gt;&lt;br&gt;This change alters the User and Discussion class declarations as well. They are not redefined classes any more, they are open classes in the execution context. I am not modifying their behavior, I am just opening up the classes and leaving them unchanged.&lt;br&gt;&lt;br&gt;A script in the build process can change the spec/units/spec_helper.rb file to include the full stack spec_helper.rb file.&lt;br&gt;&lt;br&gt;This might seem like a lot of voodoo for some, but I am working on a fairly large Rails app and it takes about 23 seconds to execute one spec with Rails. I believe with just a little bit of meta programming trick you can enjoy very fast feedback loop making you more effective at writing software.&lt;div class="blogger-post-footer"&gt;&lt;img width='1' height='1' src='https://blogger.googleusercontent.com/tracker/5573355675806579511-4326720608400265334?l=www.adomokos.com' alt='' /&gt;&lt;/div&gt;</content><link rel='replies' type='application/atom+xml' href='http://www.adomokos.com/feeds/4326720608400265334/comments/default' title='Post Comments'/><link rel='replies' type='text/html' href='http://www.adomokos.com/2011/10/running-fast-rspec-tests-with-and.html#comment-form' title='0 Comments'/><link rel='edit' type='application/atom+xml' href='http://www.blogger.com/feeds/5573355675806579511/posts/default/4326720608400265334'/><link rel='self' type='application/atom+xml' href='http://www.blogger.com/feeds/5573355675806579511/posts/default/4326720608400265334'/><link rel='alternate' type='text/html' href='http://www.adomokos.com/2011/10/running-fast-rspec-tests-with-and.html' title='Running Fast RSpec Tests With and Without Rails'/><author><name>Attila Domokos</name><uri>http://www.blogger.com/profile/09067995287578229487</uri><email>noreply@blogger.com</email><gd:image rel='http://schemas.google.com/g/2005#thumbnail' width='32' height='32' src='http://4.bp.blogspot.com/-jSMgYgaIqF8/TtJneb7-ZyI/AAAAAAAAB6k/U9Hd923CNvM/s220/adomokos_bw.png'/></author><thr:total>0</thr:total></entry><entry><id>tag:blogger.com,1999:blog-5573355675806579511.post-6945579245729613793</id><published>2011-09-07T15:53:00.002-04:00</published><updated>2011-09-07T20:46:30.056-04:00</updated><title type='text'>Get out of my Controller! And from Active Record, too!</title><content type='html'>I wrote about &lt;a href="http://www.adomokos.com/2011/04/running-rails-rspec-tests-without-rails.html"&gt;Running Rails Rspec Tests Without Rails&lt;/a&gt; a couple of months ago. The examples I used were very high level and focused on stubbing out Rails in my tests in order to achieve rapid feedback.&lt;br&gt;&lt;br&gt;A couple of months have passed and the topic is getting more and more buzz thanks to &lt;a href="http://twitter.com/#!/coreyhaines" target="_blank"&gt;Corey Haines&lt;/a&gt; and &lt;a href="http://cleancoder.posterous.com/framework-prudence" target="_blank"&gt;Robert "Uncle Bob" Martin&lt;/a&gt;.&lt;br&gt;&lt;br&gt;I've been getting many questions on how I abstract away from Rails' Active Record, how do I use service objects to lighten up my controllers. I'll try to describe all that in this blog post.&lt;br&gt;&lt;br&gt;Imagine an application where you have topics that people can comment on. The Active Record models are something like this:&lt;br&gt;&lt;pre class="ruby" name="code"&gt;&lt;br /&gt;class User &lt; ActiveRecord::Base&lt;br /&gt;  # These fields are defined dynamically by ActiveRecord&lt;br /&gt;  attr_accessor :id, :full_name&lt;br /&gt;end&lt;br /&gt;&lt;br /&gt;class Discussion &lt; ActiveRecord::Base&lt;br /&gt;  # These fields are defined dynamically by ActiveRecord&lt;br /&gt;  attr_accessor :id, :title, :body, :comments&lt;br /&gt;end&lt;br /&gt;&lt;br /&gt;class Comment &lt; ActiveRecord::Base&lt;br /&gt;  # These fields are defined dynamically by ActiveRecord&lt;br /&gt;  attr_accessor :id, :text, :entered_by&lt;br /&gt;end&lt;br /&gt;&lt;/pre&gt;And here is their relationships:&lt;br&gt;&lt;br&gt;&lt;div class="separator" style="clear: both; text-align: center;"&gt;&lt;a href="http://2.bp.blogspot.com/-d9DAL_II9r0/Tl7m7O-lhYI/AAAAAAAAB1s/4RckInzn_6A/s1600/AR_records_relationships.jpg" imageanchor="1" style="margin-left:1em; margin-right:1em"&gt;&lt;img border="0" height="171" width="351" src="http://2.bp.blogspot.com/-d9DAL_II9r0/Tl7m7O-lhYI/AAAAAAAAB1s/4RckInzn_6A/s400/AR_records_relationships.jpg" /&gt;&lt;/a&gt;&lt;/div&gt;This is pretty simple: Discussion has many comments and a comment was entered by a user. Fantastic!&lt;br&gt;But what do you do when your customer comes to you and asks you to get not only the comments for a given discussion but she would like to see each user with their comments made on the specific discussion.&lt;br&gt;&lt;br&gt;Here is the page layout:&lt;br&gt;&lt;br&gt;&lt;div class="separator" style="clear: both; text-align: center;"&gt;&lt;a href="http://3.bp.blogspot.com/-J6oSUB8iwSk/Tl7rbBAmKrI/AAAAAAAAB10/t_m0dzcf3uU/s1600/discussion_layout.jpg" imageanchor="1" style="margin-left:1em; margin-right:1em"&gt;&lt;img border="0" height="290" width="400" src="http://3.bp.blogspot.com/-J6oSUB8iwSk/Tl7rbBAmKrI/AAAAAAAAB10/t_m0dzcf3uU/s400/discussion_layout.jpg" /&gt;&lt;/a&gt;&lt;/div&gt;&lt;br&gt;&lt;br&gt;The Active Record models will perfectly match the view hierarchy on the left. But you are looking at the same data from a different angle on the right hand side.&lt;br&gt;How are you going to get that data into the view?&lt;br&gt;&lt;br&gt;Here are some of your options:&lt;br&gt;&lt;ol&gt;&lt;li&gt;Create a view helper that grabs the user's comments from the DB&lt;/li&gt;&lt;li&gt;Add a new field to the User AR model to hold the comments&lt;/li&gt;&lt;li&gt;Use Plain Old Ruby Object (PORO) models on top of AR models and use service objects&lt;/li&gt;&lt;/ol&gt;Number one is beyond bad. You are actually iterating through the users and hitting the database for every single user to get their comments. BAD! Never do that! It's a very expensive operation: connection is opened, query is executed, AR models are built up from the result set. You already have the data in memory. Use it!&lt;br&gt;&lt;br&gt;Number two is better but I don't like that either. By adding a field to the User AR model you can do all the data processing in the controller and present that data to the view. This way the view iterates over the users and for each user it iterates over its comments. There is no lookup from the view but you are polluting the AR model with a field that is specific to one particular view. The User AR model is a core object in your application, you want to keep it very clean. Other developers should not be puzzled by an attr_accessor called :comments.&lt;br&gt;&lt;br&gt;Here is what I'd do: create small model objects that wrap the AR models. Use service objects to populate these POROs and prepare them exactly as the view needs it. Then the view is very simple: it iterates over these model objects and uses their properties.&lt;br&gt;I call these PORO objects DTOs or &lt;a href="http://martinfowler.com/eaaCatalog/dataTransferObject.html" target="_blank"&gt;Data Transfer Objects&lt;/a&gt;. They serve custom data from the model to the view.&lt;br&gt;&lt;br&gt;Here is how a UserDTO looks:&lt;br&gt;&lt;pre class="ruby" name="code"&gt;&lt;br /&gt;module DTO&lt;br /&gt;  class User&lt;br /&gt;    attr_reader :source_object, :id, :full_name&lt;br /&gt;    attr_accessor :comments&lt;br /&gt;    def initialize(source_object)&lt;br /&gt;      @source_object = source_object&lt;br /&gt;      @id = source_object.id&lt;br /&gt;      @full_name = source_object.full_name&lt;br /&gt;    end&lt;br /&gt;  end&lt;br /&gt;end&lt;br /&gt;&lt;/pre&gt;I keep a reference to the original AR model through the @source_object variable. Whatever field I can populate from the source object I do that in the object's initializer. But in our case there is an extra field that does not exist in the source model: comments. This field is declared but not yet populated. The service object will take care of that.&lt;br&gt;&lt;br&gt;The controller's index action has to do three things:&lt;ul&gt;&lt;li&gt;Get the currently viewed discussion from the database&lt;/li&gt;&lt;li&gt;Retrieve all the users&lt;/li&gt;&lt;li&gt;Find the users' comments under the current discussion&lt;/li&gt;&lt;/ul&gt;You could place all the code into the controller's action, but you'll have a bloated controller thats very hard to test and the logic will be impossible to reuse.&lt;br&gt;I use very granular service objects from the controller.&lt;pre class="ruby" name="code"&gt;&lt;br /&gt;# Services used in the app&lt;br /&gt;module Service&lt;br /&gt;  class FindsDiscussion&lt;br /&gt;    def self.for(id)&lt;br /&gt;      # This is very high level&lt;br /&gt;      ::DTO::Discussion.new(Discussion.find(id))&lt;br /&gt;    end&lt;br /&gt;  end&lt;br /&gt;&lt;br /&gt;  class FindsUsers&lt;br /&gt;    def self.all&lt;br /&gt;      User.all.map { |user| ::DTO::User.new(user) }&lt;br /&gt;    end&lt;br /&gt;  end&lt;br /&gt;&lt;br /&gt;  class SetsComments&lt;br /&gt;    def self.on_users(users, comments)&lt;br /&gt;      # There is no trip to the DB!&lt;br /&gt;      users.each do |user|&lt;br /&gt;        user.comments = comments.select do |comment|&lt;br /&gt;          user.source_object.id == comment.source_object.entered_by&lt;br /&gt;        end&lt;br /&gt;      end&lt;br /&gt;    end&lt;br /&gt;  end&lt;br /&gt;end&lt;br /&gt;&lt;/pre&gt;Look at how small they are! The first and second service looks up data in the database, but the third one is using an in memory lookup. &lt;span style="font-weight: bold; text-decoration: underline;"&gt;This is how I am saving the trip to the data store.&lt;/span&gt;&lt;br&gt;&lt;a href="http://en.wikipedia.org/wiki/Single_responsibility_principle" target="_blank"&gt;SRP&lt;/a&gt; is strictly followed, these little logic classes are super easy to test and using them from the controller is straightforward:&lt;pre class="ruby" name="code"&gt;&lt;br /&gt;class DiscussionsController &lt; ApplicationController&lt;br /&gt;  attr_reader :users, :discussion&lt;br /&gt;&lt;br /&gt;  def index&lt;br /&gt;    @users = Service::FindsUsers.all&lt;br /&gt;    @discussion = Service::FindsDiscussion.for(params[:id])&lt;br /&gt;    Service::SetsComments.on_users(@users, @discussion.comments)&lt;br /&gt;  end&lt;br /&gt;end&lt;br /&gt;&lt;/pre&gt;You are creating many more small classes, but that's OK. They are easy to understand, easy to test and you can use them like little LEGO blocks to construct the logic your controller needs.&lt;br&gt;&lt;br&gt;You can find the examples I used in the blog post in &lt;a href="https://gist.github.com/1201507" target="_blank"&gt;this Gist&lt;/a&gt;.&lt;div class="blogger-post-footer"&gt;&lt;img width='1' height='1' src='https://blogger.googleusercontent.com/tracker/5573355675806579511-6945579245729613793?l=www.adomokos.com' alt='' /&gt;&lt;/div&gt;</content><link rel='replies' type='application/atom+xml' href='http://www.adomokos.com/feeds/6945579245729613793/comments/default' title='Post Comments'/><link rel='replies' type='text/html' href='http://www.adomokos.com/2011/09/get-out-of-my-controller-and-from.html#comment-form' title='12 Comments'/><link rel='edit' type='application/atom+xml' href='http://www.blogger.com/feeds/5573355675806579511/posts/default/6945579245729613793'/><link rel='self' type='application/atom+xml' href='http://www.blogger.com/feeds/5573355675806579511/posts/default/6945579245729613793'/><link rel='alternate' type='text/html' href='http://www.adomokos.com/2011/09/get-out-of-my-controller-and-from.html' title='Get out of my Controller! And from Active Record, too!'/><author><name>Attila Domokos</name><uri>http://www.blogger.com/profile/09067995287578229487</uri><email>noreply@blogger.com</email><gd:image rel='http://schemas.google.com/g/2005#thumbnail' width='32' height='32' src='http://4.bp.blogspot.com/-jSMgYgaIqF8/TtJneb7-ZyI/AAAAAAAAB6k/U9Hd923CNvM/s220/adomokos_bw.png'/></author><media:thumbnail xmlns:media='http://search.yahoo.com/mrss/' url='http://2.bp.blogspot.com/-d9DAL_II9r0/Tl7m7O-lhYI/AAAAAAAAB1s/4RckInzn_6A/s72-c/AR_records_relationships.jpg' height='72' width='72'/><thr:total>12</thr:total></entry><entry><id>tag:blogger.com,1999:blog-5573355675806579511.post-4309251671733128554</id><published>2011-06-06T08:48:00.000-04:00</published><updated>2011-06-06T08:48:19.557-04:00</updated><title type='text'>Spec it or not?</title><content type='html'>I had a conversation with my friend &lt;a href="http://twitter.com/#!/joefiorini" target="_blank"&gt;Joe Fiorini&lt;/a&gt; a couple of days ago about this particular code:&lt;pre class="ruby" name="code"&gt;&lt;br /&gt;module ApplicationHelper&lt;br /&gt;  def gravatar_for(email)&lt;br /&gt;    image_tag Utils::Gravatar.for(email), size: "30x30"&lt;br /&gt;  end&lt;br /&gt;end&lt;br /&gt;&lt;/pre&gt;He showed me this code and my first question was: "Did you write tests for it?" He said: "Why? This is a simple method. What should I test?"&lt;br/&gt;We both agreed that there isn't much that could go wrong there.&lt;br/&gt;&lt;br/&gt;But:&lt;ol&gt;&lt;li&gt;Writing a spec against this code is quick and easy.&lt;/li&gt;&lt;li&gt;I'd much rather look at a spec documentation that describes this code than the code itself.&lt;/li&gt;&lt;li&gt;A spec describes intent.&lt;/li&gt;&lt;/ol&gt;It took me about 4 minutes to write this spec:&lt;pre class="ruby" name="code"&gt;&lt;br /&gt;require 'units/spec_helper'&lt;br /&gt;require 'application_helper'&lt;br /&gt;&lt;br /&gt;describe ApplicationHelper do&lt;br /&gt;  context "#gravatar_for(email)" do&lt;br /&gt;    specify "provides an image tag with Gravatar url" do&lt;br /&gt;      dummy = Object.new.extend ApplicationHelper # It's a module&lt;br /&gt;&lt;br /&gt;      Utils::Gravatar.stub(:for).and_return("some url")&lt;br /&gt;      dummy.stub(:image_tag).and_return("some image tag")&lt;br /&gt;&lt;br /&gt;      dummy.gravatar_for("some_email").should == "some image tag"&lt;br /&gt;    end&lt;br /&gt;  end&lt;br /&gt;end&lt;br /&gt;&lt;/pre&gt;I am not using mocks, I like to keep my tests "loose". I want to make sure when I call this helper I get the result I expect through the canned responses provided by the stubs.&lt;br/&gt;&lt;br/&gt;The image_tag helper needs a url. This url is provided by a utility class method, stubbed out on line 9.&lt;br/&gt;&lt;br/&gt;It's important to mention that I am not testing Rails' &lt;a href="https://github.com/rails/rails/blob/master/actionpack/lib/action_view/helpers/asset_tag_helper.rb#L344" target="_blank"&gt;image_tag&lt;/a&gt; helper. I leave that to the Rails core developers and contributors. I want to make sure that the method image_tag is recognized in the given context and it returns the string I expect.&lt;br/&gt;&lt;br/&gt;Once I execute the spec, this is the output:&lt;pre style="color: #707070"&gt;&lt;br /&gt;ApplicationHelper&lt;br /&gt;  #gravatar_for(email)&lt;br /&gt;    &lt;span style="color: #090"&gt;provides an image tag with Gravatar url&lt;/span&gt;&lt;br /&gt;&lt;br /&gt;Finished in 0.00151 seconds&lt;br /&gt;&lt;span style="color: #090"&gt;1 example, 0 failures&lt;/span&gt;&lt;br /&gt;&lt;/pre&gt;What did I mean by "communicating intent"?&lt;br/&gt;Let's say a new developer comes to the team and decides to change the gravatar_for method like this:&lt;pre class="ruby" name="code"&gt;&lt;br /&gt;def gravatar_for(email)&lt;br /&gt;  #image_tag Utils::Gravatar.for(email), size: "31x30"&lt;br /&gt;  url_for some_kind_of_named_route(email)&lt;br /&gt;end&lt;br /&gt;&lt;/pre&gt;As soon as he runs the spec the error is obvious:&lt;pre style="color: #707070"&gt;&lt;span style="color: #C00"&gt;F&lt;/span&gt;&lt;br /&gt;Failures:&lt;br /&gt;&lt;br /&gt;  1) ApplicationHelper#gravatar_for(email) provides an image tag with Gravatar url&lt;br /&gt;     &lt;span style="color: #C00"&gt;Failure/Error: dummy.gravatar_for("some_email").should == "some image tag"&lt;br /&gt;     NoMethodError:&lt;br /&gt;       undefined method `some_kind_of_named_route' for #&amp;lt;Object:0x01008e8838&amp;gt;&lt;/span&gt;&lt;br /&gt;     # ./app/helpers/application_helper.rb:7:in `gravatar_for'&lt;br /&gt;     # ./spec/units/helpers/application_helper_spec.rb:12:in `block (3 levels) in &amp;lt;top (required)&amp;gt;'&lt;br /&gt;&lt;br /&gt;Finished in 0.00139 seconds&lt;br /&gt;&lt;span style="color: #C00"&gt;1 example, 1 failure&lt;/span&gt;&lt;br /&gt;&lt;/pre&gt;To me this is a good warning sign that suggests the following: "You can change the method behavior, but the original developer meant it the way it's described in the spec. Now please fix the spec so it's green again. Oh, and make sure you run the full stack automated acceptance tests before you push your code."&lt;br/&gt;&lt;br/&gt;Knowing metaprogramming, stubbing and mocking makes it easy to write specs.&lt;br/&gt;I would have a totally different opinion if it took a lot more code and ceremony to do it.&lt;div class="blogger-post-footer"&gt;&lt;img width='1' height='1' src='https://blogger.googleusercontent.com/tracker/5573355675806579511-4309251671733128554?l=www.adomokos.com' alt='' /&gt;&lt;/div&gt;</content><link rel='replies' type='application/atom+xml' href='http://www.adomokos.com/feeds/4309251671733128554/comments/default' title='Post Comments'/><link rel='replies' type='text/html' href='http://www.adomokos.com/2011/06/spec-it-or-not.html#comment-form' title='1 Comments'/><link rel='edit' type='application/atom+xml' href='http://www.blogger.com/feeds/5573355675806579511/posts/default/4309251671733128554'/><link rel='self' type='application/atom+xml' href='http://www.blogger.com/feeds/5573355675806579511/posts/default/4309251671733128554'/><link rel='alternate' type='text/html' href='http://www.adomokos.com/2011/06/spec-it-or-not.html' title='Spec it or not?'/><author><name>Attila Domokos</name><uri>http://www.blogger.com/profile/09067995287578229487</uri><email>noreply@blogger.com</email><gd:image rel='http://schemas.google.com/g/2005#thumbnail' width='32' height='32' src='http://4.bp.blogspot.com/-jSMgYgaIqF8/TtJneb7-ZyI/AAAAAAAAB6k/U9Hd923CNvM/s220/adomokos_bw.png'/></author><thr:total>1</thr:total></entry><entry><id>tag:blogger.com,1999:blog-5573355675806579511.post-2266381772896876727</id><published>2011-04-15T09:18:00.000-04:00</published><updated>2011-04-15T09:18:55.095-04:00</updated><title type='text'>Running Rails Rspec Tests - Without Rails</title><content type='html'>I opened up my twitter client this afternoon and I saw "54 Messages, 28 Mentions". I tell you honestly, the first thought I had was: my twitter account had been hacked. Then I started to comb through the messages and I found out what happened. It all started with a tweet from &lt;a href="http://blog.densitypop.com/" target="_blank"&gt;Joe Fiorini&lt;/a&gt;.&lt;br/&gt;&lt;br/&gt;&lt;div class="separator" style="clear: both; text-align: center;"&gt;&lt;a href="http://3.bp.blogspot.com/-sH78ybrc9Iw/TaetEKtpZwI/AAAAAAAABjk/Z1qDnzXB0b8/s1600/joe_tweet.jpg" imageanchor="1" style="margin-left:1em; margin-right:1em"&gt;&lt;img border="0" height="64" width="400" src="http://3.bp.blogspot.com/-sH78ybrc9Iw/TaetEKtpZwI/AAAAAAAABjk/Z1qDnzXB0b8/s400/joe_tweet.jpg" /&gt;&lt;/a&gt;&lt;/div&gt;&lt;br/&gt;We both worked together on a large Rails application. The application was a little light on tests, so I asked the other developers why they are not writing more specs? The answer was all too familiar: "it just takes forever to run them". Yup, Rails had to load up, schema needed to be verified, the entire universe had to be included and 30 seconds later our specs were executed.&lt;br/&gt;&lt;br/&gt;We started creating POROs - Plain Old Ruby Objects - as pure services and put their RSpec tests into APP_ROOT/spec/units directory. Our goal was to keep the execution time under or around 2 seconds. Sure, it's easy when you don't have to load Rails controllers or active record models. But what happens when you have to?&lt;br/&gt;This post will explain that.&lt;br/&gt;&lt;br/&gt;The controller I used for this example is simple:&lt;pre class="ruby" name="code"&gt;class TracksController &lt; ApplicationController&lt;br /&gt;  def index&lt;br /&gt;    signed_in_user&lt;br /&gt;  end&lt;br /&gt;&lt;br /&gt;  def new&lt;br /&gt;    @track = Track.new&lt;br /&gt;  end&lt;br /&gt;&lt;br /&gt;  def create&lt;br /&gt;    feed = params[:track]["feed"]&lt;br /&gt;    @track = TrackParserService.parse(feed)&lt;br /&gt;&lt;br /&gt;    unless @track.valid?&lt;br /&gt;      render :action =&gt; 'new'&lt;br /&gt;      return&lt;br /&gt;    end&lt;br /&gt;&lt;br /&gt;    @track.save_with_user!(signed_in_user)&lt;br /&gt;&lt;br /&gt;    render :action =&gt; 'index'&lt;br /&gt;  end&lt;br /&gt;&lt;br /&gt;  def destroy&lt;br /&gt;    Track.find(params[:id]).destroy&lt;br /&gt;&lt;br /&gt;    @user = User.first&lt;br /&gt;    render :action =&gt; 'index'&lt;br /&gt;  end&lt;br /&gt;&lt;br /&gt;  private&lt;br /&gt;&lt;br /&gt;  def signed_in_user&lt;br /&gt;    # No authentication yet&lt;br /&gt;    @user ||= User.first&lt;br /&gt;  end&lt;br /&gt;end&lt;br /&gt;&lt;/pre&gt;The first controller action I wanted to test was "index".&lt;br/&gt;&lt;br/&gt;I created the directory structure APP_ROOT/spec/units/controllers and saved my file in this directory under the name tracks_controller_spec.rb.&lt;br/&gt;&lt;br/&gt;I started out with this code:&lt;pre class="ruby" name="code"&gt;APP_ROOT = File.expand_path(File.join(File.dirname(__FILE__), "..", "..", ".."))&lt;br /&gt;$: &lt;&lt; File.join(APP_ROOT, "app/controllers")&lt;br /&gt;&lt;br /&gt;require 'tracks_controller'&lt;br /&gt;&lt;br /&gt;describe TracksController do&lt;br /&gt;  &lt;br /&gt;end&lt;br /&gt;&lt;/pre&gt;You could move the first two lines into a spec_helper, I wanted to keep it here for clarity.&lt;br/&gt;&lt;br/&gt;I received the following error:&lt;br/&gt;&lt;div style="color: #c00"&gt;`const_missing': uninitialized constant Object::ApplicationController (NameError)&lt;/div&gt;&lt;br/&gt;No worries: TracksController inherits from ApplicationController, it's part of my app, I just had to require it.&lt;pre class="ruby" name="code"&gt;require 'application_controller'&lt;br /&gt;&lt;/pre&gt;And the error:&lt;br/&gt;&lt;div style="color: #c00"&gt;`const_missing': uninitialized constant Object::ActionController (NameError)&lt;/div&gt;&lt;br/&gt;This was the point where I had to require Rails.&lt;br/&gt;&lt;br/&gt;Instead of doing that, I just defined the class myself so the controller was aware of it. I also needed to declare the class method "protect_from_forgery", but I left the implementation blank. Please note that the class declaration is above the require statements.&lt;br/&gt;Here is the entire spec after my changes:&lt;pre class="ruby" name="code"&gt;APP_ROOT = File.expand_path(File.join(File.dirname(__FILE__), "..", "..", ".."))&lt;br /&gt;$: &lt;&lt; File.join(APP_ROOT, "app/controllers")&lt;br /&gt;&lt;br /&gt;# A test double for ActionController::Base&lt;br /&gt;module ActionController&lt;br /&gt;  class Base&lt;br /&gt;    def self.protect_from_forgery; end&lt;br /&gt;  end&lt;br /&gt;end&lt;br /&gt;&lt;br /&gt;require 'application_controller'&lt;br /&gt;require 'tracks_controller'&lt;br /&gt;&lt;br /&gt;describe TracksController do&lt;br /&gt;  &lt;br /&gt;end&lt;br /&gt;&lt;/pre&gt;Running the spec:&lt;br/&gt;&lt;br/&gt;Finished in 0.00003 seconds&lt;div style="color: #090"&gt;0 examples, 0 failures&lt;/div&gt;&lt;br/&gt;The first test just ensures that the User active record model will load the first user if the @user instance is nil.&lt;pre class="ruby" name="code"&gt;describe TracksController do&lt;br /&gt;  let(:controller) { TracksController.new }&lt;br /&gt;&lt;br /&gt;  specify "index action returns the signed_in_user" do&lt;br /&gt;    # setup&lt;br /&gt;    user = stub&lt;br /&gt;    User.stub(:first).and_return user&lt;br /&gt;&lt;br /&gt;    # execute action under test&lt;br /&gt;    returned_user = controller.index&lt;br /&gt;&lt;br /&gt;    # verify&lt;br /&gt;    returned_user.should == user&lt;br /&gt;    controller.instance_variable_get(:@user).should == user&lt;br /&gt;  end&lt;br /&gt;end&lt;br /&gt;&lt;/pre&gt;The test is straightforward. User model is returning a stub - I don't really care what that returned object is, I just check if they're the same object. In the verification part I made sure that the instance variable was set properly. Great that your can check an un-exposed field on a object with a little bit of metaprogramming?&lt;br/&gt;&lt;br/&gt;I executed the spec and received the following error:&lt;br/&gt;&lt;br/&gt;Failures:&lt;br/&gt;&lt;br/&gt;&amp;nbsp;&amp;nbsp;1) TracksController index action returns the signed_in_user&lt;div style="color: #C00"&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;Failure/Error: User.stub(:first).and_return user&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;NameError:&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;uninitialized constant RSpec::Core::ExampleGroup::Nested_1::User&lt;/div&gt;&lt;br/&gt;Well, I need to require the User model to fix this error. Or do I? I am not using any functionality of the User class - whatever I am using is stubbed out. I just defined the class without any implementation.&lt;br/&gt;&lt;br/&gt;This line was added to the spec right above the describe block.&lt;pre class="ruby" name="code"&gt;class User; end&lt;br /&gt;&lt;/pre&gt;I execute the test and it's all green.&lt;br/&gt;&lt;br/&gt;TracksController&lt;br/&gt;&amp;nbsp;&amp;nbsp;&lt;span style="color: #090"&gt;index action returns the signed in user&lt;/span&gt;&lt;br/&gt;&lt;br/&gt;Finished in 0.00079 seconds&lt;span style="color: #090"&gt;1 example, 0 failures&lt;/span&gt;1.26s user 0.28s system 99% cpu &lt;span style="font-weight: bold;"&gt;1.546 total&lt;/span&gt;&lt;br/&gt;&lt;br/&gt;1.5 seconds is not all that bad to run a controller action test.&lt;br/&gt;&lt;br/&gt;Let me describe how I tested the "create" action.&lt;br/&gt;Take a look at the controller code above and review what it does. The @track instance is constructed by the TrackParserService class' parse method. Then active record validates it and if the model is invalid the controller's "new" action is rendered.&lt;br/&gt;&lt;br/&gt;Here is the spec for that:&lt;pre class="ruby" name="code"&gt;context "when the model is not valid" do&lt;br /&gt;  it "renders action =&gt; 'new'" do&lt;br /&gt;    # define a method for params - TracksController is unaware of it&lt;br /&gt;    controller.class.send(:define_method, :params) do&lt;br /&gt;      {:track =&gt; "feed"}&lt;br /&gt;    end&lt;br /&gt;&lt;br /&gt;    track = stub(:valid? =&gt; false)&lt;br /&gt;    TrackParserService.stub(:parse).and_return(track)&lt;br /&gt;&lt;br /&gt;    render_hash = {}&lt;br /&gt;    # hang on to the input hash the render method is invoked with&lt;br /&gt;    # I'll use it to very that the render argument is correct&lt;br /&gt;    controller.class.send(:define_method, :render) do |hash_argument|&lt;br /&gt;      render_hash = hash_argument&lt;br /&gt;    end&lt;br /&gt;&lt;br /&gt;    controller.create&lt;br /&gt;&lt;br /&gt;    # verify the render was called with the right hash&lt;br /&gt;    render_hash.should == { :action =&gt; 'new' }&lt;br /&gt;  end&lt;br /&gt;end&lt;br /&gt;&lt;/pre&gt;I used Ruby's metaprogramming again to set up the params hash. It really doesn't matter what's in it, since I stub out the TrackParserService. The method "render" comes from Rails, I had to define that as well. Please note that I record what the render method was invoked with, this way I can verify that the input hash was correct.&lt;br/&gt;I also had to define - with no implementation - the Track and TrackParserService classes.&lt;br/&gt;&lt;br/&gt;When I executed the specs, all of them passed:&lt;br/&gt;&lt;br/&gt;TracksController&lt;br/&gt;&lt;span style="color: #090"&gt;&amp;nbsp;&amp;nbsp;index action returns the signed in user&lt;/span&gt;&lt;br/&gt;&lt;span style="color: #090"&gt;&amp;nbsp;&amp;nbsp;new action returns an instance of Track&lt;/span&gt;&lt;br/&gt;&amp;nbsp;&amp;nbsp;when the model is not valid&lt;br/&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&lt;span style="color: #090"&gt;renders action =&gt; 'new'&lt;/span&gt;&lt;br/&gt;&lt;br/&gt;Finished in 0.00203 seconds&lt;br/&gt;&lt;span style="color: #090"&gt;3 examples, 0 failures&lt;/span&gt;&lt;br/&gt;bundle exec rspec spec/units/controllers/tracks_controller_spec.rb -fd  1.32s user 0.29s system 99% cpu &lt;span style="font-weight: bold;"&gt;1.614 total&lt;/span&gt;&lt;br/&gt;&lt;br/&gt;You can review the entire example in this &lt;a href="https://gist.github.com/921111" target="_blank"&gt;gist&lt;/a&gt;.&lt;br/&gt;&lt;br/&gt;This code is rough. I just used it to show you how we try to keep our test execution fast. I acknowledge that I am doing some very dangerous stubbing here. However, I have the higher level cucumber tests to protect me against unexpected errors.&lt;br/&gt;&lt;br/&gt;I can't tell you what it means to run all of my 150+ specs within 2 seconds. I think it's a little bit of an extra work, but it's well worth the effort!&lt;div class="blogger-post-footer"&gt;&lt;img width='1' height='1' src='https://blogger.googleusercontent.com/tracker/5573355675806579511-2266381772896876727?l=www.adomokos.com' alt='' /&gt;&lt;/div&gt;</content><link rel='replies' type='application/atom+xml' href='http://www.adomokos.com/feeds/2266381772896876727/comments/default' title='Post Comments'/><link rel='replies' type='text/html' href='http://www.adomokos.com/2011/04/running-rails-rspec-tests-without-rails.html#comment-form' title='20 Comments'/><link rel='edit' type='application/atom+xml' href='http://www.blogger.com/feeds/5573355675806579511/posts/default/2266381772896876727'/><link rel='self' type='application/atom+xml' href='http://www.blogger.com/feeds/5573355675806579511/posts/default/2266381772896876727'/><link rel='alternate' type='text/html' href='http://www.adomokos.com/2011/04/running-rails-rspec-tests-without-rails.html' title='Running Rails Rspec Tests - Without Rails'/><author><name>Attila Domokos</name><uri>http://www.blogger.com/profile/09067995287578229487</uri><email>noreply@blogger.com</email><gd:image rel='http://schemas.google.com/g/2005#thumbnail' width='32' height='32' src='http://4.bp.blogspot.com/-jSMgYgaIqF8/TtJneb7-ZyI/AAAAAAAAB6k/U9Hd923CNvM/s220/adomokos_bw.png'/></author><media:thumbnail xmlns:media='http://search.yahoo.com/mrss/' url='http://3.bp.blogspot.com/-sH78ybrc9Iw/TaetEKtpZwI/AAAAAAAABjk/Z1qDnzXB0b8/s72-c/joe_tweet.jpg' height='72' width='72'/><thr:total>20</thr:total></entry><entry><id>tag:blogger.com,1999:blog-5573355675806579511.post-4795843912645798037</id><published>2011-04-10T14:12:00.001-04:00</published><updated>2011-04-10T14:38:50.873-04:00</updated><title type='text'>Rapid Feedback</title><content type='html'>I am not a &lt;a href="http://en.wikipedia.org/wiki/Windows_Presentation_Foundation" target="_blank"&gt;WPF&lt;/a&gt; expert. In fact, I don't know it well enough. But let me tell you what it was like working on a WPF project last year.&lt;br /&gt;&lt;br /&gt;&lt;div class="separator" style="clear: both; text-align: center;"&gt;&lt;a href="http://1.bp.blogspot.com/-dcUoEwu-g6w/TaGc32Pu0TI/AAAAAAAABjc/3ahnEy0VO_U/s1600/IMG_7152.JPG" imageanchor="1" style="margin-left:1em; margin-right:1em"&gt;&lt;img border="0" height="240" width="320" src="http://1.bp.blogspot.com/-dcUoEwu-g6w/TaGc32Pu0TI/AAAAAAAABjc/3ahnEy0VO_U/s320/IMG_7152.JPG" /&gt;&lt;/a&gt;&lt;/div&gt;&lt;br /&gt;It was 10 o'clock in the morning and I sat in my cubicle. I had to pull the latest changes from the source control server. I had to run a couple of batch files to compile all the code that took about 3 minutes. In the mean time I fired up Visual Studio 2010 and ran my latest unit tests to make sure everything was in good shape.&lt;br /&gt;&lt;br /&gt;I started up the &lt;a href="http://en.wikipedia.org/wiki/Windows_Communication_Foundation" target="_blank"&gt;WCF&lt;/a&gt; services, 86 of them, and a little later I was ready to run the UI app. It took another 30 to 40 seconds to load and get to the login screen. I logged in and selected from the menu where I wanted to get to. That page was a list of items, I had to select one of them just to get to the detail page. Finally, I was there!&lt;br /&gt;&lt;br /&gt;Let me sum it up:&lt;br /&gt;* 180 seconds to compile the app&lt;br /&gt;* &amp;nbsp;&amp;nbsp;50 seconds to fire up all WPF services&lt;br /&gt;* &amp;nbsp;&amp;nbsp;20 seconds to start the WPF UI App&lt;br /&gt;&lt;span style="text-decoration: underline;"&gt;* &amp;nbsp;&amp;nbsp;60 seconds to log in and go to the page I had to modify&lt;/span&gt;&lt;br /&gt;&lt;span style="font-weight: bold;"&gt;TOTAL:&lt;/span&gt; 310 seconds&lt;br /&gt;&lt;br /&gt;My task was adding a new TextBox to this page. Simple. I opened up the &lt;a href="http://en.wikipedia.org/wiki/Xaml" target="_blank"&gt;XAML&lt;/a&gt; file which was an ugly xml file with weird namespaces and special attributes all over. I grabbed a TextBox XAML code from somewhere, pasted it in, made sure all the namespaces were fine and I was ready to run it.&lt;br /&gt;&lt;br /&gt;I had to shut down the UI app, compile the UI project, start it up again, log in, select the menu option to get to the list page and choose one item to see the detail.&lt;br /&gt;&lt;br /&gt;Here is how long this took:&lt;br /&gt;* 30 seconds to compile the UI app&lt;br /&gt;* 20 seconds to start the WPF UI App&lt;br /&gt;&lt;span style="text-decoration: underline;"&gt;* 60 seconds to log in and go to the page I had to modify&lt;/span&gt;&lt;br /&gt;&lt;span style="font-weight: bold;"&gt;TOTAL:&lt;/span&gt; 110 seconds&lt;br /&gt;&lt;br /&gt;And it turned out that I did not set up the Grid for this TextBox properly, so I had to do some minor tweaks to the XAML page. I did that, killed the UI app, complied the code, ran the WPF UI app, logged in, went to the page and 110 - or one hundred and ten - seconds later I verified that all look good.&lt;br /&gt;&lt;br /&gt;But this was the fast part. Once I had all the UI set up properly, I had to get under the hood and modify the domain object. The change was "simple": just add a text field to the database, modify the domain object, set up the NHIbernate mapping, change the Data Transfer Object, add this field to it and set up its mapping if I had to.&lt;br /&gt;Now to make sure all this worked I had to shut down the UI app, the WPF services. Compile the code, regenerate the NHibernate mappings, fire up the WPF services, run the UI, log in, select the page and pick an item to get to its detail. Simple, right?&lt;br /&gt;&lt;br /&gt;Here is the break down:&lt;br /&gt;* 30 seconds to compile the Data Access Code&lt;br /&gt;* 30 seconds to regenerate the NHibernate mapping xml&lt;br /&gt;* 50 seconds to fire up all WCF services&lt;br /&gt;* 20 seconds to start the WPF UI App&lt;br /&gt;&lt;span style="text-decoration: underline;"&gt;* 60 seconds to log in and make sure that all looks good&lt;/span&gt;&lt;br /&gt;&lt;span style="font-weight: bold;"&gt;TOTAL:&lt;/span&gt; 190 seconds&lt;br /&gt;&lt;br /&gt;Wait! 3 minutes just to see if everything is working properly?&lt;br /&gt;&lt;br /&gt;Give me a break.&lt;br /&gt;&lt;br /&gt;What company with a tight budget and ever approaching deadlines could afford spending 3 minutes just to see if a simple change is functioning properly or not? Who would dare to touch the existing code to clean it up a bit?&lt;br /&gt;&lt;br /&gt;One of the great things I like about working with Ruby and Rails is the rapid feedback. No, I am not talking about how long it takes to execute my - Rails disconnected RSpec - tests. (I'll try to write about that in an upcoming post.) I just change the code, hit the browser's refresh button and about 5 seconds later I have the page loaded, the session preserved and I have the answer.&lt;br /&gt;&lt;br /&gt;I am talking about 5 seconds and not a couple of minutes.&lt;div class="blogger-post-footer"&gt;&lt;img width='1' height='1' src='https://blogger.googleusercontent.com/tracker/5573355675806579511-4795843912645798037?l=www.adomokos.com' alt='' /&gt;&lt;/div&gt;</content><link rel='replies' type='application/atom+xml' href='http://www.adomokos.com/feeds/4795843912645798037/comments/default' title='Post Comments'/><link rel='replies' type='text/html' href='http://www.adomokos.com/2011/04/rapid-feedback.html#comment-form' title='5 Comments'/><link rel='edit' type='application/atom+xml' href='http://www.blogger.com/feeds/5573355675806579511/posts/default/4795843912645798037'/><link rel='self' type='application/atom+xml' href='http://www.blogger.com/feeds/5573355675806579511/posts/default/4795843912645798037'/><link rel='alternate' type='text/html' href='http://www.adomokos.com/2011/04/rapid-feedback.html' title='Rapid Feedback'/><author><name>Attila Domokos</name><uri>http://www.blogger.com/profile/09067995287578229487</uri><email>noreply@blogger.com</email><gd:image rel='http://schemas.google.com/g/2005#thumbnail' width='32' height='32' src='http://4.bp.blogspot.com/-jSMgYgaIqF8/TtJneb7-ZyI/AAAAAAAAB6k/U9Hd923CNvM/s220/adomokos_bw.png'/></author><media:thumbnail xmlns:media='http://search.yahoo.com/mrss/' url='http://1.bp.blogspot.com/-dcUoEwu-g6w/TaGc32Pu0TI/AAAAAAAABjc/3ahnEy0VO_U/s72-c/IMG_7152.JPG' height='72' width='72'/><thr:total>5</thr:total></entry><entry><id>tag:blogger.com,1999:blog-5573355675806579511.post-3811503514917831986</id><published>2011-02-16T17:03:00.000-05:00</published><updated>2011-02-16T17:03:18.651-05:00</updated><title type='text'>First Month Without Windows</title><content type='html'>&lt;p&gt;It's been a little over than a month since I left the Microsoft Universe behind. I had used OS X and Linux before - mostly in the evenings and weekends - but I made the jump finally and I only touch Windows when I browse the web on my wife's laptop. And I just couldn't be happier...&lt;/p&gt;&lt;div style="font-weight: bold;"&gt;Life on the Mac&lt;/div&gt;&lt;p&gt;I have a pretty powerful Mac. It has an Intel Core i5 CPU and I upgraded the RAM from 4GB to 8GB as soon as I received it. I don't have an SSD just yet, but I am still pretty happy with its performance. Whenever I feel like checking the current state of the machine I just run "&lt;a href="http://htop.sourceforge.net/" target="_blank"&gt;htop&lt;/a&gt;" in the terminal.&lt;br /&gt;&lt;/p&gt;&lt;p&gt;I started out using Firefox for my work email, but I write automated tests in it as well and I had to find another solution. "&lt;a href="http://www.docondev.com/2011/01/squirrel.html" target="_blank"&gt;Before I ran out of browsers&lt;/a&gt;" I investigated my options. I could have purchased &lt;a href="http://mailplaneapp.com/" target="_blank"&gt;Mailplane&lt;/a&gt; but I did not feel I needed a full blown app for it. I found &lt;a href="http://fluidapp.com/" target="_blank"&gt;Fluid&lt;/a&gt;, a site specific browser. Now I can open my work email just like any other application and I am still using a browser inside. I found a nice PNG file that I set up with it and the app looks just like a native app when I tab between applications. Here is how it appears under the Applications folder:&lt;br /&gt;&lt;/p&gt;&lt;div class="separator" style="clear: both; text-align: center;"&gt;&lt;a href="http://4.bp.blogspot.com/-fpoKaCHHbrE/TVhKlFgd9TI/AAAAAAAABh4/2vywThSKFlA/s1600/W3Mail.jpg" imageanchor="1" style="margin-left:1em; margin-right:1em"&gt;&lt;img border="0" height="92" width="104" src="http://4.bp.blogspot.com/-fpoKaCHHbrE/TVhKlFgd9TI/AAAAAAAABh4/2vywThSKFlA/s320/W3Mail.jpg" /&gt;&lt;/a&gt;&lt;/div&gt;&lt;p&gt;To organize my thoughts and notes I started using &lt;a href="http://www.evernote.com/" target="_blank"&gt;Evernote&lt;/a&gt;. It's a great tool for note taking but I think it does an even better job at &lt;span style="font-weight: bold;"&gt;organizing&lt;/span&gt; them. I even installed the &lt;a href="https://chrome.google.com/extensions/detail/pioclpoplcdbaefihamjohnefbikjilc" target="_blank"&gt;Evernote Chrome extension&lt;/a&gt;:&lt;br /&gt;&lt;/p&gt;&lt;div class="separator" style="clear: both; text-align: center;"&gt;&lt;a href="http://2.bp.blogspot.com/-CqdZKoTaKKU/TVhN3sdtdOI/AAAAAAAABiA/tSQTtpqVlkk/s1600/evernote_chrome_extension.jpg" imageanchor="1" style="margin-left:1em; margin-right:1em"&gt;&lt;img border="0" height="67" width="234" src="http://2.bp.blogspot.com/-CqdZKoTaKKU/TVhN3sdtdOI/AAAAAAAABiA/tSQTtpqVlkk/s320/evernote_chrome_extension.jpg" /&gt;&lt;/a&gt;&lt;/div&gt;&lt;p&gt;I don't use the mouse - or track pad - to launch an application. First I used Spotlight but switched to &lt;a href="http://www.alfredapp.com/" target="_blank"&gt;Alfred App&lt;/a&gt; recently. It's fast and I can use keyboard shortcuts. This image tells it all:&lt;br /&gt;&lt;/p&gt;&lt;div class="separator" style="clear: both; text-align: center;"&gt;&lt;a href="http://1.bp.blogspot.com/-w90zgYSn4go/TVhPOPJ-_0I/AAAAAAAABiI/vqJjUd13QXo/s1600/alfred.jpg" imageanchor="1" style="margin-left:1em; margin-right:1em"&gt;&lt;img border="0" height="130" width="320" src="http://1.bp.blogspot.com/-w90zgYSn4go/TVhPOPJ-_0I/AAAAAAAABiI/vqJjUd13QXo/s320/alfred.jpg" /&gt;&lt;/a&gt;&lt;/div&gt;&lt;p&gt;&lt;div style="font-weight: bold;"&gt;Living my life in the Terminal (iTerm)&lt;/div&gt;&lt;/p&gt;&lt;p&gt;After using the terminal for a couple of weeks I switched to &lt;a href="http://sites.google.com/site/iterm2home/" target="_blank"&gt;iTerm2&lt;/a&gt;. I don't use all its neat features just yet but I do like the split view mode. I have cucumber features running on one side and rails logs on the other.&lt;br /&gt;&lt;/p&gt;&lt;p&gt;I am still learning (who doesn't) and tweaking all the different configuration options of my ~/.vimrc and ~/.zshrc files. I am particularly happy with this addition to my ~/.vimrc file that ignores my arrow keys when I am in command mode. No more arrows to move around!&lt;br /&gt;&lt;/p&gt;&lt;pre class="html" name="code"&gt;" Ignore arrow keys in vim&lt;br /&gt;:map &amp;lt;Left&amp;gt; &amp;lt;Nop&amp;gt;&lt;br /&gt;:map &amp;lt;Right&amp;gt; &amp;lt;Nop&amp;gt;&lt;br /&gt;:map &amp;lt;Up&amp;gt; &amp;lt;Nop&amp;gt;&lt;br /&gt;:map &amp;lt;Down&amp;gt; &amp;lt;Nop&amp;gt;&lt;br /&gt;:map &amp;lt;PageUp&amp;gt; &amp;lt;Nop&amp;gt;&lt;br /&gt;:map &amp;lt;PageDown&amp;gt; &amp;lt;Nop&amp;gt;&lt;br /&gt;:map &amp;lt;Home&amp;gt; &amp;lt;Nop&amp;gt;&lt;br /&gt;:map &amp;lt;End&amp;gt; &amp;lt;Nop&amp;gt;&lt;br /&gt;&lt;br /&gt;:map! &amp;lt;Left&amp;gt; &amp;lt;Nop&amp;gt;&lt;br /&gt;:map! &amp;lt;Right&amp;gt; &amp;lt;Nop&amp;gt;&lt;br /&gt;:map! &amp;lt;Up&amp;gt; &amp;lt;Nop&amp;gt;&lt;br /&gt;:map! &amp;lt;Down&amp;gt; &amp;lt;Nop&amp;gt;&lt;br /&gt;:map! &amp;lt;PageUp&amp;gt; &amp;lt;Nop&amp;gt;&lt;br /&gt;:map! &amp;lt;PageDown&amp;gt; &amp;lt;Nop&amp;gt;&lt;br /&gt;:map! &amp;lt;Home&amp;gt; &amp;lt;Nop&amp;gt;&lt;br /&gt;:map! &amp;lt;End&amp;gt; &amp;lt;Nop&amp;gt;&lt;br /&gt;&lt;/pre&gt;&lt;p&gt;I even started listening to &lt;a href="http://www.pandora.com/" target="_blank"&gt;Pandora&lt;/a&gt; in the terminal through &lt;a href="https://github.com/PromyLOPh/pianobar" target="_blank"&gt;Pianobar&lt;/a&gt;.&lt;br /&gt;&lt;/p&gt;&lt;div class="separator" style="clear: both; text-align: center;"&gt;&lt;a href="http://4.bp.blogspot.com/-yA0_h4ovsgs/TVicrTyocjI/AAAAAAAABig/DIicp6O9KIw/s1600/pianobar.jpg" imageanchor="1" style="margin-left:1em; margin-right:1em"&gt;&lt;img border="0" height="142" width="400" src="http://4.bp.blogspot.com/-yA0_h4ovsgs/TVicrTyocjI/AAAAAAAABig/DIicp6O9KIw/s400/pianobar.jpg" /&gt;&lt;/a&gt;&lt;/div&gt;&lt;p&gt;I have used Git before, but my skills are not where it should be. I started reading the book &lt;a href="http://pragprog.com/titles/tsgit/pragmatic-version-control-using-git" target="_blank"&gt;Pragmatic Version Control Using Git&lt;/a&gt; which I'd recommend to anybody who wants to go deep with Git. You should also check out the great &lt;a href="http://gitimmersion.com/" target="_blank"&gt;Git Immersion class&lt;/a&gt; created by &lt;a href="http://edgecase.com/" target="_blank"&gt;EdgeCase&lt;/a&gt;.&lt;br /&gt;&lt;/p&gt;&lt;div style="font-weight: bold;"&gt;My Typing Sucks&lt;/div&gt;&lt;p&gt;Well, maybe it's not that bad, but my typing could and should improve. It's just not fast enough and I don't use all my fingers. I need to take my eyes off the monitor and look at the keyboard when I try to use special key commands that I have not used much before. And my typing is not accurate, I frequently have to go back and fix words that I mistyped.&lt;br /&gt;Unacceptable. I wonder if companies should check in an interview how well a candidate can type.&lt;br /&gt;&lt;br /&gt;I used the web site &lt;a href="http://www.typingweb.com/" target="_blank"&gt;typingweb.com&lt;/a&gt; and an app called &lt;a href="http://web.me.com/typetrainer4mac/aTypeTrainer4Mac/home.html" target="_blank"&gt;aTypeTrainer4Mac&lt;/a&gt; to practice. I am still not where I'd like to be, but I am working on it.&lt;br /&gt;&lt;/p&gt;&lt;p&gt;Thanks to &lt;a href="http://joefiorini.com/" target="_blank"&gt;Joe Fiorini&lt;/a&gt; for showing me endless tips mentioned in this blog post.&lt;br /&gt;&lt;/p&gt;&lt;div class="blogger-post-footer"&gt;&lt;img width='1' height='1' src='https://blogger.googleusercontent.com/tracker/5573355675806579511-3811503514917831986?l=www.adomokos.com' alt='' /&gt;&lt;/div&gt;</content><link rel='replies' type='application/atom+xml' href='http://www.adomokos.com/feeds/3811503514917831986/comments/default' title='Post Comments'/><link rel='replies' type='text/html' href='http://www.adomokos.com/2011/02/first-month-without-windows.html#comment-form' title='2 Comments'/><link rel='edit' type='application/atom+xml' href='http://www.blogger.com/feeds/5573355675806579511/posts/default/3811503514917831986'/><link rel='self' type='application/atom+xml' href='http://www.blogger.com/feeds/5573355675806579511/posts/default/3811503514917831986'/><link rel='alternate' type='text/html' href='http://www.adomokos.com/2011/02/first-month-without-windows.html' title='First Month Without Windows'/><author><name>Attila Domokos</name><uri>http://www.blogger.com/profile/09067995287578229487</uri><email>noreply@blogger.com</email><gd:image rel='http://schemas.google.com/g/2005#thumbnail' width='32' height='32' src='http://4.bp.blogspot.com/-jSMgYgaIqF8/TtJneb7-ZyI/AAAAAAAAB6k/U9Hd923CNvM/s220/adomokos_bw.png'/></author><media:thumbnail xmlns:media='http://search.yahoo.com/mrss/' url='http://4.bp.blogspot.com/-fpoKaCHHbrE/TVhKlFgd9TI/AAAAAAAABh4/2vywThSKFlA/s72-c/W3Mail.jpg' height='72' width='72'/><thr:total>2</thr:total></entry><entry><id>tag:blogger.com,1999:blog-5573355675806579511.post-8818721806049970749</id><published>2010-12-26T20:51:00.002-05:00</published><updated>2010-12-27T09:26:35.446-05:00</updated><title type='text'>Ruby Mocks vs Stubs - CleRB Presentation</title><content type='html'>I talked to somebody a while ago about a line of code that had tremendous beauty in my eyes:&lt;br /&gt;&lt;pre class="ruby" name="code"&gt;setup_response = stub(:token =&gt;'xyz_some_token')&lt;br /&gt;&lt;/pre&gt;This might seem strange to someone new to dynamic languages. The stub object returns the string 'xyz_some_token' when the "token message" is sent to it. I have no idea - and I don't really care - what type of object it is. What really matters is that is has a canned response for the "token message".&lt;br /&gt;&lt;br /&gt;He suggested that I should do a talk on this. I submitted the idea to our &lt;a href="http://www.meetup.com/ClevelandRuby/" target="_blank"&gt;local Ruby user group&lt;/a&gt; and &lt;a href="http://www.docondev.com/" target="_blank"&gt;Michael "Doc" Norton&lt;/a&gt;  - the user group organizer - asked me to present it.&lt;br /&gt;&lt;br /&gt;Preparing for a presentation is hard - takes time and effort - but I learned so much from it that I would and I will do it again!&lt;br /&gt;&lt;br /&gt;I used the Order - Warehouse example from &lt;a href="http://martinfowler.com/" target="_blank"&gt;Martin Fowler&lt;/a&gt;'s &lt;a href="http://martinfowler.com/articles/mocksArentStubs.html" target="_blank"&gt;Mocks Aren't Stubs&lt;/a&gt; writing. I also wrote a Twitter client where I used mocking/stubbing in the controller tests and &lt;a href="http://fakeweb.rubyforge.org/" target="_blank"&gt;Fakeweb&lt;/a&gt; to stub out http calls from Cucumber.&lt;br /&gt;&lt;br /&gt;After the talk we had the following conclusions:&lt;br /&gt;* Although Stubs are not as sophisticated as mocks, they are really powerful and reflect clean code&lt;br /&gt;* Try to use stubs over mocks&lt;br /&gt;* Abused mocking could be a code smell -&gt; introduce abstraction and use stubs &lt;br /&gt;&lt;br /&gt;The examples from the talk are in &lt;a href="https://github.com/adomokos/mocks_vs_stubs" target="_blank"&gt;my github repository&lt;/a&gt;.&lt;br /&gt;&lt;br /&gt;&lt;div style="text-align: center;"&gt;&lt;div&gt;&lt;a href="http://www.slideshare.net/AttilaDomokos/mock-vs-stubs-clerb-presentation" title="Mock vs Stubs CleRB Presentation" style="font-weight: bold;"&gt;Mock vs Stubs CleRB Presentation&lt;/a&gt;&lt;object id="__sse6129521" width="425" height="355"&gt;&lt;param name="movie" value="http://static.slidesharecdn.com/swf/ssplayer2.swf?doc=presentation-101212104534-phpapp01&amp;stripped_title=mock-vs-stubs-clerb-presentation&amp;userName=AttilaDomokos" /&gt;&lt;param name="allowFullScreen" value="true"/&gt;&lt;param name="allowScriptAccess" value="always"/&gt;&lt;embed name="__sse6129521" src="http://static.slidesharecdn.com/swf/ssplayer2.swf?doc=presentation-101212104534-phpapp01&amp;stripped_title=mock-vs-stubs-clerb-presentation&amp;userName=AttilaDomokos" type="application/x-shockwave-flash" allowscriptaccess="always" allowfullscreen="true" width="425" height="355"&gt;&lt;/embed&gt;&lt;/object&gt;&lt;br /&gt;&lt;/div&gt;&lt;/div&gt;&lt;br /&gt;I'd like to thank &lt;a href="http://joefiorini.com/" target="_blank"&gt;Joe Fiorini&lt;/a&gt; for meeting with me a couple of days before my talk. He had great ideas that I used in my presentation. Thanks for it!&lt;div class="blogger-post-footer"&gt;&lt;img width='1' height='1' src='https://blogger.googleusercontent.com/tracker/5573355675806579511-8818721806049970749?l=www.adomokos.com' alt='' /&gt;&lt;/div&gt;</content><link rel='replies' type='application/atom+xml' href='http://www.adomokos.com/feeds/8818721806049970749/comments/default' title='Post Comments'/><link rel='replies' type='text/html' href='http://www.adomokos.com/2010/12/ruby-mocks-vs-stubs-clerb-presentation.html#comment-form' title='0 Comments'/><link rel='edit' type='application/atom+xml' href='http://www.blogger.com/feeds/5573355675806579511/posts/default/8818721806049970749'/><link rel='self' type='application/atom+xml' href='http://www.blogger.com/feeds/5573355675806579511/posts/default/8818721806049970749'/><link rel='alternate' type='text/html' href='http://www.adomokos.com/2010/12/ruby-mocks-vs-stubs-clerb-presentation.html' title='Ruby Mocks vs Stubs - CleRB Presentation'/><author><name>Attila Domokos</name><uri>http://www.blogger.com/profile/09067995287578229487</uri><email>noreply@blogger.com</email><gd:image rel='http://schemas.google.com/g/2005#thumbnail' width='32' height='32' src='http://4.bp.blogspot.com/-jSMgYgaIqF8/TtJneb7-ZyI/AAAAAAAAB6k/U9Hd923CNvM/s220/adomokos_bw.png'/></author><thr:total>0</thr:total></entry><entry><id>tag:blogger.com,1999:blog-5573355675806579511.post-744949885711243489</id><published>2010-11-13T22:06:00.003-05:00</published><updated>2010-11-14T13:15:42.012-05:00</updated><title type='text'>Running Jasmine BDD Specs in the Terminal</title><content type='html'>I had a pretty bad week with Ruby on Windows 7 64 bit: I tried to set up DBI and ODBC on my work machine but I did not have any luck with that.&lt;br /&gt;&lt;br /&gt;I needed something to feel good about, so I decided to set up &lt;a href="http://pivotal.github.com/jasmine/" target="_blank"&gt;Jasmine BDD&lt;/a&gt; spec execution in the terminal on OS X.&lt;br /&gt;&lt;br /&gt;I downloaded the &lt;a href="http://pivotal.github.com/jasmine/download.html" target="_blank"&gt;standalone zip file&lt;/a&gt; from Jasmine's web site and made sure that the sample specs are executing fine with the SpecRunner.html file in the browser. &lt;br /&gt;&lt;br /&gt;&lt;div class="separator" style="clear: both; text-align: center;"&gt;&lt;a href="http://4.bp.blogspot.com/_8Lbw_GBR3W4/TN9LbewGCCI/AAAAAAAABck/bINt1jIN2YA/s1600/spec_runner.jpg" imageanchor="1" style="margin-left: 1em; margin-right: 1em;"&gt;&lt;img border="0" height="210" src="http://4.bp.blogspot.com/_8Lbw_GBR3W4/TN9LbewGCCI/AAAAAAAABck/bINt1jIN2YA/s400/spec_runner.jpg" width="400" /&gt;&lt;/a&gt;&lt;/div&gt;&lt;br /&gt;I wanted to execute the exact same specs but instead of running it in the browser, I wanted to do it in terminal.&lt;br /&gt;&lt;br /&gt;&lt;a href="http://blog.michaelphines.net/javascript-testing-with-jasmine-presentation" target="_blank"&gt;Michael Hines'&lt;/a&gt; blog post was a pretty good starting point. He used &lt;a href="https://github.com/pivotalexperimental/jazz_money" target="_blank"&gt;JazzMoney&lt;/a&gt;, so I tried it myself.&lt;br /&gt;&lt;br /&gt;I easily found JazzMoney's installation instructions on their github page.&lt;br /&gt;&lt;br /&gt;&lt;div class="separator" style="clear: both; text-align: center;"&gt;&lt;img border="0" height="133" src="http://3.bp.blogspot.com/_8Lbw_GBR3W4/TN8GEScj2zI/AAAAAAAABcg/UId3dZxXxC8/s400/jazzmoney_install.jpg" width="400" /&gt;&lt;/div&gt;&lt;br /&gt;It has prerequisites: I had to install &lt;a href="https://github.com/mynyml/harmony" target="_blank"&gt;harmony&lt;/a&gt; first.&lt;br /&gt;&lt;br /&gt;I picked ruby-1.9.2-head from &lt;a href="http://rvm.beginrescueend.com/" target="_blank"&gt;RVM&lt;/a&gt;, created a new gemset called "jasmine" and I got started.&lt;br /&gt;Harmony has dependencies as well, I had to get stackdeck and johnson before I installed harmony.&lt;br /&gt;&lt;pre class="js" name="code"&gt;$ gem install stackdeck&lt;br /&gt;$ gem install johnson -v "2.0.0.pre3"&lt;br /&gt;&lt;/pre&gt;This is where it turned ugly. Stackdeck got installed fine, but johnson had some issues.&lt;br /&gt;&lt;br /&gt;&lt;div style="color: #cc0000;"&gt;Building native extensions.  This could take a while...&lt;br /&gt;ERROR:  Error installing johnson:&lt;br /&gt;ERROR: Failed to build gem native extension.&lt;/div&gt;&lt;br /&gt;After Googling the error I found out that johnson is not playing nice with Ruby beyond 1.8.7. I went back to RVM and started out by installing a new version of Ruby.&lt;br /&gt;Here is what I did:&lt;br /&gt;&lt;pre class="js" name="code"&gt;$ rvm install 1.8.7-p249&lt;br /&gt;$ rvm 1.8.7-p249&lt;br /&gt;$ rvm gemset create jasmine&lt;br /&gt;$ rvm 1.8.7-p249@jasmine // switched to jasmine gemset&lt;br /&gt;$ gem install stackdeck&lt;br /&gt;$ gem install johnson -v "2.0.0.pre3"&lt;br /&gt;$ gem install harmony // I tested harmony with a quick test in IRB, worked fine&lt;br /&gt;$ gem install jazz_money&lt;br /&gt;&lt;/pre&gt;I did not have any problems with installing the gems under 1.8.7.&lt;br /&gt;&lt;br /&gt;I had to create a Ruby script that sets up the test suite and this is the file I ran in the terminal. My run_specs.rb file was placed into the root folder right next to SpecRunner.html:&lt;br /&gt;&lt;pre class="ruby" name="code"&gt;require 'rubygems'&lt;br /&gt;require 'jazz_money'&lt;br /&gt;&lt;br /&gt;javascript_files = [&lt;br /&gt;  'spec/SpecHelper.js',&lt;br /&gt;  'spec/PlayerSpec.js'&lt;br /&gt;]&lt;br /&gt;&lt;br /&gt;jasmine_spec_files = [&lt;br /&gt;  'src/Player.js',&lt;br /&gt;  'src/Song.js'&lt;br /&gt;]&lt;br /&gt;&lt;br /&gt;JazzMoney::Runner.new(javascript_files, jasmine_spec_files).call&lt;br /&gt;&lt;/pre&gt;I ran the file with the following parameters:&lt;br /&gt;&lt;pre class="js" name="code"&gt;$  ruby run_specs.rb -f n -c&lt;br /&gt;&lt;/pre&gt;Success! This is the output I received in the terminal:&lt;br /&gt;&lt;br /&gt;&lt;div class="separator" style="clear: both; text-align: center;"&gt;&lt;a href="http://3.bp.blogspot.com/_8Lbw_GBR3W4/TN9OHUz9WkI/AAAAAAAABco/jCpPTZ3ns1c/s1600/terminal_jasmine.jpg" imageanchor="1" style="margin-left: 1em; margin-right: 1em;"&gt;&lt;img border="0" height="209" src="http://3.bp.blogspot.com/_8Lbw_GBR3W4/TN9OHUz9WkI/AAAAAAAABco/jCpPTZ3ns1c/s400/terminal_jasmine.jpg" width="400" /&gt;&lt;/a&gt;&lt;/div&gt;&lt;br /&gt;I did one more thing: I created a shell script, this way I did not have to remember all the command line arguments.&lt;br /&gt;I saved this in the specrunner.sh file:&lt;br /&gt;&lt;pre class="js" name="code"&gt;ruby run_specs.rb -f n -c&lt;br /&gt;&lt;/pre&gt;I can now invoke my specs by running "sh specrunner.sh" in the terminal.&lt;div class="blogger-post-footer"&gt;&lt;img width='1' height='1' src='https://blogger.googleusercontent.com/tracker/5573355675806579511-744949885711243489?l=www.adomokos.com' alt='' /&gt;&lt;/div&gt;</content><link rel='replies' type='application/atom+xml' href='http://www.adomokos.com/feeds/744949885711243489/comments/default' title='Post Comments'/><link rel='replies' type='text/html' href='http://www.adomokos.com/2010/11/running-jasmine-bdd-specs-in-terminal.html#comment-form' title='5 Comments'/><link rel='edit' type='application/atom+xml' href='http://www.blogger.com/feeds/5573355675806579511/posts/default/744949885711243489'/><link rel='self' type='application/atom+xml' href='http://www.blogger.com/feeds/5573355675806579511/posts/default/744949885711243489'/><link rel='alternate' type='text/html' href='http://www.adomokos.com/2010/11/running-jasmine-bdd-specs-in-terminal.html' title='Running Jasmine BDD Specs in the Terminal'/><author><name>Attila Domokos</name><uri>http://www.blogger.com/profile/09067995287578229487</uri><email>noreply@blogger.com</email><gd:image rel='http://schemas.google.com/g/2005#thumbnail' width='32' height='32' src='http://4.bp.blogspot.com/-jSMgYgaIqF8/TtJneb7-ZyI/AAAAAAAAB6k/U9Hd923CNvM/s220/adomokos_bw.png'/></author><media:thumbnail xmlns:media='http://search.yahoo.com/mrss/' url='http://4.bp.blogspot.com/_8Lbw_GBR3W4/TN9LbewGCCI/AAAAAAAABck/bINt1jIN2YA/s72-c/spec_runner.jpg' height='72' width='72'/><thr:total>5</thr:total></entry><entry><id>tag:blogger.com,1999:blog-5573355675806579511.post-6179414068856631137</id><published>2010-11-09T09:10:00.000-05:00</published><updated>2010-11-09T09:10:24.158-05:00</updated><title type='text'>JavaScript Closures for DRY-ing Up The Logic</title><content type='html'>Our #Hackibou today was a real blast. We focused on JavaScript development using the great &lt;a href="http://pivotal.github.com/jasmine/" target="_blank"&gt;Jasmine BDD&lt;/a&gt; framework. Since most of us were a little rusty on JS, we decided to start with something very simple: a Calculator. Our task was to develop a calculator that accepts an array of integer numbers in its add(), subtract(), multiply() and divide() functions.&lt;br /&gt;&lt;br /&gt;We started out with a couple of very simple specs:&lt;br /&gt;&lt;pre class="js" name="code"&gt;describe("Calculator", function() {&lt;br /&gt;  var calculator;&lt;br /&gt;  beforeEach(function() {&lt;br /&gt;   calculator = new Calculator();&lt;br /&gt;  });&lt;br /&gt;&lt;br /&gt;  describe("Arithmetic operations on an array input", function() {&lt;br /&gt;   it("creates a new Calculator object", function() {&lt;br /&gt;    expect(calculator).not.toBeNull();&lt;br /&gt;   });&lt;br /&gt;&lt;br /&gt;   it("adds two numbers together", function() {&lt;br /&gt;    expect(calculator.add([1,0])).toEqual(1);&lt;br /&gt;   });&lt;br /&gt;&lt;br /&gt;   it("adds three numbers together", function() {&lt;br /&gt;    expect(calculator.add([1,2,3])).toEqual(6);&lt;br /&gt;   });&lt;br /&gt;&lt;br /&gt;   it("multiplies two numbers", function(){&lt;br /&gt;    expect(calculator.multiply([1,2])).toEqual(2);&lt;br /&gt;   });&lt;br /&gt;  });&lt;br /&gt;});&lt;br /&gt;&lt;/pre&gt;And our - not too elegant - solution was this:&lt;br /&gt;&lt;pre class="js" name="code"&gt;function Calculator() {&lt;br /&gt;  this.add = function(input) {&lt;br /&gt;    var result = 0;&lt;br /&gt;    for(i = 0; i&amp;lt;input.length; ++i) {&lt;br /&gt;      result += input[i];&lt;br /&gt;    }&lt;br /&gt;    return result;&lt;br /&gt;  }&lt;br /&gt;&lt;br /&gt;  this.multiply = function(input) {&lt;br /&gt;    var result = 1;&lt;br /&gt;    for(i=0; i&amp;lt;input.length; ++i) {&lt;br /&gt;      result *= input[i];&lt;br /&gt;    }&lt;br /&gt;    return result;&lt;br /&gt;  }&lt;br /&gt;}&lt;br /&gt;&lt;/pre&gt;Look at the code above. 90% of the code is duplicated there. One of us suggested assigning the first element of the array to the result right on the declaration. With this change the only difference between the two functions is the operation. One uses addition and the other multiplication. I played with JS closures a little bit before, so I proposed this:&lt;br /&gt;&lt;pre class="js" name="code"&gt;function Calculator() {&lt;br /&gt; var operator = function(result, input) { return result + input; };&lt;br /&gt; this.add = function(input){&lt;br /&gt;  return operation(input, operator);&lt;br /&gt; };&lt;br /&gt;&lt;br /&gt; this.multiply = function(input){&lt;br /&gt;  return operation(input, function(result, input){return result*input;});&lt;br /&gt; }&lt;br /&gt;&lt;br /&gt; function operation(input, operator) {&lt;br /&gt;  var result = input[0];&lt;br /&gt;  for(i = 1; i &lt; input.length; i++){&lt;br /&gt;   result = operator(result, input[i]);&lt;br /&gt;  }&lt;br /&gt;  return result;&lt;br /&gt; }&lt;br /&gt;}&lt;br /&gt;&lt;/pre&gt;Check out the operation() function. It uses two parameters, the first one is the array of integers and the other is a function object that holds the calculation logic. It's invoked on line 14. The variable result is both passed in as the first input and is assigned as the result of the function call.One of us suggested using the &lt;a href="http://www.w3schools.com/jsref/jsref_shift.asp" target="_blank"&gt;shift() function&lt;/a&gt; on the input array, this way we did not have to start our for loop with the second element of the array.Our operation() function now looked like this:&lt;pre class="js" name="code"&gt;function operation(input, operator) {&lt;br /&gt; var result = input.shift();&lt;br /&gt; for(i = 0; i &lt; input.length; i++){&lt;br /&gt;  result = operator(result, input[i]);&lt;br /&gt; }&lt;br /&gt; return result;&lt;br /&gt;}&lt;br /&gt;&lt;/pre&gt;Adding subtraction and division was very simple:&lt;pre class="js" name="code"&gt;this.subtract = function(input){&lt;br /&gt;  return operation(input, function(result, input){return result-input;});&lt;br /&gt; }&lt;br /&gt;&lt;br /&gt; this.divide = function(input){&lt;br /&gt;  return operation(input, function(result, input){return result/input;});&lt;br /&gt; }&lt;br /&gt;&lt;/pre&gt;Please note that there is no &lt;a href="http://www.antiifcampaign.com/" target="_blank"&gt;if statement&lt;/a&gt; in the Calculator object.&lt;br/&gt;&lt;br/&gt;Our final solution can be found in &lt;a href="https://gist.github.com/665954" target="_blank"&gt;this gist&lt;/a&gt;.&lt;div class="blogger-post-footer"&gt;&lt;img width='1' height='1' src='https://blogger.googleusercontent.com/tracker/5573355675806579511-6179414068856631137?l=www.adomokos.com' alt='' /&gt;&lt;/div&gt;</content><link rel='replies' type='application/atom+xml' href='http://www.adomokos.com/feeds/6179414068856631137/comments/default' title='Post Comments'/><link rel='replies' type='text/html' href='http://www.adomokos.com/2010/11/javascript-closures-for-dry-ing-up.html#comment-form' title='0 Comments'/><link rel='edit' type='application/atom+xml' href='http://www.blogger.com/feeds/5573355675806579511/posts/default/6179414068856631137'/><link rel='self' type='application/atom+xml' href='http://www.blogger.com/feeds/5573355675806579511/posts/default/6179414068856631137'/><link rel='alternate' type='text/html' href='http://www.adomokos.com/2010/11/javascript-closures-for-dry-ing-up.html' title='JavaScript Closures for DRY-ing Up The Logic'/><author><name>Attila Domokos</name><uri>http://www.blogger.com/profile/09067995287578229487</uri><email>noreply@blogger.com</email><gd:image rel='http://schemas.google.com/g/2005#thumbnail' width='32' height='32' src='http://4.bp.blogspot.com/-jSMgYgaIqF8/TtJneb7-ZyI/AAAAAAAAB6k/U9Hd923CNvM/s220/adomokos_bw.png'/></author><thr:total>0</thr:total></entry><entry><id>tag:blogger.com,1999:blog-5573355675806579511.post-4016391656836849259</id><published>2010-09-12T22:33:00.002-04:00</published><updated>2010-09-13T13:44:37.270-04:00</updated><title type='text'>Making Your Tests More Readable with MSpec</title><content type='html'>After playing with &lt;a href="http://github.com/machine/machine.specifications" target="_blank"&gt;MSpec&lt;/a&gt; on somebody else' machine a couple of weeks ago I decided to give it a try today and tweak it a bit. I worked on the &lt;a href="http://codingkata.org/katas/unit/movie-tickets" target="_blank"&gt;Movie Tickets kata&lt;/a&gt; with someone who was not an experienced TDD/BDD-er and this is how far we went.&lt;br /&gt;&lt;br /&gt;Getting MSpec is pretty easy. Pull the source code from the github repo and build it. Once you have everything built you should just reference the dll in your C# project: Machine.Specifications.&lt;br /&gt;I wanted to keep my solution as simple as possible so I kept both the TicketCalculator and its specs in the same .cs file.&lt;br /&gt;&lt;br /&gt;&lt;div style="text-align: center;"&gt;&lt;img src="http://lh6.ggpht.com/_8Lbw_GBR3W4/TIvaQhpM9vI/AAAAAAAABSY/oMhsi5juES8/TryMSpecSolution.png" /&gt;&lt;br /&gt;&lt;/div&gt;&lt;br /&gt;The kata specifies the method names that you start with so I went ahead and created the class:&lt;br /&gt;&lt;pre class="csharp" name="code"&gt;public class TicketCalculator {&lt;br /&gt;&amp;nbsp;&amp;nbsp;public void StartPurchase(int runtime, DayOfWeek day, bool isParquet, bool is3D) {}&lt;br /&gt;&amp;nbsp;&amp;nbsp;public void AddTicket(int age, bool isStudent) {}&lt;br /&gt;&amp;nbsp;&amp;nbsp;public decimal FinishPurchase() {return 0m;}&lt;br /&gt;}&lt;br /&gt;&lt;/pre&gt;I put my first spec right below the TicketCalculator class:&lt;br /&gt;&lt;pre class="csharp" name="code"&gt;[Subject("Basic admission rates")]&lt;br /&gt;public class Purchasing_general_ticket_as_an_adult_non_student{&lt;br /&gt;&amp;nbsp;&amp;nbsp;private static TicketCalculator _calculator;&lt;br /&gt;&lt;br /&gt;&amp;nbsp;&amp;nbsp;Establish setup_expectation = () =&gt; {&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;_calculator = new TicketCalculator();&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;_calculator.StartPurchase(115, DayOfWeek.Monday, true, false);};&lt;br /&gt;&lt;br /&gt;&amp;nbsp;&amp;nbsp;Because trigger = () =&gt;&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;_calculator.AddTicket(33, false);&lt;br /&gt;&lt;br /&gt;&amp;nbsp;&amp;nbsp;It verify = () =&gt;&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;_calculator.FinishPurchase().ShouldEqual(11m);&lt;br /&gt;}&lt;br /&gt;&lt;/pre&gt;It compiled fine, however, when I executed the spec this is the error I got:&lt;br /&gt;&lt;br /&gt;&lt;div style="color: #CC0000"&gt;&lt;trymspec&gt; (1 test), 1 test failed&lt;br /&gt;TryMSpec (1 test), 1 test failed&lt;br /&gt;Basic admission rates, Purchasing general ticket as an adult non student (1 test), 1 test failed&lt;br /&gt;verify, Failed: Machine.Specifications.SpecificationException: Should equal [11] but is [0]&lt;br /&gt;&lt;/div&gt;&lt;br /&gt;Since refactoring in the "red" is forbidden, I did the simplest thing that would make my test to pass: I returned 11 from the "FinishPurchase()" method.&lt;br /&gt;&lt;br /&gt;I am looking at both the MSpec code and the output and they are ugly: it's really hard to read. To me a test is readable when I can show it to someone and can pretty much tell what's going on in there.&lt;br /&gt;&lt;br /&gt;My spec passed so I started cleaning up my code. The first thing I did was introducing new aliases for the delegate names. Establish, Because and It felt awkward. I always think about Given-When-Then state transitions in my tests and this change just felt more natural to me.&lt;br /&gt;&lt;pre class="csharp" name="code"&gt;using Given = Machine.Specifications.Establish;&lt;br /&gt;using When = Machine.Specifications.Because;&lt;br /&gt;using Then = Machine.Specifications.It;&lt;br /&gt;&lt;br /&gt;[Subject("Basic admission rates")]&lt;br /&gt;&amp;nbsp;&amp;nbsp;public class Purchasing_general_ticket_as_an_adult_non_student{&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;private static TicketCalculator _calculator;&lt;br /&gt;&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;Given setup_expectation = () =&gt;{&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;_calculator = new TicketCalculator();&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;_calculator.StartPurchase(115, DayOfWeek.Monday, true, false);};&lt;br /&gt;&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;When trigger = () =&gt;&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;_calculator.AddTicket(33, false);&lt;br /&gt;&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;Then verify = () =&gt;&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;_calculator.FinishPurchase().ShouldEqual(11m);&lt;br /&gt;  }&lt;br /&gt;&lt;/pre&gt;This was a good start, but the code is far from readable. I tweaked the delegate names a little bit and I ended up with this:&lt;br /&gt;&lt;pre class="csharp" name="code"&gt;[Subject("Basic admission rates")]&lt;br /&gt;&amp;nbsp;&amp;nbsp;public class Purchasing_general_ticket_as_an_adult_non_student{&lt;br /&gt;&amp;nbsp;&amp;nbsp&amp;nbsp;&amp;nbsp;private static TicketCalculator _calculator;&lt;br /&gt;&lt;br /&gt;&amp;nbsp;&amp;nbsp&amp;nbsp;&amp;nbsp;Given i_buy_a_standard_movie_ticket = () =&gt;{&lt;br /&gt;&amp;nbsp;&amp;nbsp&amp;nbsp;&amp;nbsp&amp;nbsp;&amp;nbsp;_calculator = new TicketCalculator();&lt;br /&gt;&amp;nbsp;&amp;nbsp&amp;nbsp;&amp;nbsp&amp;nbsp;&amp;nbsp;_calculator.StartPurchase(115, DayOfWeek.Monday, true, false);};&lt;br /&gt;&lt;br /&gt;&amp;nbsp;&amp;nbsp&amp;nbsp;&amp;nbsp;When i_purchase_it_for_an_adult_non_student = () =&gt;&lt;br /&gt;&amp;nbsp;&amp;nbsp&amp;nbsp;&amp;nbsp&amp;nbsp;&amp;nbsp;_calculator.AddTicket(33, false);&lt;br /&gt;&lt;br /&gt;&amp;nbsp;&amp;nbsp&amp;nbsp;&amp;nbsp;Then i_pay_11_bucks = () =&gt;&lt;br /&gt;&amp;nbsp;&amp;nbsp&amp;nbsp;&amp;nbsp&amp;nbsp;&amp;nbsp;_calculator.FinishPurchase().ShouldEqual(11m);&lt;br /&gt;&amp;nbsp;&amp;nbsp;}&lt;br /&gt;&lt;/pre&gt;Try to read this code! There is some noise around it, but you should be able to read it out:&lt;br /&gt;&lt;br /&gt;&amp;nbsp;&amp;nbsp;&lt;span style="font-weight: bold;"&gt;Given&lt;/span&gt; - I buy a standard movie ticket&lt;br /&gt;&amp;nbsp;&amp;nbsp;&lt;span style="font-weight: bold;"&gt;When&lt;/span&gt; - I purchase it for an adult non-student&lt;br /&gt;&amp;nbsp;&amp;nbsp;&lt;span style="font-weight: bold;"&gt;Then&lt;/span&gt; - I pay $11&lt;br /&gt;&lt;br /&gt;I don't have much space on this page, but if you indent the lambda a little more the Given - When - Then words will become more apparent.&lt;br /&gt;&lt;br /&gt;Let's look at the second scenario: a standard movie ticket for a student is $8.&lt;br /&gt;Here is my spec for that:&lt;br /&gt;&lt;pre class="csharp" name="code"&gt;[Subject("Basic admission rates")]&lt;br /&gt;public class Purchasing_general_ticket_as_an_adult_student{&lt;br /&gt;&amp;nbsp;&amp;nbsp;private static TicketCalculator _calculator;&lt;br /&gt;&lt;br /&gt;&amp;nbsp;&amp;nbsp;Given i_buy_a_standard_movie_ticket = () =&gt; {&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;_calculator = new TicketCalculator();&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;_calculator.StartPurchase(115, DayOfWeek.Monday, true, false);};&lt;br /&gt;&lt;br /&gt;&amp;nbsp;&amp;nbsp;When i_purchase_it_for_an_adult_student = () =&gt;&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;_calculator.AddTicket(33, true);&lt;br /&gt;&lt;br /&gt;&amp;nbsp;&amp;nbsp;Then i_pay_8_bucks = () =&gt;&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;_calculator.FinishPurchase().ShouldEqual(8m);&lt;br /&gt;}&lt;br /&gt;&lt;/pre&gt;When I executed the spec with MSpec I received the following error:&lt;br /&gt;&lt;br /&gt;&lt;div style="color: #CC0000"&gt;&lt;trymspec&gt; (2 tests), 1 test failed&lt;br /&gt;TryMSpec (2 tests), 1 test failed&lt;br /&gt;Basic admission rates, Purchasing general ticket as an adult non student (1 test), Success&lt;br /&gt;i pay 11 bucks, Success&lt;br /&gt;Basic admission rates, Purchasing general ticket as an adult student (1 test), 1 test failed&lt;br /&gt;i pay 8 bucks, Failed: Machine.Specifications.SpecificationException: Should equal [8] but is [11]&lt;br /&gt;&lt;/div&gt;&lt;br /&gt;Again, I did the simplest thing that could possibly work:&lt;br /&gt;&lt;pre class="csharp" name="code"&gt;public class TicketCalculator{&lt;br /&gt;&amp;nbsp;&amp;nbsp;private decimal _ticket_price = 11m;&lt;br /&gt;&amp;nbsp;&amp;nbsp;public void StartPurchase(int runtime, DayOfWeek day, bool isParquet, bool is3D) {}&lt;br /&gt;&lt;br /&gt;&amp;nbsp;&amp;nbsp;public void AddTicket(int age, bool isStudent){&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;if (isStudent)&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;_ticket_price = 8m;&lt;br /&gt;&amp;nbsp;&amp;nbsp;}&lt;br /&gt;&lt;br /&gt;&amp;nbsp;&amp;nbsp;public decimal FinishPurchase(){ return _ticket_price; }&lt;br /&gt;}&lt;br /&gt;&lt;/pre&gt;Everything passed:&lt;br /&gt;&lt;br /&gt;&lt;div style="color: #336600"&gt;&lt;trymspec&gt; (2 tests), Success&lt;br /&gt;TryMSpec (2 tests), Success&lt;br /&gt;Basic admission rates, Purchasing general ticket as an adult non student (1 test), Success&lt;br /&gt;i pay 11 bucks, Success&lt;br /&gt;Basic admission rates, Purchasing general ticket as an adult student (1 test), Success&lt;br /&gt;i pay 8 bucks, Success&lt;br /&gt;&lt;/div&gt;&lt;br /&gt;Notice we have duplication in our specs: the "Given" delegate is duplicated the exact same way in both of them. Time to &lt;a href="http://en.wikipedia.org/wiki/Don't_repeat_yourself" target="_blank"&gt;DRY&lt;/a&gt; up the code a little bit. I created a base class called SpecBase and moved the field "_calculator" and the "Given" delegate into it.&lt;br /&gt;&lt;pre class="csharp" name="code"&gt;public class SpecBase{&lt;br /&gt;&amp;nbsp;&amp;nbsp;protected static TicketCalculator _calculator;&lt;br /&gt;&lt;br /&gt;&amp;nbsp;&amp;nbsp;Given i_buy_a_standard_movie_ticket = () =&gt; {&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;_calculator = new TicketCalculator();&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;_calculator.StartPurchase(115, DayOfWeek.Monday, true, false);&lt;br /&gt;&amp;nbsp;&amp;nbsp;};&lt;br /&gt;}&lt;br /&gt;&lt;/pre&gt;Both of the specs are now inheriting from this base class. The second one looks like this:&lt;br /&gt;&lt;pre class="csharp" name="code"&gt;[Subject("Basic admission rates")]&lt;br /&gt;public class Purchasing_general_ticket_as_an_adult_student : SpecBase{&lt;br /&gt;&amp;nbsp;&amp;nbsp;When i_purchase_it_for_an_adult_student = () =&gt;&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;_calculator.AddTicket(33, true);&lt;br /&gt;&lt;br /&gt;&amp;nbsp;&amp;nbsp;Then i_pay_8_bucks = () =&gt;&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;_calculator.FinishPurchase().ShouldEqual(8m);&lt;br /&gt;}&lt;br /&gt;&lt;/pre&gt;This way the spec is short and there is no duplication, however, I don't see the "Given" part in it. I took care of it by renaming the "SpecBase" class to "Given_i_buy_a_standard_ticket" and with the right indentation I should have a readable spec that tells a story.&lt;br /&gt;&lt;pre class="csharp" name="code"&gt;[Subject("Basic admission rates")]&lt;br /&gt;public class Purchasing_general_ticket_as_an_adult_student : &lt;br /&gt;&amp;nbsp;&amp;nbsp;Given_i_buy_a_standard_ticket{&lt;br /&gt;&amp;nbsp;&amp;nbsp;When i_purchase_it_for_an_adult_student = () =&gt; _calculator.AddTicket(33, true);&lt;br /&gt;&amp;nbsp;&amp;nbsp;Then i_pay_8_bucks = () =&gt; _calculator.FinishPurchase().ShouldEqual(8m);&lt;br /&gt;}&lt;br /&gt;&lt;/pre&gt;You can find the final C# file in this gist: &lt;a href="http://gist.github.com/576433"&gt;http://gist.github.com/576433&lt;/a&gt;.&lt;br /&gt;&lt;br /&gt;I'd like to list a couple of things that you should see - and maybe follow - based on this example:&lt;br /&gt;&lt;ul&gt;&lt;li&gt;look at the length of the specs, none of them are more than 3 lines of code,&lt;/li&gt;&lt;li&gt;there is no code duplication,&lt;/li&gt;&lt;li&gt;there is only one assertion in each one of them,&lt;/li&gt;&lt;li&gt;the spec tells you a story.&lt;/li&gt;&lt;/ul&gt;&lt;div class="blogger-post-footer"&gt;&lt;img width='1' height='1' src='https://blogger.googleusercontent.com/tracker/5573355675806579511-4016391656836849259?l=www.adomokos.com' alt='' /&gt;&lt;/div&gt;</content><link rel='replies' type='application/atom+xml' href='http://www.adomokos.com/feeds/4016391656836849259/comments/default' title='Post Comments'/><link rel='replies' type='text/html' href='http://www.adomokos.com/2010/09/making-your-tests-more-readable-with.html#comment-form' title='0 Comments'/><link rel='edit' type='application/atom+xml' href='http://www.blogger.com/feeds/5573355675806579511/posts/default/4016391656836849259'/><link rel='self' type='application/atom+xml' href='http://www.blogger.com/feeds/5573355675806579511/posts/default/4016391656836849259'/><link rel='alternate' type='text/html' href='http://www.adomokos.com/2010/09/making-your-tests-more-readable-with.html' title='Making Your Tests More Readable with MSpec'/><author><name>Attila Domokos</name><uri>http://www.blogger.com/profile/09067995287578229487</uri><email>noreply@blogger.com</email><gd:image rel='http://schemas.google.com/g/2005#thumbnail' width='32' height='32' src='http://4.bp.blogspot.com/-jSMgYgaIqF8/TtJneb7-ZyI/AAAAAAAAB6k/U9Hd923CNvM/s220/adomokos_bw.png'/></author><media:thumbnail xmlns:media='http://search.yahoo.com/mrss/' url='http://lh6.ggpht.com/_8Lbw_GBR3W4/TIvaQhpM9vI/AAAAAAAABSY/oMhsi5juES8/s72-c/TryMSpecSolution.png' height='72' width='72'/><thr:total>0</thr:total></entry><entry><id>tag:blogger.com,1999:blog-5573355675806579511.post-4375371957378755568</id><published>2010-08-17T11:22:00.024-04:00</published><updated>2010-10-19T21:19:55.479-04:00</updated><title type='text'>Where Should I Host My Family Videos?</title><content type='html'>Wow, it's been a while since I last posted anything on my blog! A lot has happened: I spent an entire month with my family in &lt;a href="http://en.wikipedia.org/wiki/Hungary" target="_blank"&gt;Hungary&lt;/a&gt;. I even took a short trip to &lt;a href="http://en.wikipedia.org/wiki/Austria" target="_blank"&gt;Austria&lt;/a&gt; as well. I enjoyed many &lt;a href="http://en.wikipedia.org/wiki/Black_forest_cake" target="_blank"&gt;Black Forest cakes&lt;/a&gt; with great &lt;a href="http://en.wikipedia.org/wiki/Cappucino" target="_blank"&gt;cappuccinos&lt;/a&gt;. &lt;br /&gt;Returning to the US has its ups and downs: on one hand we are back in our comfortable home, on the other we miss all our relatives living on the Old Continent.&lt;br /&gt;&lt;br /&gt;We've been posting short - 30 seconds long - videos on our family web site that I host on my home server. We have a LAMP setup with &lt;a href="http://gallery.menalto.com/" target="_blank"&gt;Gallery2&lt;/a&gt; on it. It has served us well, however, our cable provider noticed that there is significant data download through our 8080 port. I received their first warning letter right after we got back. It was pretty obvious that I had to look for some kind of a cloud hosting solution.&lt;br /&gt;&lt;br /&gt;Some people recommended using &lt;a href="http://aws.amazon.com/s3/" target="_blank"&gt;Amazon S3&lt;/a&gt; but I wanted to stick with Google. Posting photos to a Picasa web album is very simple but it has one shortcoming: you can't upload a video in flv format. You can upload them to YouTube and then somehow link that into the album but I just was not ready to go with that hybrid solution.&lt;br /&gt;&lt;br /&gt;We chose Blogger.com to host our photos and videos. We can easily upload photos there plus adding a little more text is always welcomed by our family. But what about the videos? Well, Google Sites is there for the rescue.&lt;br /&gt;&lt;br /&gt;I use the unbelievably awesome &lt;a href="http://www.ffmpeg.org/" target="_blank"&gt;ffmpeg&lt;/a&gt; to convert my ~60 MB 30 seconds clip into a 1 MB flash movie.&lt;br /&gt;&lt;br /&gt;For you, dear reader, here is the script I use:&lt;br /&gt;&lt;pre class="ruby" name="code"&gt;ffmpeg -i [your_avi_movie.AVI] -ar 11025 -ab 32 -f flv -s 320x240 [your_output_movie.flv]&lt;/pre&gt;&lt;br /&gt;This might seem cryptic when you look at it first, but google ffmpeg and you should find all the info you need. For example the "-f" switch is the format, "-s" is the size.&lt;br /&gt;&lt;br /&gt;Once the movie is converted, I just upload it to my Google Site. It was not straightforward where and how to upload it, so here are the steps I took:&lt;br /&gt;&lt;br /&gt;1) Go to your Google site (you should have one with a Google account)&lt;br /&gt;2) Go to More Actions =&amp;gt; Manage Site&lt;br /&gt;3) Under "Site content" select "Attachments"&lt;br /&gt;4) You should see the "Upload" button to upload your movies&lt;br /&gt;&lt;br /&gt;Having the movie there is all fine, but I needed a Flash Player to properly play them in the web page. After a quick search I settled with &lt;a href="http://www.osflv.com/" target="_blank"&gt;OS Flv&lt;/a&gt;. I uploaded the player swf file to my site and I had everything ready to post my videos.&lt;br /&gt;&lt;br /&gt;Here is the short HTML code I used to link in the flv movies through the &lt;a href="http://www.osflv.com/" target="_blank"&gt;OS Flv&lt;/a&gt; player into our blog page.&lt;br /&gt;&lt;pre class="html" name="code"&gt;&amp;lt;object height="240" id="flvPlayer" width="320"&amp;gt;&lt;br /&gt;  &amp;lt;param name="movie" value="[link_to_os_flv_player.swf]"&amp;gt;&lt;br /&gt;  &amp;lt;param name="FlashVars" value="&amp;amp;movie=[link_to_your_movie_file.flv]"&amp;gt;&lt;br /&gt;  &amp;lt;embed src="[link_to_os_flv_player.swf]" flashvars="&amp;amp;movie=[link_to_your_movie_file.flv]" width="320" height="240" type="application/x-shockwave-flash"&amp;gt;&amp;lt;/embed&amp;gt;&lt;br /&gt;&amp;lt;/object&amp;gt;&lt;br /&gt;&lt;/pre&gt;&lt;br /&gt;And here is a quick movie clip I made in July, 76 MB compressed to around 1 MB. Enjoy!&lt;br /&gt;&lt;br /&gt;&lt;div style="text-align: center;"&gt;&lt;object height="240" id="flvPlayer" width="320"&gt; &lt;param name="movie" value="http://iridge.com/photos/videos/OSplayer.swf"&gt;&lt;param name="FlashVars" value="&amp;movie=http://iridge.com/photos/videos/MVI_6307_Geese.flv"&gt;&lt;embed src="http://iridge.com/photos/videos/OSplayer.swf" flashvars="&amp;movie=http://iridge.com/photos/videos/MVI_6307_Geese.flv" width="320" height="240" type="application/x-shockwave-flash"&gt;&lt;/embed&gt; &lt;/object&gt;&lt;/div&gt;&lt;div class="blogger-post-footer"&gt;&lt;img width='1' height='1' src='https://blogger.googleusercontent.com/tracker/5573355675806579511-4375371957378755568?l=www.adomokos.com' alt='' /&gt;&lt;/div&gt;</content><link rel='replies' type='application/atom+xml' href='http://www.adomokos.com/feeds/4375371957378755568/comments/default' title='Post Comments'/><link rel='replies' type='text/html' href='http://www.adomokos.com/2010/08/where-should-i-host-my-family-videos.html#comment-form' title='0 Comments'/><link rel='edit' type='application/atom+xml' href='http://www.blogger.com/feeds/5573355675806579511/posts/default/4375371957378755568'/><link rel='self' type='application/atom+xml' href='http://www.blogger.com/feeds/5573355675806579511/posts/default/4375371957378755568'/><link rel='alternate' type='text/html' href='http://www.adomokos.com/2010/08/where-should-i-host-my-family-videos.html' title='Where Should I Host My Family Videos?'/><author><name>Attila Domokos</name><uri>http://www.blogger.com/profile/09067995287578229487</uri><email>noreply@blogger.com</email><gd:image rel='http://schemas.google.com/g/2005#thumbnail' width='32' height='32' src='http://4.bp.blogspot.com/-jSMgYgaIqF8/TtJneb7-ZyI/AAAAAAAAB6k/U9Hd923CNvM/s220/adomokos_bw.png'/></author><thr:total>0</thr:total></entry><entry><id>tag:blogger.com,1999:blog-5573355675806579511.post-5646918231839857445</id><published>2010-05-23T17:06:00.012-04:00</published><updated>2010-05-23T21:28:57.809-04:00</updated><title type='text'>Dynamic Properties In Ruby</title><content type='html'>I've been working on this Rails 3 app where users can define what fields they want to have for their track records. The problem I was facing: how can I dynamically add properties to an object?&lt;br /&gt;&lt;br /&gt;I have this class:&lt;br /&gt;&lt;pre class="ruby" name="code"&gt;class Track&lt;br /&gt;&amp;nbsp;&amp;nbsp;attr_accessor :value&lt;br /&gt;&lt;br /&gt;&amp;nbsp;&amp;nbsp;def initialize(value)&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;@value = value&lt;br /&gt;&amp;nbsp;&amp;nbsp;end&lt;br /&gt;end&lt;br /&gt;&lt;/pre&gt;The value field is populated by the application, it'll always be valid JSON data.&lt;br /&gt;&lt;br /&gt;After the track object is initialized, I'd like to be able to call "distance" and "running" properties on my track object. The following RSpec code describes what I need:&lt;br /&gt;&lt;pre class="ruby" name="code"&gt;describe Track do&lt;br /&gt;&amp;nbsp;&amp;nbsp;it "should add distance and running as read-only properties" do&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;track = Track.new('{"distance":2,"what":"running in the park"}')    &lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;track.distance.should == 2&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;track.what.should == 'running in the park'&lt;br /&gt;&amp;nbsp;&amp;nbsp;end&lt;br /&gt;end&lt;br /&gt;&lt;/pre&gt;The question is: how can I do that?&lt;br /&gt;&lt;br /&gt;First I thought about hooking into the method_missing method in the Track object. It worked, but I was unhappy with the solution. It seemed clumsy and it's not going to provide the best performance either. I exactly know what my methods are going to be called since it'll be set from the value field.&lt;br /&gt;&lt;br /&gt;After googling the topic I found the solution: &lt;a href="http://ruby-doc.org/core/classes/Module.html#M001654" target="_blank"&gt;define_method&lt;/a&gt;.&lt;br /&gt;&lt;br /&gt;I had to parse the JSON data which was easy with the &lt;a href="http://flori.github.com/json/" target="_blank"&gt;json gem&lt;/a&gt;. &lt;br /&gt;&lt;pre class="ruby" name="code"&gt;require 'rubygems'&lt;br /&gt;require 'json'&lt;br /&gt;&lt;br /&gt;data = '{"distance":2,"what":"running"}'&lt;br /&gt;parsed_data = JSON.parse(data)&lt;br /&gt;puts parsed_data["distance"] # =&amp;gt; 2&lt;br /&gt;&lt;/pre&gt;Once I knew how I'll parse the JSON string, adding the define_method calls to the initialize method was easy.&lt;br /&gt;You can find the final solution here:&lt;br /&gt;&lt;pre class="ruby" name="code"&gt;require 'rubygems'&lt;br /&gt;require 'json'&lt;br /&gt;require 'spec'&lt;br /&gt;&lt;br /&gt;class Track&lt;br /&gt;&amp;nbsp;&amp;nbsp;attr_accessor :value&lt;br /&gt;&lt;br /&gt;&amp;nbsp;&amp;nbsp;def initialize(value)&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;@value = value&lt;br /&gt;&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;parsed_values = JSON.parse(value)&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;fields = parsed_values.keys.inject([]) do |result, element|&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;result &amp;lt;&amp;lt; element.to_sym&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;end&lt;br /&gt;&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;fields.each do |field|&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;self.class.send(:define_method, field) do&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;parsed_values[field.to_s]    &lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;end&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;end&lt;br /&gt;&amp;nbsp;&amp;nbsp;end&lt;br /&gt;end&lt;br /&gt;&lt;br /&gt;describe Track do&lt;br /&gt;&amp;nbsp;&amp;nbsp;it "should add distance and running as read-only properties" do&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;track = Track.new('{"distance":2,"what":"running in the park"}')&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;track.distance.should == 2&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;track.what.should == 'running in the park'&lt;br /&gt;&amp;nbsp;&amp;nbsp;end&lt;br /&gt;end&lt;br /&gt;&lt;/pre&gt;There are a couple of things worth mentioning here.&lt;br /&gt;See how elegantly the symbol array is populated from the hash keys on line 12 with the array's "inject" method.&lt;br /&gt;All the magic with dynamic methods happens on line 15. Please note how define_method message is sent to the object's class reference. All it does is returns the value from the parsed JSON data.&lt;br /&gt;&lt;br /&gt;I found this &lt;a href="http://www.vitarara.org/cms/ruby_metaprogamming_declaratively_adding_methods_to_a_class" target="_blank"&gt;article&lt;/a&gt; very helpful. I went through the examples and now I know what's going on behind the scene when I use "has_many :addresses" is my Rails models.&lt;div class="blogger-post-footer"&gt;&lt;img width='1' height='1' src='https://blogger.googleusercontent.com/tracker/5573355675806579511-5646918231839857445?l=www.adomokos.com' alt='' /&gt;&lt;/div&gt;</content><link rel='replies' type='application/atom+xml' href='http://www.adomokos.com/feeds/5646918231839857445/comments/default' title='Post Comments'/><link rel='replies' type='text/html' href='http://www.adomokos.com/2010/05/dynamic-properties-in-ruby.html#comment-form' title='0 Comments'/><link rel='edit' type='application/atom+xml' href='http://www.blogger.com/feeds/5573355675806579511/posts/default/5646918231839857445'/><link rel='self' type='application/atom+xml' href='http://www.blogger.com/feeds/5573355675806579511/posts/default/5646918231839857445'/><link rel='alternate' type='text/html' href='http://www.adomokos.com/2010/05/dynamic-properties-in-ruby.html' title='Dynamic Properties In Ruby'/><author><name>Attila Domokos</name><uri>http://www.blogger.com/profile/09067995287578229487</uri><email>noreply@blogger.com</email><gd:image rel='http://schemas.google.com/g/2005#thumbnail' width='32' height='32' src='http://4.bp.blogspot.com/-jSMgYgaIqF8/TtJneb7-ZyI/AAAAAAAAB6k/U9Hd923CNvM/s220/adomokos_bw.png'/></author><thr:total>0</thr:total></entry><entry><id>tag:blogger.com,1999:blog-5573355675806579511.post-2192267357877004817</id><published>2010-04-26T21:40:00.002-04:00</published><updated>2010-04-26T21:44:49.662-04:00</updated><title type='text'>Hackibou</title><content type='html'>It all started a couple of weeks ago when I suggested to &lt;a href="http://twitter.com/pragmaticpat" target="_blank"&gt;@pragmaticpat&lt;/a&gt; that we should get together for a little BDD with RSpec in Ruby. I have always wanted to work on the &lt;a href="http://rubyquiz.com/quiz22.html" target="_blank"&gt;Roman Numerals ruby quiz&lt;/a&gt; that I found in the book "Best Ruby Quiz", I thought this could be a pretty good chance of trying it. Set up the place (Caribou - hence the name Hackibou), date and time and we were ready to do it. A couple of days before the date I tweeted about it. Word got out and two great guys (&lt;a href="http://twitter.com/joelhelbling" target="_blank"&gt;@joelhelbling&lt;/a&gt; and &lt;a href="http://twitter.com/johnwesp" target="_blank"&gt;@johnwesp&lt;/a&gt;) decided to join us.&lt;br /&gt;&lt;br /&gt;We had three hours to work on this quiz. I got a latte, asked a lady to give up the table she used, set up a blank project with &lt;a href="http://twitter.com/pragmaticpat" target="_blank"&gt;@pragmaticpat&lt;/a&gt; and we started coding. We did two 45 minutes sessions in the afternoon. We discussed what we did and switched pairs after the first session. We intentionally kept the code we wrote and tried to build on top of that in the second session. Unfortunately we did not finish the quiz. Joel took it the farthest, he worked on it at the end of the day. You can see his code on &lt;a href="http://is.gd/bGGBA" target="_blank"&gt;github&lt;/a&gt;.&lt;br /&gt;&lt;br /&gt;All in all, we all had a great time.&lt;br /&gt;&lt;br /&gt;&lt;div class="separator" style="clear: both; text-align: center;"&gt;&lt;a href="http://4.bp.blogspot.com/_8Lbw_GBR3W4/S9Y9F5IscwI/AAAAAAAABGY/Oyo6JPdz9Yc/s1600/hackibou_mac.jpg" imageanchor="1" style="margin-left: 1em; margin-right: 1em;"&gt;&lt;img border="0" src="http://4.bp.blogspot.com/_8Lbw_GBR3W4/S9Y9F5IscwI/AAAAAAAABGY/Oyo6JPdz9Yc/s320/hackibou_mac.jpg" /&gt;&lt;/a&gt;&lt;/div&gt;&lt;br /&gt;At the end of our session Joel did a quick demo on &lt;a href="http://visionmedia.github.com/jspec/" target="_blank"&gt;jspec&lt;/a&gt; with JavaScript. It seems really cool, I have to give it try soon.&lt;br /&gt;&lt;br /&gt;I hope Hackibou won't be a one time event. I am planning on organizing one in 2-3 weeks. I hope to see you there!&lt;div class="blogger-post-footer"&gt;&lt;img width='1' height='1' src='https://blogger.googleusercontent.com/tracker/5573355675806579511-2192267357877004817?l=www.adomokos.com' alt='' /&gt;&lt;/div&gt;</content><link rel='replies' type='application/atom+xml' href='http://www.adomokos.com/feeds/2192267357877004817/comments/default' title='Post Comments'/><link rel='replies' type='text/html' href='http://www.adomokos.com/2010/04/hackibou.html#comment-form' title='1 Comments'/><link rel='edit' type='application/atom+xml' href='http://www.blogger.com/feeds/5573355675806579511/posts/default/2192267357877004817'/><link rel='self' type='application/atom+xml' href='http://www.blogger.com/feeds/5573355675806579511/posts/default/2192267357877004817'/><link rel='alternate' type='text/html' href='http://www.adomokos.com/2010/04/hackibou.html' title='Hackibou'/><author><name>Attila Domokos</name><uri>http://www.blogger.com/profile/09067995287578229487</uri><email>noreply@blogger.com</email><gd:image rel='http://schemas.google.com/g/2005#thumbnail' width='32' height='32' src='http://4.bp.blogspot.com/-jSMgYgaIqF8/TtJneb7-ZyI/AAAAAAAAB6k/U9Hd923CNvM/s220/adomokos_bw.png'/></author><media:thumbnail xmlns:media='http://search.yahoo.com/mrss/' url='http://4.bp.blogspot.com/_8Lbw_GBR3W4/S9Y9F5IscwI/AAAAAAAABGY/Oyo6JPdz9Yc/s72-c/hackibou_mac.jpg' height='72' width='72'/><thr:total>1</thr:total></entry><entry><id>tag:blogger.com,1999:blog-5573355675806579511.post-3725370833700876557</id><published>2010-04-12T09:16:00.004-04:00</published><updated>2010-04-12T09:48:30.998-04:00</updated><title type='text'>Put on Your Hat and Dance to Sinatra</title><content type='html'>&lt;span class="Apple-style-span" style="font-family: Verdana; font-size: 13px;"&gt;&lt;/span&gt;&lt;br /&gt;&lt;div style="margin-bottom: 0px; margin-top: 0px;"&gt;It's been almost two weeks since we had our kick-off/planning meeting. We ended up with screen shots - very vague, drawn on a white board - and some stories in &lt;a href="http://www.pivotaltracker.com/" target="_blank"&gt;Pivotal Tracker&lt;/a&gt;. I immediately started to look for css templates. I am not a designer, but I do like simple and clean design. Ended up picking one from&amp;nbsp;&lt;a href="http://www.freecsstemplates.org/" target="_blank"&gt;http://www.freecsstemplates.org/&lt;/a&gt;.&lt;/div&gt;&lt;div style="margin-bottom: 0px; margin-top: 0px;"&gt;&lt;br /&gt;My goal was not to jump into code and create models, database tables and such. I'd like to have a rough HTML prototype that can help us figuring out what we really need. I emphasize HTML, because I want to be able to change it easily. I don't want to have a migration script created when we need to add one more field to a form.&lt;br /&gt;&lt;br /&gt;&lt;/div&gt;&lt;div style="margin-bottom: 0px; margin-top: 0px;"&gt;I started out with an index and the user signup page. When I added the confirmation page I quickly realized that I'll have serious duplication unless I do something about it. My header and footer html content was repeated in three HTML files. I know this is only a prototype helping us discovering how our web app should work. But still. Even my prototype code should be &lt;a href="http://en.wikipedia.org/wiki/Don't_repeat_yourself" target="_blank"&gt;DRY&lt;/a&gt;-ed up. I badly needed a template solution.&lt;/div&gt;&lt;br /&gt;&lt;div style="margin-bottom: 0px; margin-top: 0px;"&gt;I could have created a skeleton Rails app, but that just seemed too heavy for this purpose.&lt;/div&gt;&lt;br /&gt;&lt;div style="margin-bottom: 0px; margin-top: 0px;"&gt;I remember Matt Otto's (&lt;a href="http://twitter.com/matthewotto" target="_blank"&gt;@matthewotto&lt;/a&gt;) tweet from late March:&lt;/div&gt;&lt;br /&gt;&lt;div class="separator" style="clear: both; text-align: center;"&gt;&lt;a href="http://3.bp.blogspot.com/_8Lbw_GBR3W4/S8I7LEMKyII/AAAAAAAABF0/krPILKoiqN0/s1600/sinatra_matthewotto1.png" imageanchor="1" style="margin-left: 1em; margin-right: 1em;"&gt;&lt;img border="0" src="http://3.bp.blogspot.com/_8Lbw_GBR3W4/S8I7LEMKyII/AAAAAAAABF0/krPILKoiqN0/s320/sinatra_matthewotto1.png" /&gt;&lt;/a&gt;&lt;/div&gt;&lt;br /&gt;&lt;div style="margin-bottom: 0px; margin-top: 0px;"&gt;I saw the tweet, I did not really pay attention to it then. But after I created my thrid page and noticed the duplication, I knew I had to find something. I can't recall why I looked at &lt;a href="http://www.sinatrarb.com/" target="_blank"&gt;Sinatra&lt;/a&gt; a couple of days later, but I was blown away. It is exactly what I needed! I put a simple app together with it and I was convinced: I found my template engine for the prototype!&lt;/div&gt;&lt;br /&gt;&lt;div style="margin-bottom: 0px; margin-top: 0px;"&gt;Here is the only Ruby file I had in my sample app:&lt;/div&gt;&lt;br /&gt;&lt;pre class="ruby" name="code"&gt;# myapp.rb&lt;br /&gt;require 'rubygems'&lt;br /&gt;require 'sinatra'&lt;br /&gt;&lt;br /&gt;get '/' do&lt;br /&gt;&amp;nbsp;&amp;nbsp;'Hello, World!'&lt;br /&gt;end&lt;br /&gt;&lt;br /&gt;get '/hello/:name' do&lt;br /&gt;&amp;nbsp;&amp;nbsp;# matches "GET /hello/foo" and "GET /hello/bar"&lt;br /&gt;&amp;nbsp;&amp;nbsp;# params[:name] is 'foo' or 'bar'&lt;br /&gt;&amp;nbsp;&amp;nbsp;"Hello #{params[:name]}!"&lt;br /&gt;end&lt;br /&gt;&lt;br /&gt;get '/test' do&lt;br /&gt;&amp;nbsp;&amp;nbsp;@data = 'Hello from test!'&lt;br /&gt;&amp;nbsp;&amp;nbsp;erb :index&lt;br /&gt;end&lt;br /&gt;&lt;/pre&gt;&lt;br /&gt;The first block is executed when you make a request - assuming you run the app locally - to http://localhost:4567. There is no template rendering, you'll only see the string "Hello, World!" in the browser.&lt;br /&gt;&lt;div style="margin-bottom: 0px; margin-top: 0px;"&gt;Ok, this works, but how could I use this for my templates?&amp;nbsp;&lt;/div&gt;&lt;br /&gt;Digging into the &lt;a href="http://www.sinatrarb.com/intro" target="_blank"&gt;Sinatra documentation&lt;/a&gt; I found the solution. I had to create the following directory structure:&lt;br /&gt;&lt;br /&gt;&lt;div style="margin-bottom: 0px; margin-top: 0px;"&gt;-|&lt;/div&gt;&lt;div style="margin-bottom: 0px; margin-top: 0px;"&gt;-|-views&lt;/div&gt;&lt;div style="margin-bottom: 0px; margin-top: 0px;"&gt;&amp;nbsp;&amp;nbsp; &amp;nbsp;&amp;nbsp;&amp;nbsp; &amp;nbsp;|- index.erb&lt;/div&gt;&lt;div style="margin-bottom: 0px; margin-top: 0px;"&gt;&amp;nbsp;&amp;nbsp; &amp;nbsp;&amp;nbsp;&amp;nbsp; &amp;nbsp;|- layout.erb&lt;/div&gt;&lt;div style="margin-bottom: 0px; margin-top: 0px;"&gt;-|-myapp.rb&lt;/div&gt;&lt;div style="margin-bottom: 0px; margin-top: 0px;"&gt;&lt;br /&gt;layout.erb is my template:&lt;/div&gt;&lt;br /&gt;&lt;div style="margin-bottom: 0px; margin-top: 0px;"&gt;&lt;pre class="xml" name="code"&gt;&amp;lt;html&amp;gt;&lt;br /&gt;&amp;lt;head&amp;gt;&lt;br /&gt;&amp;lt;title&amp;gt;Something Test&amp;lt;/title&amp;gt;&lt;br /&gt;&amp;lt;/head&amp;gt;&lt;br /&gt;&amp;lt;body&amp;gt;&lt;br /&gt;&amp;lt;div id="content"&amp;gt;&lt;br /&gt;&amp;lt;%= yield %&amp;gt;&lt;br /&gt;&amp;lt;/div&amp;gt;&lt;br /&gt;&amp;lt;div id="footer"&amp;gt;&lt;br /&gt;&amp;lt;hr/&amp;gt;&lt;br /&gt;&amp;lt;/div&amp;gt;&lt;br /&gt;&amp;lt;/body&amp;gt;&lt;br /&gt;&amp;lt;/html&amp;gt;&lt;br /&gt;&lt;/pre&gt;&lt;/div&gt;&lt;br /&gt;&lt;div style="margin-bottom: 0px; margin-top: 0px;"&gt;And index.erb provides the page content:&lt;/div&gt;&lt;br /&gt;&lt;pre class="xml" name="code"&gt;&amp;lt;div&amp;gt;&lt;br /&gt;This is the content: &amp;lt;%= @data %&amp;gt;&lt;br /&gt;&amp;lt;/div&amp;gt;&lt;br /&gt;&lt;/pre&gt;&lt;br /&gt;&lt;div style="margin-bottom: 0px; margin-top: 0px;"&gt;Please note that I set the @data variable in the "get '/test'" block.&lt;br /&gt;&lt;br /&gt;And that's it. I tell Sinatra to grab the index.erb file and since I have layout.erb it knows it has to render that as a template. There is a whole lot more to Sinatra than this of course. I found the "&lt;a href="http://sinatra-book.gittr.com/" target="_blank"&gt;Sinatra Book&lt;/a&gt;" really helpful. I'd recommend taking a look at it if you want to give Sinatra a try.&lt;/div&gt;&lt;br /&gt;&lt;div style="margin-bottom: 0px; margin-top: 0px;"&gt;I used HTML in ERB first, but then Matt Otto tweeted that he went with (&lt;a href="http://haml-lang.com/" style="color: #551a8b;" target="_blank"&gt;HAML&lt;/a&gt;) so I started playing with that.&lt;/div&gt;&lt;div style="margin-bottom: 0px; margin-top: 0px;"&gt;HAML looked cryptic, but once you get the hang of it, it's really simple.&lt;br /&gt;&lt;br /&gt;In the end, I started using HAML and SASS for my prototypes, I'll blog about them in my follow-up blog post.&lt;/div&gt;&lt;div class="blogger-post-footer"&gt;&lt;img width='1' height='1' src='https://blogger.googleusercontent.com/tracker/5573355675806579511-3725370833700876557?l=www.adomokos.com' alt='' /&gt;&lt;/div&gt;</content><link rel='replies' type='application/atom+xml' href='http://www.adomokos.com/feeds/3725370833700876557/comments/default' title='Post Comments'/><link rel='replies' type='text/html' href='http://www.adomokos.com/2010/04/put-on-your-hat-and-dance-to-sinatra.html#comment-form' title='2 Comments'/><link rel='edit' type='application/atom+xml' href='http://www.blogger.com/feeds/5573355675806579511/posts/default/3725370833700876557'/><link rel='self' type='application/atom+xml' href='http://www.blogger.com/feeds/5573355675806579511/posts/default/3725370833700876557'/><link rel='alternate' type='text/html' href='http://www.adomokos.com/2010/04/put-on-your-hat-and-dance-to-sinatra.html' title='Put on Your Hat and Dance to Sinatra'/><author><name>Attila Domokos</name><uri>http://www.blogger.com/profile/09067995287578229487</uri><email>noreply@blogger.com</email><gd:image rel='http://schemas.google.com/g/2005#thumbnail' width='32' height='32' src='http://4.bp.blogspot.com/-jSMgYgaIqF8/TtJneb7-ZyI/AAAAAAAAB6k/U9Hd923CNvM/s220/adomokos_bw.png'/></author><media:thumbnail xmlns:media='http://search.yahoo.com/mrss/' url='http://3.bp.blogspot.com/_8Lbw_GBR3W4/S8I7LEMKyII/AAAAAAAABF0/krPILKoiqN0/s72-c/sinatra_matthewotto1.png' height='72' width='72'/><thr:total>2</thr:total></entry><entry><id>tag:blogger.com,1999:blog-5573355675806579511.post-1386062251864159773</id><published>2010-03-30T09:01:00.000-04:00</published><updated>2010-03-30T09:01:52.241-04:00</updated><title type='text'>Planning Meeting</title><content type='html'>We started to work on our new application over the weekend. No specifics at this time, let's just call it "Project X". It'll be a web app, most likely developed in Rails using mySQL back-end. But it doesn't really matter just yet.&lt;br /&gt;&lt;br /&gt;We got together Saturday around 10 AM. After establishing the agenda we started to work on the details.&lt;br /&gt;Here is what our agenda was:&lt;br /&gt;&lt;br /&gt;1)      Check competitors&lt;br /&gt;2) Look at HTML Templates&lt;br /&gt;3)      Define system user types&lt;br /&gt;4)      Work out the workflows for our different user types&lt;br /&gt;5)      Create wire frames&lt;br /&gt;6)      Create mock-ups&lt;br /&gt;7)      Enter story cards in Pivotal tracker&lt;br /&gt;&lt;div class="separator" style="clear: both; text-align: center;"&gt;&lt;/div&gt;&lt;div style="text-align: auto;"&gt;&lt;br /&gt;&lt;/div&gt;&lt;a href="http://2.bp.blogspot.com/_8Lbw_GBR3W4/S7FX7rHMhiI/AAAAAAAABEs/Kdtf8cKJoi0/s1600/agenda.jpg" imageanchor="1" style="margin-left: 1em; margin-right: 1em;"&gt;&lt;img border="0" src="http://2.bp.blogspot.com/_8Lbw_GBR3W4/S7FX7rHMhiI/AAAAAAAABEs/Kdtf8cKJoi0/s320/agenda.jpg" /&gt;&lt;/a&gt;&lt;br /&gt;&lt;br /&gt;The real work started with item 3 on our list. It is like the 10,000 feet view of the app. No specifics, discussing what happens when. Our technique was similar to a UML activity diagram, focusing on what triggers the application to move from one state to the other.&lt;br /&gt;&lt;br /&gt;Once we had a general idea of the different workflows that different user types had, we started to "zoom into" the areas by creating wire frames. We wanted to focus on what pages we needed and how you could get from one page to the other.&lt;br /&gt;&lt;br /&gt;We estimated around 12 – 15 pages we need to get the application off the ground. Our mock up task focused on what elements or fields we'd have on those pages. What describes a user? Name, email, company name, etc. Do we need categories to help searches? Sure! So we added that making it easy for the different user types to connect.&lt;br /&gt;&lt;br /&gt;&lt;div class="separator" style="clear: both; text-align: center;"&gt;&lt;a href="http://4.bp.blogspot.com/_8Lbw_GBR3W4/S7FYMTh_Y5I/AAAAAAAABE0/E4SA04l-B08/s1600/mockup.jpg" imageanchor="1" style="margin-left: 1em; margin-right: 1em;"&gt;&lt;img border="0" src="http://4.bp.blogspot.com/_8Lbw_GBR3W4/S7FYMTh_Y5I/AAAAAAAABE0/E4SA04l-B08/s320/mockup.jpg" /&gt;&lt;/a&gt;&lt;/div&gt;&lt;br /&gt;&lt;br /&gt;One of us was working on the white board, while the other entered stories into the excellent &lt;a href="http://www.pivotaltracker.com/" target="_blank"&gt;Pivotal Tracker&lt;/a&gt;.&lt;br /&gt;We finished around 3:30 PM. I was tired, but we did accomplish what we wanted. Create stories and start to move forward with the project.&lt;br /&gt;&lt;br /&gt;What's next? We'll pick a free web template and just build up HTML prototypes. Our idea is vague right now and we want to make changes freely and easily. Having a DB back-end would slow us down when we want to switch things around. We'll learn about the application by doing this and asking ourselves: As User Type "A" when I go to my dashboard, should I see historical items to do rating?&lt;br /&gt;Development will start when we are OK with our HTML pages.&lt;br /&gt;&lt;br /&gt;Do you follow the same process with your customers?&lt;div class="blogger-post-footer"&gt;&lt;img width='1' height='1' src='https://blogger.googleusercontent.com/tracker/5573355675806579511-1386062251864159773?l=www.adomokos.com' alt='' /&gt;&lt;/div&gt;</content><link rel='replies' type='application/atom+xml' href='http://www.adomokos.com/feeds/1386062251864159773/comments/default' title='Post Comments'/><link rel='replies' type='text/html' href='http://www.adomokos.com/2010/03/planning-meeting.html#comment-form' title='0 Comments'/><link rel='edit' type='application/atom+xml' href='http://www.blogger.com/feeds/5573355675806579511/posts/default/1386062251864159773'/><link rel='self' type='application/atom+xml' href='http://www.blogger.com/feeds/5573355675806579511/posts/default/1386062251864159773'/><link rel='alternate' type='text/html' href='http://www.adomokos.com/2010/03/planning-meeting.html' title='Planning Meeting'/><author><name>Attila Domokos</name><uri>http://www.blogger.com/profile/09067995287578229487</uri><email>noreply@blogger.com</email><gd:image rel='http://schemas.google.com/g/2005#thumbnail' width='32' height='32' src='http://4.bp.blogspot.com/-jSMgYgaIqF8/TtJneb7-ZyI/AAAAAAAAB6k/U9Hd923CNvM/s220/adomokos_bw.png'/></author><media:thumbnail xmlns:media='http://search.yahoo.com/mrss/' url='http://2.bp.blogspot.com/_8Lbw_GBR3W4/S7FX7rHMhiI/AAAAAAAABEs/Kdtf8cKJoi0/s72-c/agenda.jpg' height='72' width='72'/><thr:total>0</thr:total></entry><entry><id>tag:blogger.com,1999:blog-5573355675806579511.post-8380789466540351917</id><published>2010-03-14T22:07:00.003-04:00</published><updated>2010-03-15T09:40:42.719-04:00</updated><title type='text'>Testing Excel with Cucumber</title><content type='html'>Testing an Excel application with &lt;a href="http://ruby-doc.org/stdlib/libdoc/test/unit/rdoc/classes/Test/Unit.html" target="_blank"&gt;Ruby's unit testing tool&lt;/a&gt; is doable, however, the tests are not easily readable. Why not using Given/When/Then structured &lt;a href="http://cukes.info/" target="_blank"&gt;Cucumber&lt;/a&gt; tests instead? Cucumber is an active &lt;a href="http://wiki.github.com/aslakhellesoy/cucumber/" target="_blank"&gt;open source project&lt;/a&gt; enjoying great popularity in the Rails universe. I won’t be describing the tool in this blog post, if you’re new to Cucumber, please visit the official website or read the excellent &lt;a href="http://www.pragprog.com/titles/achbd/the-rspec-book" target="_blank"&gt;Cucumber book&lt;/a&gt; to learn more about it.&lt;br /&gt;&lt;br /&gt;Here is the task I need to write tests for: we have a basic Excel spreadsheet with a handful of cells in it.&lt;br /&gt;&lt;br /&gt;&lt;img border="0" src="http://3.bp.blogspot.com/_8Lbw_GBR3W4/S51AyXQGHsI/AAAAAAAABEQ/7-fNaB9YwF8/s320/excel_spreadsheet.jpg" /&gt;&lt;br /&gt;&lt;br /&gt;The row cells with yellow background are the column headers. Column cells starting from A2 are captions. "Category1" has three child elements and "Category2" has only two. The category rows are calculated by the sum of their children. Value Total is the sum of Value1 and Value2 for each row. The totals in the bottom are calculated by adding up Category1 and Category2 values.&lt;br /&gt;&lt;br /&gt;First I'd like to verify that the column headers and captions are in place. I started writing my feature this way:&lt;br /&gt;&lt;br /&gt;&lt;span class="Apple-style-span" style="color: blue;"&gt;Feature: Modify values in the Excel sheet&lt;/span&gt;&lt;br /&gt;&lt;span class="Apple-style-span" style="color: blue;"&gt;In order to show my power&lt;/span&gt;&lt;br /&gt;&lt;span class="Apple-style-span" style="color: blue;"&gt;&amp;nbsp;&amp;nbsp;As a user&lt;/span&gt;&lt;br /&gt;&lt;span class="Apple-style-span" style="color: blue;"&gt;&amp;nbsp;&amp;nbsp;I want to interact with Excel&lt;/span&gt;&lt;br /&gt;&lt;br /&gt;&lt;span class="Apple-style-span" style="color: blue;"&gt;Scenario: Display column headers and captions&lt;/span&gt;&lt;br /&gt;&lt;span class="Apple-style-span" style="color: blue;"&gt;&amp;nbsp;&amp;nbsp;Given I have 2 categories&lt;/span&gt;&lt;br /&gt;&lt;span class="Apple-style-span" style="color: blue;"&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;And I have 3 child elements under the first category&lt;/span&gt;&lt;br /&gt;&lt;span class="Apple-style-span" style="color: blue;"&gt;&amp;nbsp;&amp;nbsp;When I open the Excel workbook&lt;/span&gt;&lt;br /&gt;&lt;span class="Apple-style-span" style="color: blue;"&gt;&amp;nbsp;&amp;nbsp;Then I should see "Category" in the "A1" cell&lt;/span&gt;&lt;br /&gt;&lt;span class="Apple-style-span" style="color: blue;"&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;And I should see "Value Total" in the "B1" cell&lt;/span&gt;&lt;br /&gt;&lt;span class="Apple-style-span" style="color: blue;"&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;And I should see "Value1" in the "C1" cell&lt;/span&gt;&lt;br /&gt;&lt;span class="Apple-style-span" style="color: blue;"&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;And I should see "Value2" in the "D1" cell&lt;/span&gt;&lt;br /&gt;&lt;span class="Apple-style-span" style="color: blue;"&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;And I should see "Category1" in the "A2" cell&lt;/span&gt;&lt;br /&gt;&lt;span class="Apple-style-span" style="color: blue;"&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;And I should see "Child1" in the "A3" cell&lt;/span&gt;&lt;br /&gt;&lt;span class="Apple-style-span" style="color: blue;"&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;And I should see "Child2" in the "A4" cell&lt;/span&gt;&lt;br /&gt;&lt;br /&gt;I created the following folder structure:&lt;br /&gt;/features&lt;br /&gt;&amp;nbsp;&amp;nbsp;|&lt;br /&gt;&amp;nbsp;&amp;nbsp;|-- step_definitions&lt;br /&gt;&amp;nbsp;&amp;nbsp;|&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;|-- excel_handler.rb&lt;br /&gt;&amp;nbsp;&amp;nbsp;|&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;|-- excel_steps.rb&lt;br /&gt;&amp;nbsp;&amp;nbsp;|-- support&lt;br /&gt;&amp;nbsp;&amp;nbsp;|&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;|-- env.rb&lt;br /&gt;&amp;nbsp;&amp;nbsp;| - excel.feature&lt;br /&gt;&lt;br /&gt;The feature from above was saved in the excel.feature file.&lt;br /&gt;&lt;br /&gt;I am not particularly concerned with the "Given" part of the scenario. The data can be loaded into Excel either from a CSV, XML or a remote data store. I'll ignore this part to keep my examples clear and concise.&lt;br /&gt;&lt;br /&gt;My &lt;a href="http://adomokos.blogspot.com/2010/03/automated-testing-of-oba-app-with-ruby.html" target="_blank"&gt;previous blog post&lt;/a&gt; described how I can interact with Excel through the great &lt;a href="http://ruby-doc.org/stdlib/libdoc/win32ole/rdoc/classes/WIN32OLE.html" target="_blank"&gt;WIN32OLE&lt;/a&gt; object. I created the ExcelHandler class which does that:&lt;br /&gt;&lt;pre class="ruby" name="code"&gt;class ExcelHandler&lt;br /&gt;&amp;nbsp;&amp;nbsp;include Singleton&lt;br /&gt;&lt;br /&gt;&amp;nbsp;&amp;nbsp;# set this to your Excel file path&lt;br /&gt;&amp;nbsp;&amp;nbsp;@@excel_file_path = 'C:\Temp\TestWorkbook.xlsx'&lt;br /&gt;&amp;nbsp;&amp;nbsp;def open_excel&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;begin&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;@excel = WIN32OLE.connect('excel.application')&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;@wb = @excel.ActiveWorkbook&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;rescue&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;@excel = WIN32OLE::new('excel.application')&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;@excel.visible =true&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;@wb = @excel.Workbooks.Open(@@excel_file_path )&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;end&lt;br /&gt;&amp;nbsp;&amp;nbsp;end&lt;br /&gt;end&lt;br /&gt;&lt;/pre&gt;&lt;br /&gt;I might not have the most elegant code in the open_excel method, but this allows me to attach to a running instance of the Excel workbook which is a big thing for me. In case the workbook has not been launched yet, I take care of it here. Launching and closing Excel takes time and resources, I'd like to reuse running instances of Excel whenever I can.&lt;br /&gt;&lt;br /&gt;I take advantage of the singleton pattern in this class. Starting up Excel is an expensive operation, I want to make sure that one and only instance is handling the workbook. My tests are single threaded, I think I should be safe there.&lt;br /&gt;&lt;br /&gt;The env.rb file has the require statements:&lt;br /&gt;&lt;pre class="ruby" name="code"&gt;require 'spec/expectations'&lt;br /&gt;require 'win32ole'&lt;br /&gt;require 'singleton'&lt;br /&gt;&lt;/pre&gt;&lt;br /&gt;All the magic happens in the excel_steps.rb file:&lt;br /&gt;&lt;pre class="ruby" name="code"&gt;When /^I open the Excel workbook$/ do&lt;br /&gt;&amp;nbsp;&amp;nbsp;ExcelHandler.instance.open_excel&lt;br /&gt;&amp;nbsp;&amp;nbsp;@worksheet = ExcelHandler.instance.worksheet&lt;br /&gt;&amp;nbsp;&amp;nbsp;@worksheet.extend CellValueGetter&lt;br /&gt;end&lt;br /&gt;&lt;br /&gt;Then /^I should see "([^\"]*)" in the "([^\"]*)" cell$/ do |value, cell|&lt;br /&gt;&amp;nbsp;&amp;nbsp;@worksheet.get_cell_value(cell).strip.should == value&lt;br /&gt;end&lt;br /&gt;&lt;br /&gt;module CellValueGetter&lt;br /&gt;&amp;nbsp;&amp;nbsp;def get_cell_value(cell)&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;get_cell(cell).value&lt;br /&gt;&amp;nbsp;&amp;nbsp;end&lt;br /&gt;&lt;br /&gt;&amp;nbsp;&amp;nbsp;def set_cell_value(cell, value)&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;get_cell(cell).value = value&lt;br /&gt;&amp;nbsp;&amp;nbsp;end&lt;br /&gt;&lt;br /&gt;&amp;nbsp;&amp;nbsp;def get_cell(cell)&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;cell_values = cell.split('')&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;cell_values.length.should == 2&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;cells(cell_values[1].to_i, cell_values[0])&lt;br /&gt;&amp;nbsp;&amp;nbsp;end&lt;br /&gt;end&lt;br /&gt;&lt;/pre&gt;&lt;br /&gt;Look at how I add methods to the @worksheet object. You gotta love Ruby for that!&lt;br /&gt;The methods in this module are responsible for getting and setting a value based on the provided cell.&lt;br /&gt;I left out the Given parts that I ignore anyway. You can look at the entire source code after you pulled it from my &lt;a href="http://github.com/adomokos/cucumber_excel" target="_blank"&gt;github account&lt;/a&gt;.&lt;br /&gt;When I execute "cucumber features" in the command prompt I get this:&lt;br /&gt;&lt;br /&gt;&lt;span class="Apple-style-span" style="color: #6aa84f;"&gt;1&lt;/span&gt;&lt;span class="Apple-style-span" style="color: #38761d;"&gt; scenario (1 passed)&lt;/span&gt;&lt;br /&gt;&lt;span class="Apple-style-span" style="color: #38761d;"&gt;10 steps (10 passed)&lt;/span&gt;&lt;br /&gt;&lt;span class="Apple-style-span" style="color: #38761d;"&gt;0m0.027s&lt;/span&gt;&lt;br /&gt;&lt;br /&gt;Hey, the first scenario is passing!!&lt;br /&gt;&lt;br /&gt;All right, let's verify in the second scenario that the data was loaded correctly:&lt;br /&gt;&lt;span class="Apple-style-span" style="color: blue;"&gt;&lt;br /&gt;Scenario: Display loaded values&lt;br /&gt;&amp;nbsp;&amp;nbsp;Given I have 2 categories&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;And I have 3 child elements under the first category&lt;br /&gt;&amp;nbsp;&amp;nbsp;When I open the Excel workbook&lt;br /&gt;&amp;nbsp;&amp;nbsp;Then I should see 111 in the "C3" cell&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;And I should see 353 in the "C2" cell&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;And I should see 458 in the "B3" cell&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;And I should see 1523 in the "B2" cell&lt;/span&gt;&lt;br /&gt;&lt;br /&gt;I had to add one step definition to the excel_step.rb file:&lt;br /&gt;&lt;pre class="ruby" name="code"&gt;Then /^I should see (\d+) in the "([^\"]*)" cell$/ do |value, cell|&lt;br /&gt;&amp;nbsp;&amp;nbsp;@worksheet.get_cell_value(cell).should == value.to_i&lt;br /&gt;end&lt;br /&gt;&lt;/pre&gt;&lt;br /&gt;When I execute "cucumber features" in the command prompt I see this:&lt;br /&gt;&lt;br /&gt;&lt;span class="Apple-style-span" style="color: #6aa84f;"&gt;1 scenario (1 passed)&lt;br /&gt;7 steps (7 passed)&lt;br /&gt;0m0.024s&lt;br /&gt;&lt;/span&gt;&lt;br /&gt;&lt;br /&gt;I know I am not using the "Given" part of the scenarios, however, I do repeat code there. I used the &lt;a href="http://wiki.github.com/aslakhellesoy/cucumber/background" target="_blank"&gt;background feature&lt;/a&gt; of Cucumber and DRY-ed up my scenarios a little bit.&lt;br /&gt;&lt;br /&gt;&lt;span class="Apple-style-span" style="color: blue;"&gt;Background:&lt;br /&gt;Given I have 2 categories&lt;br /&gt;And I have 3 child elements under the first category&lt;/span&gt;&lt;br /&gt;&lt;br /&gt;I use &lt;a href="http://wiki.github.com/aslakhellesoy/cucumber/scenario-outlines" target="_blank"&gt;scenario outline&lt;/a&gt; in my third scenario. I set the "Value1" cell for the "Child1" row to 211. Take a look at the result in Excel:&lt;br /&gt;&lt;br /&gt;&lt;img border="0" height="240" src="http://2.bp.blogspot.com/_8Lbw_GBR3W4/S51KXgqwOiI/AAAAAAAABEY/iAxkSu5gaec/s320/excel_spreadsheet_add_100.jpg" width="320" /&gt;&lt;br /&gt;&lt;br /&gt;I also try to set the same cell to 51, I got these numbers then:&lt;br /&gt;&lt;br /&gt;&lt;img border="0" height="240" src="http://4.bp.blogspot.com/_8Lbw_GBR3W4/S51KvILkxPI/AAAAAAAABEg/ecYsJj8wtaE/s320/excel_spreadsheet_subtract_160.jpg" width="320" /&gt;&lt;br /&gt;&lt;br /&gt;I am verifying numbers in red in the last scenario:&lt;br /&gt;&lt;br /&gt;&lt;span class="Apple-style-span" style="color: blue;"&gt;Scenario Outline: Change values&lt;br /&gt;&amp;nbsp;&amp;nbsp;Given the default values were loaded&lt;br /&gt;&amp;nbsp;&amp;nbsp;When I open the Excel workbook&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;And I put &amp;lt;child1_value&amp;gt; in the "C3" cell&lt;br /&gt;&amp;nbsp;&amp;nbsp;Then I should see &amp;lt;category_value1&amp;gt; in the "C2" cell&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;And I should see &amp;lt;child1_sum&amp;gt; in the "B3" cell&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;And I should see &amp;lt;category_total&amp;gt; in the "B2" cell&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;And I should see &amp;lt;value1_total&amp;gt; in the "C9" cell&lt;br /&gt;&lt;br /&gt;Examples:&lt;br /&gt;| child1_value | category_value1 | child1_sum | category_total | value1_total |&lt;br /&gt;| 211            | 453                  |  558          | 1623              | 1281           |&lt;br /&gt;| 51              | 293                  | 398           | 1463              | 1121           |&lt;/value1_total&gt;&lt;/category_total&gt;&lt;/child1_sum&gt;&lt;/category_value1&gt;&lt;/child1_value&gt;&lt;/span&gt;&lt;br /&gt;&lt;br /&gt;I added the following step to the excel_steps.rb file:&lt;br /&gt;&lt;pre class="ruby" name="code"&gt;When /^I put (\d+) in the "([^\"]*)" cell$/ do |value, cell|&lt;br /&gt;&amp;nbsp;&amp;nbsp;@worksheet.set_cell_value(cell, value)&lt;br /&gt;end&lt;br /&gt;&lt;/pre&gt;&lt;br /&gt;Let's see what we get now:&lt;br /&gt;&lt;br /&gt;&lt;span class="Apple-style-span" style="color: #6aa84f;"&gt;2 scenarios (2 passed)&lt;br /&gt;18 steps (18 passed)&lt;br /&gt;0m0.038s&lt;/span&gt;&lt;br /&gt;&lt;br /&gt;All the scenarios passed individually; let's see how we do when we execute them all at once:&lt;br /&gt;&lt;br /&gt;&lt;span class="Apple-style-span" style="color: #6aa84f;"&gt;4 scenarios (4 passed)&lt;br /&gt;37 steps (37 passed)&lt;br /&gt;0m3.718s&lt;/span&gt;&lt;br /&gt;&lt;br /&gt;Everything is rock solid so far. Am I testing the right file? I change the first scenario's "Then I should see "Category" in the "A1" cell" line to this: "Then I should see "CategoryABC" in the "A1" cell".&lt;br /&gt;When I execute "cucumber features" I get the following output:&lt;br /&gt;&lt;br /&gt;&lt;span class="Apple-style-span" style="color: #cc0000;"&gt;4 scenarios (1 failed, 3 passed)&lt;/span&gt;&lt;br /&gt;&lt;span class="Apple-style-span" style="color: #cc0000;"&gt;37 steps (1 failed, 6 skipped, 30 passed)&lt;/span&gt;&lt;br /&gt;&lt;span class="Apple-style-span" style="color: #cc0000;"&gt;0m3.710s&lt;/span&gt;&lt;br /&gt;&lt;br /&gt;I see that the scenario I just changed is now failing:&lt;br /&gt;&lt;br /&gt;&lt;span class="Apple-style-span" style="color: #cc0000;"&gt;Then I should see "CategoryABC" in the "A1" cell     # features/step_definitions/excel_steps.rb:23&lt;br /&gt;&amp;nbsp;&amp;nbsp;expected: "CategoryABC",&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;got: "Category" (using ==)&lt;br /&gt;&amp;nbsp;&amp;nbsp;Diff:&lt;br /&gt;&amp;nbsp;&amp;nbsp;@@ -1,2 +1,2 @@&lt;br /&gt;&amp;nbsp;&amp;nbsp;-CategoryABC&lt;br /&gt;&amp;nbsp;&amp;nbsp;+Category&lt;br /&gt;&amp;nbsp;&amp;nbsp;(Spec::Expectations::ExpectationNotMetError)&lt;br /&gt;&amp;nbsp;&amp;nbsp;./features/step_definitions/excel_steps.rb:24:in `/^I should see "([^\"]*)" in the "([^\"]*)" cell$/'&lt;br /&gt;features\excel.feature:13:in `Then I should see "CategoryABC" in the "A1" cell'&lt;/span&gt;&lt;br /&gt;&lt;br /&gt;I change it back to the original one and everything is OK again.&lt;br /&gt;&lt;br /&gt;One more thing I need to do: when the feature is complete, I'd like to close Excel.&lt;br /&gt;&lt;br /&gt;I use the cucumber's at_exit hook to accomplish that:&lt;br /&gt;&lt;pre class="ruby" name="code"&gt;at_exit do&lt;br /&gt;&amp;nbsp;&amp;nbsp;ExcelHandler.instance.close_excel&lt;br /&gt;end&lt;/pre&gt;&lt;br /&gt;When I execute the cucumber feature I see Excel popping up and closing down.&lt;br /&gt;What if I wanted to run the feature in headless mode? It's simple, I just have to change the @excel.visible = true value to false in the excel_handler.rb file.&lt;br /&gt;&lt;br /&gt;I think cucumber is a great way to automate testing on an Excel spreadsheet. The cucumber features can serve both as requirement documents and test scripts, hence providing executable documentation. They are easy to read and understand for the entire team.&lt;br /&gt;&lt;br /&gt;You can get the demo's source code from my &lt;a href="http://github.com/adomokos/cucumber_excel" target="_blank"&gt;github account&lt;/a&gt;.&lt;br /&gt;&lt;br /&gt;I developed the code on Windows 7 using ruby version 1.9.1. You also need cucumber and the rspec gems installed to run the code.&lt;br /&gt;&lt;br /&gt;Happy coding!&lt;div class="blogger-post-footer"&gt;&lt;img width='1' height='1' src='https://blogger.googleusercontent.com/tracker/5573355675806579511-8380789466540351917?l=www.adomokos.com' alt='' /&gt;&lt;/div&gt;</content><link rel='replies' type='application/atom+xml' href='http://www.adomokos.com/feeds/8380789466540351917/comments/default' title='Post Comments'/><link rel='replies' type='text/html' href='http://www.adomokos.com/2010/03/testing-excel-with-cucumber.html#comment-form' title='9 Comments'/><link rel='edit' type='application/atom+xml' href='http://www.blogger.com/feeds/5573355675806579511/posts/default/8380789466540351917'/><link rel='self' type='application/atom+xml' href='http://www.blogger.com/feeds/5573355675806579511/posts/default/8380789466540351917'/><link rel='alternate' type='text/html' href='http://www.adomokos.com/2010/03/testing-excel-with-cucumber.html' title='Testing Excel with Cucumber'/><author><name>Attila Domokos</name><uri>http://www.blogger.com/profile/09067995287578229487</uri><email>noreply@blogger.com</email><gd:image rel='http://schemas.google.com/g/2005#thumbnail' width='32' height='32' src='http://4.bp.blogspot.com/-jSMgYgaIqF8/TtJneb7-ZyI/AAAAAAAAB6k/U9Hd923CNvM/s220/adomokos_bw.png'/></author><media:thumbnail xmlns:media='http://search.yahoo.com/mrss/' url='http://3.bp.blogspot.com/_8Lbw_GBR3W4/S51AyXQGHsI/AAAAAAAABEQ/7-fNaB9YwF8/s72-c/excel_spreadsheet.jpg' height='72' width='72'/><thr:total>9</thr:total></entry><entry><id>tag:blogger.com,1999:blog-5573355675806579511.post-44980817507509733</id><published>2010-03-06T19:39:00.000-05:00</published><updated>2010-03-13T22:42:14.960-05:00</updated><category scheme='http://www.blogger.com/atom/ns#' term='ruby testing OBA VSTO Excel'/><title type='text'>Automated Testing of an OBA App with Ruby</title><content type='html'>Hello!&lt;br /&gt;&lt;br /&gt;You could call a me a late adopter, or a busy dad, but what matters is that it's finally here. I am writing my first blog post!!&lt;br /&gt;I was thinking what the best topic could be for The First one? How I got into programming? Or what I worked on yesterday? Maybe a little bit of both.&lt;br /&gt;&lt;br /&gt;I've been doing .NET development as a day job but I am hacking out Ruby/Rails code in the evenings.&lt;br /&gt;I am test infected, I can't live without tests and it really bothers me when I see code written without supporting tests around it. I've seen far too many projects starting out with everything running smooth: features are being developed with the speed of light, everybody is happy. And then, maybe 4-6 months into the project development speed slows down, defect rates go up and the code is just plain fragile. Nobody dares to touch it, not knowing what could go wrong. Changing anything is close to impossible.&lt;br /&gt;&lt;br /&gt;I admire the testing enthusiasm I see in the Ruby community. The way they practice TDD, BDD, the way they won't write a single line of code without a failing test first. And while Ruby is really flexible, it can be very dangerous. I'd say not having sufficient test coverage in a dynamic language is like you sitting in car running towards a cliff.&lt;br /&gt;&lt;br /&gt;I work on an OBA (&lt;a href="http://msdn.microsoft.com/en-us/office/aa905528.aspx" target="_blank"&gt;Office Business Application&lt;/a&gt;) project at work. We have decent unit test coverage, however, not having automated functional testing is a risk. Sure we have QA staff, they can just go through the test scripts manually over and over. I think what could automated should be automated.&lt;br /&gt;I bumped into the win 32 OLE automation object in ruby (&lt;a href="http://ruby-doc.org/core/classes/WIN32OLE.html"&gt;http://ruby-doc.org/core/classes/WIN32OLE.html&lt;/a&gt;) a couple of months ago but I never had the time to fully investigate it. I was able to open Excel, read and write a value in the cell, save the worksheet and close it. This was all cool, but not enough. &lt;br /&gt;&lt;br /&gt;&lt;pre class="ruby" name="code"&gt;require 'win32ole'&lt;br /&gt;require 'test/unit'&lt;br /&gt;ExcelTest &amp;lt; Test::Unit::TestCase&lt;br /&gt;&lt;br /&gt;&amp;nbsp;def setup&lt;br /&gt;&amp;nbsp;&amp;nbsp;@excel = WIN32OLE::new("Excel.Application")&lt;br /&gt;&amp;nbsp;&amp;nbsp;@excel.visible = true&lt;br /&gt;&amp;nbsp;&amp;nbsp;@workbook = @excel.Workbooks.Open("C:\\path_to_your_excel_file\some_file.xls")&lt;br /&gt;&amp;nbsp;&amp;nbsp;@sheet1 = @workbook.worksheets(1)&lt;br /&gt;&amp;nbsp;&amp;nbsp;@sheet2 = @workbook.worksheets(2)&lt;br /&gt;&amp;nbsp;end&lt;br /&gt;&lt;br /&gt;&amp;nbsp;def test_should_verify_captions&lt;br /&gt;&amp;nbsp;&amp;nbsp;assert_equal('Category 1', @sheet1.cells(2, "A").value)&lt;br /&gt;&amp;nbsp;&amp;nbsp;assert_equal('Child 1', @sheet1.cells(3, "A").value)&lt;br /&gt;&amp;nbsp;&amp;nbsp;assert_equal('Child 2', @sheet1.cells(4, "A").value)&lt;br /&gt;&amp;nbsp;&amp;nbsp;assert_equal('Child 3', @sheet1.cells(5, "A").value)&lt;br /&gt;&amp;nbsp;&amp;nbsp;assert_equal('Child 4', @sheet1.cells(6, "A").value)&lt;br /&gt;&amp;nbsp;end&lt;br /&gt;end&lt;br /&gt;&lt;/pre&gt;&lt;br /&gt;I am trying to use Ruby unit testing. It's easy and simple for now. In the setup method I create an Excel object with the tested workbook loaded into it. I create two instance variables for the worksheets to keep my code simple.&lt;br /&gt;&lt;br /&gt;Then in the first test, I verify caption cells. Nothing magical here, I just make sure that in the 2nd row's "A" column the cell should have a value of "Category1".&lt;br /&gt;&lt;br /&gt;To be continued...&lt;div class="blogger-post-footer"&gt;&lt;img width='1' height='1' src='https://blogger.googleusercontent.com/tracker/5573355675806579511-44980817507509733?l=www.adomokos.com' alt='' /&gt;&lt;/div&gt;</content><link rel='replies' type='application/atom+xml' href='http://www.adomokos.com/feeds/44980817507509733/comments/default' title='Post Comments'/><link rel='replies' type='text/html' href='http://www.adomokos.com/2010/03/automated-testing-of-oba-app-with-ruby.html#comment-form' title='1 Comments'/><link rel='edit' type='application/atom+xml' href='http://www.blogger.com/feeds/5573355675806579511/posts/default/44980817507509733'/><link rel='self' type='application/atom+xml' href='http://www.blogger.com/feeds/5573355675806579511/posts/default/44980817507509733'/><link rel='alternate' type='text/html' href='http://www.adomokos.com/2010/03/automated-testing-of-oba-app-with-ruby.html' title='Automated Testing of an OBA App with Ruby'/><author><name>Attila Domokos</name><uri>http://www.blogger.com/profile/09067995287578229487</uri><email>noreply@blogger.com</email><gd:image rel='http://schemas.google.com/g/2005#thumbnail' width='32' height='32' src='http://4.bp.blogspot.com/-jSMgYgaIqF8/TtJneb7-ZyI/AAAAAAAAB6k/U9Hd923CNvM/s220/adomokos_bw.png'/></author><thr:total>1</thr:total></entry></feed>
