Web application development workflow with Node.js
2011/12/27 12 Comments
Context
The aim of the article is to describe a good workflow to use when developing web applications. I always read advice on good workflow practices but they are never aggregated together, that’s why I do it right now.
UI Driven Development
We are going to follow a Behavior Driven Development style. Moreover we are making a web application, so we decide to focus on user: we will start by writing code for the UI. Then for UI and backend, we will write our specs/tests first.
Technos
As technology we are going to use :
- Node.js and its framework Expess.JS because of their popularity and to get more familiar with it.
- The language is Coffeescript for its readability.
- Backbone is the UI framework.
- Mongoose is the ODM.
- Git is the version control system.
- Jasmine is the BDD framework for UI and Vows is the BDD Framework for backend.
Use case
The application on which I will apply the workflow is Ponyo, a simple app that actually does nothing apart of allowing to create and browse “categories”. To illustrate the workflow we are going to add a new feature : allow category deletion.
Workflow
Here are all steps we need to do proper developments. Of course most of the time pressure push us to do shortcuts, but it is good to keep the good way in mind. Moreover this complete workflow could help to think about what could be optimized/automatized:
- Make a branch
- Write UI specs + commit
- Write UI specs code + commit
- Write UI code + commit
- Write backend resource specs + commit
- Write backend resource tests + commit
- Write backend resource code + commit
- Run all tests
- Test your app manually
- Rebase branch
- Merge branch + push
- Refactor if needed
Details
1. Make a branch
First of all, don’t bother other programmers right now with our commits, so let’s create a branch called feature-delete-category.
With Git
git branch feature-delete-category git checkout feature-delete-category
2. Write UI Specs
We are doing UI Driven Development, so first write client side code specs. Then, check with your browser (here the URL is http://localhost:3000/tests/) that the Jasmine entries fail.
With Jasmine
describe 'Category deletion', ->
it 'When I display newly created category', ->
expect(false).toBeTruthy()
it 'And I click on delete category button from a category page', ->
expect(false).toBeTruthy()
it 'Then it brings me back to category list', ->
expect(false).toBeTruthy()
it 'And deleted activity is no more in the list', ->
expect(false).toBeTruthy()
With Git, commit
git commit tests/categories.coffee -m "Add specs for category deletion."
3. Write UI specs code
Now that you know what you want to do, you can write corresponding tests and checks that they fail.
With Jasmine
describe 'Category deletion', ->
it 'When I display newly created category', ->
runs ->
$("#category-jasmine").click()
waits(500) # Waits to be sure that everything is done before testing
it 'And I click on delete category button from a category page', ->
runs ->
$("#delete-category-button").click()
waits(500) # Waits to be sure that everything is done before testing
it 'Then it brings me back to category list', ->
runs ->
expect($("#category-list").length).not.toEqual 0
it 'And deleted activity is no more in the list', ->
runs ->
expect($("#category-jamsine").length).toEqual 0
With Git, commit
git commit tests/categories.coffee -m "Add tests for category deletion."
4. Write UI code
Now we are going to write the UI code, it is needed to know what we expect from server. We add a button to the template displaying a category, then we code the button behavior. After that we check that our tests still fail (backend does not support request for deletion). Finally we commit.
Modify the template with Eco
<p>
<a id="delete-category-button" href="#home">
Delete category<br />
</a>
</p>
Write behavior with Backbone
categoryViewTemplate = require('../templates/category_view')
Category = require('../models/category').Category
class exports.CategoryView extends Backbone.View
id: 'category-view'
constructor: ->
super()
render: (category) ->
$("#nav-content").html null
$.get "/categories/#{category}/", (data) =>
$("#nav-content").html categoryViewTemplate(category: data)
@model = new Category data
@deleteButton = $("#delete-category-button")
@deleteButton.click(@onDeleteButtonClicked)
onDeleteButtonClicked : (event) =>
event.preventDefault()
@model.destroy
success: ->
app.routers.main.navigate("home", true)
error: ->
alert "An error occured, category was probably not deleted."
app.routers.main.navigate("home", true)
With Git, commit
git commit public/ -m "Add deletion button to UI"
5. Write backend resources specs
Now we know that we need a resource to delete category, so let’s write our category deletion resource specs and commit.
With Vows
.addBatch
'DELETE /categories/category-02/':
topic: () ->
apiTest.del 'categories/category-02/', @callback
'response should be with a 200 OK': (error, response, body) ->
assert.ok false
'GET /categories/category-02/':
topic: () ->
apiTest.get 'categories/category-02/', @callback
'response should be with a 404 Not Found': (error, response, body) ->
assert.ok false
'DELETE /categories/category-02/':
topic: () ->
apiTest.del 'categories/category-02/', @callback
'response should be with a 404 Not Found': (error, response, body) ->
assert.ok false
With Git
git commit test/ -m "Add backend resources specs"
6. Write backend resource tests
Now we write our test code, we just check that returned HTTP code are expected ones and that once category is deleted, it cannot be reached anymore. We commit.
With Vows
.addBatch
'DELETE /categories/category-02/':
topic: () ->
apiTest.del 'categories/category-02/', @callback
'response should be with a 200 OK': assertStatus 200
'GET /categories/category-02/':
topic: () ->
apiTest.get 'categories/category-02/', @callback
'response should be with a 404 Not Found': assertStatus 404
'DELETE /categories/category-02/':
topic: () ->
apiTest.del 'categories/category-02/', @callback
'response should be with a 404 Not Found': assertStatus 404
With Git
git commit test/ -m "Add backend resources specs code"
7. Write backend resource code
Now we write code : we add a new route that will link to a new resource dedicated to category deletion. We commit.
With Express
app.del "/categories/:category/", routers.deleteCategory
With Express and Mongoose
exports.deleteCategory = (req, res) ->
categoryProvider = new CategoryProvider
categoryProvider.getCategory req.params.category, (err, docs) ->
if err
console.error(err.stack)
res.json 'An error occured', 500
else if docs.length > 0
docs[0].remove (err) ->
if err
console.error(err.stack)
res.json 'An error occured', 500
else
return res.json success: true
else
res.json 'I dont have that', 404
With Git
git commit test/ -m "Add category deletion resource"
8. Run all tests
We run our backend tests and our UI tests through browser and we are glad to see they all work.
With Vows
vows --spec test/resources.coffee
9. Test your app manually
Once you launch all your tests, test your application as a normal user. BDD is great but it will never replace a manual test, we often miss something that is not revealed by our tests.
10. Rebase branch
We want to add our commits like we did them from last version of master branch, so we use rebase command.
With Git
git rebase master
11. Merge branch
Then we can merge our features to the master trunk. A push to master branch will validate that work is done !
With Git
git checkout master git merge feature-delete-category git push git branch -d feature-delete-category
12. Refactor if needed
UI Driven development has the nice advantage to not let you develop unuseful resources but it does not let you think as good as possible the way to develop your backend. So you will probably need some refactoring. Fortunately, with your tests refactoring will be easier and safer. Moreover patterns you see when you develop UI first push you to think about refactoring that match better to your needs.
NB: Feel free to comment and criticize this article so I could improve it and correct what is wrong.























