Article summary
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:
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. 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}}
{{/if}} {{#if isRetitled}}
{{/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:
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?