9 min read

Historically, a conventional Ruby on Rails application leverages server-side business logic, a relational database, and a RESTful architecture to serve dynamically-generated HTML. However, JavaScript-intensive applications and widespread use of external web APIs somewhat challenge this architecture. In many cases, Rails is tasked with performing as an orchestration layer, collecting data from various backend services and serving re-formatted JSON or XML to clients. In such instances, how is Rails’ model-view-controller architecture still relevant? In the second part of this series, we’ll continue with the creation of Noterizer that we started on in Part 1, creating a simple Rails backend that makes requests to an external XML-based web service and serves JSON. We’ll use RSpec for tests and Jbuilder for view rendering.

Building out the Noterizer backend

Currently, NotesController serves an empty JSON document, yet the goal is to serve a JSON array representing the XML data served by the aforementioned NotesXmlService endpoints:

http://NotesXmlService.herokuapp.com/note-one
http://NotesXmlService.herokuapp.com/note-two

Creating a Note model

First, create a model to represent the note data returned by each of the NotesXmlService endpoints. In a more traditional Rails application, such a model represents data from a database. In Noterizer, the Note model provides a Ruby interface to each NotesXmlService XML document.

Create a Note model:

$ touch app/models/note.rb

On initialization, Note should perform an HTTP request to the URL it’s passed on instantiation and expose relevant XML values via its public methods. The Note class will use Nokogiri to parse the XML.

Require the nokogiri gem by adding the following to your Gemfile:

gem 'nokogiri'

Install nokogiri:

$ bundle install

Let’s also store the NotesXmlService base URL in a config property. This allows us to easily change its value throughout the application should it ever change. Add the following to config/application.rb:

config.notes_xml_service_root = ‘http://notesxmlservice.herokuapp.com’

Add the following to app/models/note.rb:

