Web application development workflow with Node.js

2011/12/27

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.