
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
We perform the migration with rake db:migrate.
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"}}
<a class="button" {{action 'save'}}>OK</a>
<a class="button" {{action 'cancel'}}>Cancel</a>
{{else}}
<h1>{{document.title}}</h1>
<a class="button" {{action 'edit'}}>edit</div>
{{/if}}
{{#if isRetitled}}
<div class="retitled-line">Retitled from '{{document.originalTitle}}'</div>
{{/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?