require 'nokogiri'
class Note
def initialize(path = nil)
   @uri = URI.parse(“#{Rails.config.notes_xml_service_root}/#{path}”)
   @xml = get_and_parse_response
   create_methods
end
private
def get_and_parse_response
   Nokogiri::XML(get_response)
end
def get_response
   http = Net::HTTP.new(@uri.host, @uri.port)
   http.request(request).body
end
def request
   Net::HTTP::Get.new(@uri.request_uri)
end
def create_methods
   available_methods.each do |method|
     self.class.send(:define_method, method) { fetch_value method.to_s }
   end
end
def available_methods
   [
     :to,
     :from,
     :heading,
     :body
   ]
end
def fetch_value(value)
   @xml.xpath("//add[@key='#{value}']/@value").text
end
end

“`

The Note class now works like this:

  1. It performs an HTTP request to the URL it’s passed on initialization.

  2. It leverages Nokogiri to parse the resulting XML.

  3. It uses Nokogiri’s support of XPATH expressions to dynamically create to, from, heading, and body methods based on the corresponding values in the XML.

Testing the Note model

Let’s test Note by creating a corresponding model spec file:

$ rails g rspec:model note

First, test the Note#to method by adding the following to the newly created spec/models/note_spec.rb:

require 'rails_helper'
RSpec.describe Note, :type => :model do
before do
   @note = Note.new(‘note-one')
end
describe '#to' do
   it 'returns the correct "to" value from the XML' do
     expect(@note.to).to eq 'Samantha'
   end
end
end

Running rake spec reveals that the test passes, though it performs a real HTTP request. This is not ideal: such requests generate unwelcome traffic on NotesServiceDmo, makes hard-coded assumptions about the XML returned by the /note-one endpoint, and relies upon an Internet connection to pass.

Let’s configure RSpec to use WebMock to fake HTTP requests.

Add webmock to the Gemfile; specify that it’s part of the :test group:

gem 'webmock', group: :test

Install webmock:

$ bundle install

Add the following to spec/spec_helper.rb’s RSpec.configure block to disable network requests during RSpec runs:

require 'webmock/rspec'
RSpec.configure do |config|
WebMock.disable_net_connect!
end

Use WebMock to stub the NotesXmlService request/response in spec/models/note_spec.rb by editing its before block to the following:

before :each do
path = 'note-one'
stub_request(:get, ”#{Rails.application.config.notes_xml_service_root}/#{path}”).to_return(
   body: [
     '<?xml version="1.0" encoding="UTF-8"?>',
     '<note type="work">',
       '<add key="to" value="Samantha"/>',
       '<add key="from" value="David"/>',
       '<add key="heading" value="Our Meeting"/>',
       '<add key="body" value="Are you available to get started at 1pm?"/>',
     '</note>'
   ].join('')
)
@note = Note.new(path)
end

Running rake spec should now run the full test suite, including the Note model spec, without performing real HTTP requests.

Similar tests can be authored for Note’s from, heading, and body methods. The complete examples are viewable in Noterizer’s master branch.

Building up the controller

Now that we have a Note model, our Notes#index controller should create an instance variable, inside of which lives an array of Note models representing the data served by each NotesXmlService endpoint.

Add the following to app/controllers/notes_controller.rb‘s index method:

def index
@notes = [
   Note.new(‘note-one’),
   Note.new(‘note-two’)
]
end

Testing the NotesController

Let’s test the modifications to NotesController.

Creating a spec helper

To test the controller, we’ll need to stub the NotesXmlService requests, just as was done in spec/models/note_spec.rb.

Rather than repeat the stub_request code, let’s abstract it into a helper that can be used throughout the specs.

Create a spec/support/helpers.rb file:

$ mkdir spec/support && touch spec/support/helpers.rb

Define the helper method by adding the following to the newly created spec/support/helpers.rb file:

module Helpers
def stub_note_request(path)
   base_url = Rails.application.config.notes_xml_service_root
   stub_request(:get, "#{base_url}/#{path}").to_return(
     body: [
       '<?xml version="1.0" encoding="UTF-8"?>',
        '<note type="work">',
         '<add key="to" value="Samantha"/>',
         '<add key="from" value="David"/>',
         '<add key="heading" value="Our Meeting"/>',
         '<add key="body" value="Are you available to get started at 1pm?"/>',
       '</note>'
     ].join('')
   )
end
end

Tweak the RSpec configuration such that it can be used by adding the following to the configure block in spec/rails_helper.rb:

config.include Helpers

Edit the spec/models/note_spec.rb‘s before block to use the #stub_note_request helper:

before :each do
path = ‘note-one’
stub_note_request(path)
@note = Note.new(path)
end

Confirm that all tests continue passing by running rake spec.

Writing the new NotesController tests

Let’s make use of the stub_note_request helper by changing spec/controllers/notes_controller_spec.rb‘s before block to the following:

before :each do
stub_note_request('note-one')
stub_note_request('note-two')
get :index
end

Add the following to its #index tests to test the new functionality:

context 'the @notes it assigns' do
it 'is an array containing 2 items' do
   expect(assigns(:notes).length).to eq 2
end
it 'is an array of Note models' do
   assigns(:notes).each do |note|
    expect(note).to be_a Note
   end
end
end

rake spec should now confirm that all tests pass. See Noterizer’s master branch for the complete example code.

The Jbuilder view

With a fully functional Note model and NotesController, Noterizer now needs a view to render the proper JSON. Much like ERB offers a Ruby HTML templating solution, Jbuilder offers a JSON templating solution.

Writing the Jbuilder view templates

Add the following to app/views/notes/index.json.jbuilder:

json.array! @notes, partial: 'note' as: :note

What does this do? This instructs Jbuilder to build an array of objects with the @notes array and render each one via a partial named _note.

Create the app/views/notes/_note.json.jbuilder partial file:

$ touch app/views/notes/_note.json.jbuilder

Add the following:

json.toField     note.to
json.fromField note.from
json.heading   note.heading
json.body         note.body

Now, http://localhost:3000/notes renders the following JSON:

[{
"toField": "Samantha",
"fromField": "David",
"heading": "Our Meeting",
"body": "Are you available to get started at 1pm?"
},{
"toField": "Melissa",
"fromField": "Chris",
"heading": "Saturday",
"body": "Are you still interested in going to the beach?"
}]

Testing the Jbuilder view templates

First, let’s test the app/views/notes/_note.json.jbuilder template. Create a spec file:

$ mkdir spec/views/notes && touch spec/views/notes/_note.json.jbuilder_spec.rb

Add the following to the newly created _note spec:

require 'spec_helper'
describe 'notes/_note' do
let(:note) do
   double('Note',
     to: 'Mike',
     from: 'Sam',
     heading: 'Tomorrow',
     body: 'Call me after 3pm.',
   )
end
before :each do
   assign(:note, note)
   render '/notes/note', note: note
end
context 'verifying the JSON values it renders' do
   subject { JSON.parse(rendered) }
   describe "['toField']" do
     subject { super()['toField'] }
     it { is_expected.to eq 'Mike' }
   end
end
end

rake spec should confirm that all tests pass. You can write additional _note tests for fromField, heading, and body.

Conclusion

You have now completed this two part blog series, and you’ve built Noterizer, a basic example of a Rails application that fronts an external API. The application preserves an MVC architecture that separates concerns across the Note model, the NotesController, and JSON view templates. Noterizer also offers a simple example of using RSpec to test such an application and Jbuilder as a JSON templating language.

Mike Ball is a Philadelphia-based software developer specializing in Ruby on Rails and JavaScript. He works for Comcast Interactive Media where he helps build web-based TV and video consumption applications.

LEAVE A REPLY

Please enter your comment!
Please enter your name here