A Thin Slice of the Full Stack

I’m currently working on a full-stack web project. A recent task struck me as very deep and very narrow (i.e., a small amount of work in each layer), and I figured it’d serve as a good tour of our workflow. In this post I’ll walk through the major steps that go into development of a new feature.

The Task

First we visit Pivotal Tracker, where our Product Owner has prioritized the backlog and defined concrete, sprintable stories. We read through the story, understand its scope, and review the wireframes provided by the designers.

Our web app presents documents, and these documents have titles. The task is to make documents renameable. Once renamed, the original title should be presented in a subtitle. To illustrate this, I’ll spoil the ending and show you the working feature:

renaming_title_demo

We discuss the technical design of the feature including database changes, API endpoints, and UI interactions. This one is pretty straightforward: we’ll separately store the original title for each document.

Getting Started

Our source code is split into three repositories—one for an Ember app, one for a Rails JSON API, and one for a suite of full-stack integration tests.

All major development occurs in feature branches (as in the git flow model), so we start by branching each of the repositories with a command like git checkout -b feature/edit-document-title.

Each push to GitHub will be built and tested by CircleCI and deployed to one of several development servers on [Heroku][heroku]. This is modeled after Local Orbit’s continuous delivery.

We claim an available server, noting it on a widely-visible whiteboard and specifying that our feature branch will deploy to it in a CircleCI config file.

A Test to Drive Us

When possible we like to start with full-stack integration tests. This spells out our expectations for the feature, and continually points to the next thing to work on. Here are a couple of tests for our feature (using Capybara and rspec):

it 'shows original title when retitled' do
  document = Document.create(
    title: 'New Title',
    original_title: 'Original Title'
  )
  
  visit "/docs/#{document.id}"
  
  expect(page).to have_content("Retitled from 'Original Title'")
  expect(page).to have_content('New Title')
end

it 'can be retitled' do
  document = Document.create(
    title: 'Original Title',
    original_title: 'Original Title',

  visit "/docs/#{document.id}"
  
  expect(page).to have_content('Original Title')

  first('.document-header .edit').click

  fill_in 'document-title-input', :with => 'New Title'

  first('.rename-buttons .save-button').click

  expect(page).to have_content('New Title')
  expect(page).to have_content("Retitled from 'Original Title'")

  # still there after a reload:
  visit "/docs/#{document.id}"

  expect(page).to have_content('New Title')
  expect(page).to have_content("Retitled from 'Original Title'")
  
  first('.document-header .edit').click

  fill_in 'document-title-input', :with => 'Original Title'

  first('.rename-buttons .save-button').click

  expect(page).not_to have_content('New Title')
  expect(page).to have_content('Original Title')
end

We have tooling to run integration tests against specific web client and API repositories, but for CI we reference them as git submodules. We point the submodules to our feature branches:

git config -f .gitmodules submodule.api.branch feature/edit-document-title
git config -f .gitmodules submodule.web-client.branch feature/edit-document-title

It’s necessary to periodically update these with git submodule update --remote.

Building

From here, depending on how the tests are currently failing, we tend to bounce back and forth between repositories, slowly building up the feature from different sides. Rather than following that meandering narrative, in the interest of brevity I’ll summarize the work that goes in to each area.

Database

We’re editing a table, which calls for a schema migration. Because we’re just adding a column, this can be achieved with a single Active Record generator command:

rails g migration AddOriginalTitleToDocuments original_title:string

With the database updated, we move on to Rails.

API

We’ll start by updating the existing serializer to serve the new field:

class DocumentSerializer < ActiveModel::Serializer
-  attributes :id, :title, :created_at
+  attributes :id, :title, :original_title, :created_at

Next we add a new [route] to accept PUTs to the document:

  get 'documents/:id' => 'documents#show', as: :documents, constraints: {id: /\d+/}
+  put 'documents/:id' => 'documents#update', as: :document_update, constraints: {id: /\d+/}

Then it’s on to the controller. We update the existing #show test to expect the new original_title field to be served:

  document_json = JSON.parse(response.body)['document']
  expect(document_json['title']).to eq('example document')
+ expect(document_json['original_title']).to eq('example document')

Then we create a test for the new #update method:

describe '#update' do
    it 'accepts updates to document title' do
      put :update,
        id: @document.id,
        document: {
          original_title: 'the title',
          title: 'new title',
        }

      @document.reload

      expect(@document.original_title).to eq("the title")
      expect(@document.title).to eq("new title")

    end
  end

Finally, the controller implementation:

 class DocumentsController < ApplicationController
   def show
     document = Document.find_by!(id: params[:id])
     render json: document
   end

+  def update
+    permitted = params.permit(document: [:original_title, :title])
+    document = Document.find_by!(id: params[:id].to_i)
+    document.update_attributes(permitted[:document])
+
+    render json: document,
+      root: 'document'
+  end
 end

API tests pass! Time to move on to the UI.

Web Client

We write a handful of Ember acceptance tests to exercise UI flows around these actions and to cover edge cases like whitespace handling. Here’s one of them:

test 'custom title is used and subtitle is shown if the document has been renamed', ->
  # this shared default fixture contains varying title and original_title :
  stub 'documents/1', fixtures.defaultDocumentResponse

  visit '/documents/1'
 
  andThen ->
    expectTitleAndSubtitle('Custom Title', 'Original Title')

Ember.Test.registerHelper 'expectTitleAndSubtitle', (app, title, subtitle) ->
  equal find('h1').text().trim(), title
  if(subtitle && subtitle != '')
    equal find('.retitled-line').length, 1, 'looking for subtitle'
    subtitle_line = "Retitled from '"+subtitle+"' document"
    equal find('.retitled-line').text(), subtitle_line, 'looking for subtitle text'
  else
    equal find('.retitled-line').length, 0, 'looking for absence of subtitle'

Moving on to the implementation, first we update the Ember model to match our API change:

 `import DS from 'ember-data'`

 Document = DS.Model.extend
   title: DS.attr()
+  originalTitle: DS.attr()
   ...

 `export default Document`

Now it’s time to implement the UI. With the complexity we’re introducing, the document header now merits its own module. Ember-cli generates boilerplate for us:

] ember g component document-header
version: 0.1.7
installing
  create app/components/document-header.coffee
  create app/templates/components/document-header.hbs
