tag:www.rhnh.net,2008:/rack Rack - Xavier Shay's Blog 2009-11-18T19:25:58Z Enki Xavier Shay notreal@rhnh.net tag:www.rhnh.net,2008:Post/807 2009-11-18T19:25:00Z 2009-11-18T19:25:58Z Full stack testing rack applications <p>Herein is described a method for full stack testing <a href="http://getcloudkit.com/">CloudKit</a> apps. The same techniques could easily be applied to other rack web application or framework, which is pretty much all the ruby ones these days (<a href="http://rubyonrails.org/">rails</a>, <a href="http://www.sinatrarb.com/">sinatra</a>, <a href="http://github.com/hassox/pancake">pancake</a>, etc&#8230;) This method is ideal for non-html services. For <span class="caps">HTML</span> you&#8217;re probably better off just using webrat/selenium.</p> There are two external services that make up our stack: <ul> <li>CloudKit application</li> <li>OpenID server</li> </ul> <p>Both of these are rack applications, so we can start them up using the same method in our spec helper.</p><table class="CodeRay"><tr> <td class="line_numbers" title="click to toggle" onclick="with (this.firstChild.style) { display = (display == '') ? 'none' : '' }"><pre>1<tt> </tt>2<tt> </tt>3<tt> </tt>4<tt> </tt>5<tt> </tt>6<tt> </tt>7<tt> </tt>8<tt> </tt>9<tt> </tt><strong>10</strong><tt> </tt>11<tt> </tt>12<tt> </tt>13<tt> </tt>14<tt> </tt>15<tt> </tt>16<tt> </tt>17<tt> </tt>18<tt> </tt>19<tt> </tt><strong>20</strong><tt> </tt>21<tt> </tt>22<tt> </tt>23<tt> </tt>24<tt> </tt>25<tt> </tt>26<tt> </tt>27<tt> </tt></pre></td> <td class="code"><pre ondblclick="with (this.style) { overflow = (overflow == 'auto' || overflow == '') ? 'visible' : 'auto' }">require <span class="s"><span class="dl">'</span><span class="k">spec</span><span class="dl">'</span></span><tt> </tt>require <span class="s"><span class="dl">'</span><span class="k">pathname</span><span class="dl">'</span></span><tt> </tt>require Pathname(<span class="pc">__FILE__</span>).dirname + <span class="s"><span class="dl">'</span><span class="k">support/application_server</span><span class="dl">'</span></span><tt> </tt>require Pathname(<span class="pc">__FILE__</span>).dirname + <span class="s"><span class="dl">'</span><span class="k">support/tcp_socket</span><span class="dl">'</span></span><tt> </tt><tt> </tt><span class="co">TEST_PORTS</span> = {<tt> </tt> <span class="sy">:app</span> =&gt; <span class="i">9293</span>,<tt> </tt> <span class="sy">:openid</span> =&gt; <span class="i">9294</span><tt> </tt>}<tt> </tt><tt> </tt><span class="gv">$servers</span> = <span class="pc">nil</span><tt> </tt><span class="co">Spec</span>::<span class="co">Runner</span>.configure <span class="r">do</span> |config|<tt> </tt> config.before(<span class="sy">:all</span>) <span class="r">do</span><tt> </tt> <span class="gv">$servers</span> ||= <span class="co">Support</span>::<span class="co">ApplicationServer</span>.multi_boot(<tt> </tt> {<tt> </tt> <span class="sy">:config</span> =&gt; <span class="co">File</span>.expand_path(<span class="co">Dir</span>.pwd + <span class="s"><span class="dl">'</span><span class="k">/config.ru</span><span class="dl">'</span></span>),<tt> </tt> <span class="sy">:port</span> =&gt; <span class="co">TEST_PORTS</span>[<span class="sy">:app</span>],<tt> </tt> <span class="sy">:daemonize</span> =&gt; <span class="pc">true</span><tt> </tt> },<tt> </tt> {<tt> </tt> <span class="sy">:config</span> =&gt; <span class="co">File</span>.expand_path(<span class="co">Dir</span>.pwd + <span class="s"><span class="dl">'</span><span class="k">/spec/support/rack_my_id.rb</span><span class="dl">'</span></span>),<tt> </tt> <span class="sy">:port</span> =&gt; <span class="co">TEST_PORTS</span>[<span class="sy">:openid</span>],<tt> </tt> <span class="sy">:daemonize</span> =&gt; <span class="pc">true</span><tt> </tt> }<tt> </tt> )<tt> </tt> <span class="r">end</span><tt> </tt><span class="r">end</span><tt> </tt></pre></td> </tr></table> You need some support files &#8211; the first two are based heavily on code from webrat, the latter is a dead simple OpenID server that I wrote specifically for testing: <ul> <li><a href="http://gist.github.com/237652">application_server.rb</a></li> <li><a href="http://gist.github.com/237652">tcp_socker.rb</a></li> <li><a href="http://github.com/xaviershay/rack-my-id">rack_my_id.rb</a></li> </ul> <p>A global variable is required here, since <code>before(:all)</code> in rspec runs once per describe block, rather than once per test run. An <code>at_exit</code> hook is used to shutdown the services after the test run.</p> <p>You need a way of resetting your data between test runs. The default <code>CloudKit::MemoryTable</code> does not provide a mechanism for this &#8211; any deleted resource will exist in the version history of that resource (and will respond with a 410 rather than 404). By subclassing <code>MemoryTable</code>, we can provide a <code>purge</code> method that does what we need:</p><table class="CodeRay"><tr> <td class="line_numbers" title="click to toggle" onclick="with (this.firstChild.style) { display = (display == '') ? 'none' : '' }"><pre>1<tt> </tt>2<tt> </tt>3<tt> </tt>4<tt> </tt>5<tt> </tt>6<tt> </tt>7<tt> </tt>8<tt> </tt>9<tt> </tt><strong>10</strong><tt> </tt>11<tt> </tt>12<tt> </tt>13<tt> </tt>14<tt> </tt>15<tt> </tt>16<tt> </tt>17<tt> </tt>18<tt> </tt></pre></td> <td class="code"><pre ondblclick="with (this.style) { overflow = (overflow == 'auto' || overflow == '') ? 'visible' : 'auto' }"><span class="c"># A custom storage adapter that allows a total purge of a collection</span><tt> </tt><span class="c"># This is handy in test mode to clear out data between specs</span><tt> </tt><span class="r">class</span> <span class="cl">PurgeableTable</span> &lt; <span class="co">CloudKit</span>::<span class="co">MemoryTable</span><tt> </tt> <span class="c"># Remove all resources in a collection.</span><tt> </tt> <span class="c"># Unlike a normal delete, which versions the resource (and sets up a 410 response),</span><tt> </tt> <span class="c"># this method removes all trace of the resource (it will 404).</span><tt> </tt> <span class="c">#</span><tt> </tt> <span class="c"># Example:</span><tt> </tt> <span class="c"># CloudKit.setup_storage_adapter(adapter = PurgeableTable.new)</span><tt> </tt> <span class="c"># adapter.purge('/items')</span><tt> </tt> <span class="r">def</span> <span class="fu">purge</span>(collection)<tt> </tt> query {|q|<tt> </tt> q.add_condition(<span class="s"><span class="dl">'</span><span class="k">collection_reference</span><span class="dl">'</span></span>, <span class="sy">:eql</span>, collection)<tt> </tt> }.each <span class="r">do</span> |item|<tt> </tt> <span class="iv">@hash</span>.delete(<span class="iv">@keys</span>.delete(item[<span class="sy">:pk</span>]))<tt> </tt> <span class="r">end</span><tt> </tt> <span class="r">end</span><tt> </tt><span class="r">end</span><tt> </tt></pre></td> </tr></table> <p>Since we&#8217;ll be testing the CloudKit app from a separate process, we also need a way of triggering a purge. An easy way is some custom rack middleware that provides a <span class="caps">URL</span> we can hit to reset the app. Clearly, we only want to enable this in test mode.</p><table class="CodeRay"><tr> <td class="line_numbers" title="click to toggle" onclick="with (this.firstChild.style) { display = (display == '') ? 'none' : '' }"><pre>1<tt> </tt>2<tt> </tt>3<tt> </tt>4<tt> </tt>5<tt> </tt>6<tt> </tt>7<tt> </tt>8<tt> </tt>9<tt> </tt><strong>10</strong><tt> </tt>11<tt> </tt>12<tt> </tt>13<tt> </tt>14<tt> </tt>15<tt> </tt>16<tt> </tt></pre></td> <td class="code"><pre ondblclick="with (this.style) { overflow = (overflow == 'auto' || overflow == '') ? 'visible' : 'auto' }"><span class="r">class</span> <span class="cl">ResetApp</span><tt> </tt> <span class="r">def</span> <span class="fu">initialize</span>(app, options = {})<tt> </tt> <span class="iv">@app</span> = app<tt> </tt> <span class="iv">@options</span> = options<tt> </tt> <span class="r">end</span><tt> </tt><tt> </tt> <span class="r">def</span> <span class="fu">call</span>(env)<tt> </tt> request = <span class="co">Rack</span>::<span class="co">Request</span>.new(env)<tt> </tt> <span class="r">if</span> request.path == <span class="s"><span class="dl">'</span><span class="k">/test_reset</span><span class="dl">'</span></span> &amp;&amp; request.request_method == <span class="s"><span class="dl">'</span><span class="k">POST</span><span class="dl">'</span></span><tt> </tt> <span class="iv">@options</span>[<span class="sy">:adapter</span>].purge(<span class="s"><span class="dl">'</span><span class="k">/items</span><span class="dl">'</span></span>)<tt> </tt> <span class="r">return</span> <span class="co">Rack</span>::<span class="co">Response</span>.new([], <span class="i">200</span>).finish<tt> </tt> <span class="r">else</span><tt> </tt> <span class="iv">@app</span>.call(env)<tt> </tt> <span class="r">end</span><tt> </tt> <span class="r">end</span><tt> </tt><span class="r">end</span><tt> </tt></pre></td> </tr></table> <table class="CodeRay"><tr> <td class="line_numbers" title="click to toggle" onclick="with (this.firstChild.style) { display = (display == '') ? 'none' : '' }"><pre>1<tt> </tt>2<tt> </tt>3<tt> </tt>4<tt> </tt>5<tt> </tt>6<tt> </tt></pre></td> <td class="code"><pre ondblclick="with (this.style) { overflow = (overflow == 'auto' || overflow == '') ? 'visible' : 'auto' }"><span class="c"># config.ru</span><tt> </tt><span class="co">CloudKit</span>.setup_storage_adapter(adapter = <span class="co">PurgeableTable</span>.new)<tt> </tt><tt> </tt><span class="r">if</span> <span class="co">ENV</span>[<span class="s"><span class="dl">&quot;</span><span class="k">RACK_ENV</span><span class="dl">&quot;</span></span>] == <span class="s"><span class="dl">'</span><span class="k">test</span><span class="dl">'</span></span><tt> </tt> use <span class="co">ResetApp</span>, <span class="sy">:adapter</span> =&gt; adapter<tt> </tt><span class="r">end</span><tt> </tt></pre></td> </tr></table> <p>Now all the infrastructure is set up, we can test the CloudKit app using familiar ruby <span class="caps">HTTP</span> libraries:</p><table class="CodeRay"><tr> <td class="line_numbers" title="click to toggle" onclick="with (this.firstChild.style) { display = (display == '') ? 'none' : '' }"><pre>1<tt> </tt>2<tt> </tt>3<tt> </tt>4<tt> </tt>5<tt> </tt>6<tt> </tt>7<tt> </tt>8<tt> </tt>9<tt> </tt><strong>10</strong><tt> </tt>11<tt> </tt>12<tt> </tt>13<tt> </tt>14<tt> </tt>15<tt> </tt>16<tt> </tt>17<tt> </tt>18<tt> </tt>19<tt> </tt><strong>20</strong><tt> </tt>21<tt> </tt>22<tt> </tt>23<tt> </tt>24<tt> </tt>25<tt> </tt>26<tt> </tt>27<tt> </tt>28<tt> </tt>29<tt> </tt><strong>30</strong><tt> </tt>31<tt> </tt>32<tt> </tt>33<tt> </tt>34<tt> </tt>35<tt> </tt>36<tt> </tt>37<tt> </tt>38<tt> </tt>39<tt> </tt></pre></td> <td class="code"><pre ondblclick="with (this.style) { overflow = (overflow == 'auto' || overflow == '') ? 'visible' : 'auto' }">require <span class="s"><span class="dl">'</span><span class="k">httparty</span><span class="dl">'</span></span><tt> </tt>require <span class="s"><span class="dl">'</span><span class="k">mechanize</span><span class="dl">'</span></span><tt> </tt>require <span class="s"><span class="dl">'</span><span class="k">json</span><span class="dl">'</span></span><tt> </tt>require <span class="s"><span class="dl">'</span><span class="k">oauth</span><span class="dl">'</span></span><tt> </tt><tt> </tt>describe <span class="s"><span class="dl">'</span><span class="k">OAuth + OpendID</span><span class="dl">'</span></span> <span class="r">do</span><tt> </tt> include <span class="co">HTTParty</span><tt> </tt> base_uri <span class="s"><span class="dl">&quot;</span><span class="k">localhost:</span><span class="il"><span class="idl">#{</span><span class="co">TEST_PORTS</span>[<span class="sy">:app</span>]<span class="idl">}</span></span><span class="dl">&quot;</span></span><tt> </tt><tt> </tt> before(<span class="sy">:each</span>) <span class="r">do</span><tt> </tt> <span class="co">HTTParty</span>.post(<span class="s"><span class="dl">&quot;</span><span class="k">/test_reset</span><span class="dl">&quot;</span></span>).code.should == <span class="i">200</span><tt> </tt> <span class="r">end</span><tt> </tt><tt> </tt> specify <span class="s"><span class="dl">'</span><span class="k">Registering for an oauth token</span><span class="dl">'</span></span> <span class="r">do</span><tt> </tt> <span class="iv">@consumer</span> = <span class="co">OAuth</span>::<span class="co">Consumer</span>.new(<span class="s"><span class="dl">'</span><span class="k">cloudkitconsumer</span><span class="dl">'</span></span>,<span class="s"><span class="dl">'</span><span class="dl">'</span></span>,<tt> </tt> <span class="sy">:site</span> =&gt; <span class="s"><span class="dl">&quot;</span><span class="k">http://localhost:</span><span class="il"><span class="idl">#{</span><span class="co">TEST_PORTS</span>[<span class="sy">:app</span>]<span class="idl">}</span></span><span class="dl">&quot;</span></span>,<tt> </tt> <span class="sy">:authorize_path</span> =&gt; <span class="s"><span class="dl">&quot;</span><span class="k">/oauth/authorization</span><span class="dl">&quot;</span></span>,<tt> </tt> <span class="sy">:access_token_path</span> =&gt; <span class="s"><span class="dl">&quot;</span><span class="k">/oauth/access_tokens</span><span class="dl">&quot;</span></span>,<tt> </tt> <span class="sy">:request_token_path</span> =&gt; <span class="s"><span class="dl">&quot;</span><span class="k">/oauth/request_tokens</span><span class="dl">&quot;</span></span><tt> </tt> )<tt> </tt> <span class="iv">@request_token</span> = <span class="iv">@consumer</span>.get_request_token<tt> </tt><tt> </tt> agent = <span class="co">WWW</span>::<span class="co">Mechanize</span>.new<tt> </tt> page = agent.get(<span class="iv">@request_token</span>.authorize_url)<tt> </tt> login_form = page.forms.first<tt> </tt> login_form.field_with(<span class="sy">:name</span> =&gt; <span class="s"><span class="dl">&quot;</span><span class="k">openid_url</span><span class="dl">&quot;</span></span>).value = <span class="s"><span class="dl">&quot;</span><span class="k">localhost:</span><span class="il"><span class="idl">#{</span><span class="co">TEST_PORTS</span>[<span class="sy">:openid</span>]<span class="idl">}</span></span><span class="dl">&quot;</span></span><tt> </tt> page = agent.submit(login_form)<tt> </tt><tt> </tt> oauth_form = page.forms.first<tt> </tt> page = agent.submit(oauth_form, oauth_form.button_with(<span class="sy">:value</span> =&gt; <span class="s"><span class="dl">&quot;</span><span class="k">Approve</span><span class="dl">&quot;</span></span>))<tt> </tt><tt> </tt> <span class="c"># Get access token</span><tt> </tt> <span class="iv">@access_token</span> = <span class="iv">@request_token</span>.get_access_token<tt> </tt><tt> </tt> <span class="c"># Update an item</span><tt> </tt> result = <span class="iv">@access_token</span>.put(<span class="s"><span class="dl">&quot;</span><span class="k">/items/12345</span><span class="dl">&quot;</span></span>, {<span class="sy">:name</span> =&gt; <span class="s"><span class="dl">&quot;</span><span class="k">Hello</span><span class="dl">&quot;</span></span>}.to_json)<tt> </tt> result.code.should == <span class="s"><span class="dl">&quot;</span><span class="k">201</span><span class="dl">&quot;</span></span><tt> </tt> <span class="r">end</span><tt> </tt><span class="r">end</span><tt> </tt></pre></td> </tr></table> <p>There&#8217;s a lot of code and not much supporting text here. I&#8217;m hoping it all just clicks together pretty easy. Hit me up with any questions.</p>