installing
  create tests/unit/components/document-header-test.coffee

Here’s the implemented template—note the editMode and isRetitled conditionals, and the save/cancel/edit actions:

{{#if editMode}}
  {{input value=title name="title"}}
  OK
  Cancel
{{else}}

{{document.title}}

edit

{{/if}} {{#if isRetitled}}

Retitled from ‘{{document.originalTitle}}’

{{/if}}

Here’s the component that controls it:

`import Ember from 'ember'`

DocumentHeaderComponent = Ember.Component.extend
  editMode: false

  actions:
    save: ->
      stored_title = @get('document.title')
      entered_title = @get('title').trim()
      @set('title', entered_title)
      if entered_title != '' && entered_title != stored_title
        @set('document.title', entered_title)
        @get('document').save()
      @set('editMode', false)
    edit: ->
      @set('editMode', true)
    cancel: ->
      @set('editMode', false)
      stored_title = @get('document.title')
      @set('title', stored_title)

  title: Ember.computed.oneWay('document.title')

  isRetitled: (->
    originalTitle = @get('document.originalTitle')
    originalTitle?.length > 0 && originalTitle != @get('title').trim()
  ).property('title', 'document.originalTitle')

`export default DocumentHeaderComponent`

Lastly, now that it’s working, we spend some time styling the interface to match the provided design. SCSS helps us keep our stylesheets organized.

Putting it All Together

Our integration tests from the start of the story are now passing on CI, so it’s time to wrap up the feature:

  • We merge in the latest changes from master into our feature branches, resolve any conflicts, and generate pull requests on GitHub.
  • We manually test the deployed feature on the development server. Satisfied that it’s completed, we finish the feature on Pivotal.
  • If the feature merits code review, we select a reviewer and send them links to the pull requests. After review, we deliver the feature on Pivotal and provide a link to the development server for the product owner to test it.
  • We make any changes the product owner requests. When she accepts the story, we merge the pull requests on to master. This gets deployed to the customer-visible staging server.
  • We release the claim on the development server by erasing our branch name from the whiteboard.

Conclusion

Once more, here’s the demo:

renaming_title_demo

My favorite part is how the subtitle appears and disappears live as the user edits text—this is the magic of Ember’s computed properties.

How does this workflow compare with yours?