Subsecciones

Pruebas

Mocha, Chai y Expect: instalando las dependencias

[~/src/coffee/coffeepress(master)]$ npm install --save-dev mocha expect chai

[~/src/coffee/coffeepress(master)]$ cat package.json 
{
  "name": "coffeepress",
  "version": "0.0.0",
  "private": true,
  "scripts": {
    "start": "node ./bin/www"
  },
  "dependencies": {
    "body-parser": "~1.10.2",
    "cookie-parser": "~1.3.3",
    "debug": "~2.1.1",
    "express": "~4.11.1",
    "jade": "~1.9.1",
    "morgan": "~1.5.1",
    "serve-favicon": "~2.2.0"
  },
  "devDependencies": {
    "expect": "~1.6.0",
    "chai": "~2.2.0",
    "mocha": "~2.2.1"
  }
}

Una Prueba Sencilla

Empecemos con una prueba sencilla. Creamos el directorio test para las pruebas:

[~/src/coffee/coffeepress(master)]$ mkdir test
y creamos una primera prueba:
[~/src/coffee/coffeepress(master)]$ cat test/example.coffee
chai     = require 'chai'
expect   = chai.expect

describe "feature", ->
  it "should add two numbers", ->
    expect(2+2).to.equal(4)
para ejecutarla debemos hacer:
$ mocha --compilers coffee:coffee-script/register -R spec


  feature
    ok: should add two numbers 


  1 passing (7ms)

Vamos ahora a hacer una prueba sobre nuestro código. Nuestro objetivo es hacer que nuestra aplicación funcione como un pequeño blog.

¿Cómo lo vamos a hacer?

Reescribiremos routes/index.coffee de manera que por cada ruta URL de la aplicación el objeto routes exportado por routes/index.coffee disponga de un método que procesa esa ruta. Así:

Esta correspondencia entre métodos HHTP+rutas y métodos del objeto routes se establece en el cuerpo principal de la aplicación:
[~/javascript/expressjs/coffee-mongo(master)]$ sed -ne '3p' app.coffee 
routes   = require "./routes"
[~/javascript/expressjs/coffee-mongo(master)]$ sed -ne '24,27p' app.coffee 
app.get "/", routes.index
app.get "/post/new", routes.newPost
app.post "/post/new", routes.addPost
app.get "/post/:id", routes.viewPost

Should display index with posts: red

Empezaremos describiendo como debe ser la ruta index del objeto routes.

[~/src/coffee/coffeepress(master)]$ cat test/routes-test.coffee 
chai     = require 'chai'
expect   = chai.expect
routes = require "../routes/index"

describe "routes", ->
  req = {}
  res = {}
  describe "index", ->
    it "should display index with posts", ->
      res.render = (view, vars) ->
          expect(view).equal "index"
          expect(vars.title).equal "My Coffeepress Blog"
      routes.index(req, res)

La ruta index que fué generada por Express contiene el siguiente código:
[~/src/coffee/coffeepress(master)]$ cat routes/index.coffee 
express = require('express')
router = express.Router()

### GET home page. ###

router.get '/', (req, res, next) ->
  res.render 'index', title: 'Express'
  return
module.exports = router
como se ve, lo que estamos haciendo en la prueba es sobreescribir el método render del objeto res por un falso render que hace las dos pruebas:
routes = require "../routes/index"
...
      res.render = (view, vars) ->
          expect(view).equal "index"
          expect(vars.title).equal "My Coffeepress Blog"
      routes.index(req, res)

Run the tests:

[~/src/coffee/coffeepress(master)]$ mocha --compilers coffee:coffee-script/register -R spec


  routes
    index
      1) should display index with posts


  0 passing (7ms)
  1 failing

  1) routes index should display index with posts:
     TypeError: Object function router(req, res, next) {
    router.handle(req, res, next);
  } has no method 'index'
    at Context.<anonymous> (/Users/casiano/local/src/coffee/coffeepress/test/routes-test.coffee:13:14)
    ...

Should display index with posts: green

Se queja de que routes = require "../routes/index" no tiene un método index. Se lo añadimos:
[~/src/coffee/coffeepress(master)]$ cat routes/index.coffee 
express = require('express')

module.exports = 
  index: (req, res) ->
    res.render 'index', { title: 'My Coffeepress Blog' }

Y ahora las pruebas pasan:

[~/src/coffee/coffeepress(master)]$ mocha --compilers coffee:coffee-script/register -R spec

  routes
    index
      ok: should display index with posts 

  1 passing (6ms)

Expect vars posts equal empty array: red

Now let’s add a post variable that will be an array of posts we’ll display on the front page. Add the following assertion
          expect(vars.posts).eql []

right after the title assertion:

[~/src/coffee/coffeepress(master)]$ cat test/routes-test.coffee 
chai     = require 'chai'
expect   = chai.expect
routes = require "../routes/index"

describe "routes", ->
  req = {}
  res = {}
  describe "index", ->
    it "should display index with posts", ->
      res.render = (view, vars) ->
          expect(view).equal "index"
          expect(vars.title).equal "My Coffeepress Blog"
          expect(vars.posts).deep.equal []
      routes.index(req, res)
Obsérvese que hemos usado el método deep. También podríamos haber usado eql en vez de .deep.equal.

Now the tests fail:

[~/src/coffee/coffeepress(master)]$ mocha --compilers coffee:coffee-script/register -R spec

  routes
    index
      1) should display index with posts


  0 passing (12ms)
  1 failing

  1) routes index should display index with posts:
     AssertionError: expected undefined to deeply equal []

Expect vars posts equal empty array: green

Let us change the route to have a posts array variable available in the template.
[~/src/coffee/coffeepress(master)]$ cat routes/index.coffee 
express = require('express')

module.exports = 
  index: (req, res) ->
    res.render 'index', 
      title: 'My Coffeepress Blog' 
      posts: []
and now the tests pass:
[~/src/coffee/coffeepress(master)]$ mocha --compilers coffee:coffee-script/register -R spec

  routes
    index
      ok: should display index with posts 

  1 passing (12ms)

views/index.jade

Completemos la vista index:

[~/src/coffee/coffeepress(master)]$ cat views/index.jade 
extends layout
block content
  - each post in posts
    .page-header
      li
        a(href="/post/#{post.id}")= post.title
        .content!=post.body

views/layout.jade

Necesitamos un layout:

~/src/coffee/coffeepress(master)]$ cat views/layout.jade 
doctype html
html
  head
    title= title
    //link(rel="stylesheet", href="http://maxcdn.bootstrapcdn.com/bootstrap/3.2.0/css/bootstrap.min.css")
    //script(src="https://ajax.googleapis.com/ajax/libs/jquery/1.11.1/jquery.min.js")
    //script(src="http://maxcdn.bootstrapcdn.com/bootstrap/3.2.0/js/bootstrap.min.js")
    style(type="text/css")
      #content { padding-top: 60px;}
  body
    .topbar-wrapper
      .topbar
        .topbar-inner
          .container
            h3: a(href="/") My Coffeepress Blog
            ul.nav
              li: a(href="/post/new") New Post
              #content.container
                block content

Should display the add post page: red

Next let’s write tests for the new post route.

[~/src/coffee/coffeepress(master)]$ cat -n test/routes-test.coffee 
 1  chai     = require 'chai'
 2  expect   = chai.expect
 3  routes = require "../routes/index"
 4  
 5  describe "routes", ->
 6    req = {}
 7    res = {}
 8    describe "index", ->
 9      it "should display index with posts", ->
..        ...
15  
16    describe "new post", ->
17      it "should display the add post page", ->
18        res.render = (view, vars) ->
19            expect(view).equal "add_post"
20            expect(vars.title).equal "Write New Post"
21      
22        routes.newPost(req, res)
El test falla porque routes carece de un método newPost:
[~/src/coffee/coffeepress(master)]$ mocha --compilers coffee:coffee-script/register -R spec test/routes-test.coffee 


  routes
    index
      ok: should display index with posts 
    new post
      1) should display the add post page


  1 passing (17ms)
  1 failing

  1) routes new post should display the add post page:
     TypeError: Object #<Object> has no method 'newPost'

Should display the add post page: (false) green

Añadamos un método newPost al código de la ruta:
[~/src/coffee/coffeepress(master)]$ cat -n routes/index.coffee 
     1  express = require('express')
     2  
     3  module.exports = 
     4    index: (req, res) ->
     5      res.render 'index', 
     6        title: 'My Coffeepress Blog' 
     7        posts: []
     8  
     9    newPost: (req, res) ->
    10      # do nothing
¡Cuando ejecutamos las pruebas pasan!:
[~/src/coffee/coffeepress(master)]$ mocha --compilers coffee:coffee-script/register -R spec test/routes-test.coffee 


  routes
    index
      ok: should display index with posts 
    new post
      ok: should display the add post page 

  2 passing (8ms)

Mocha: done

That’s not good.

Esto es así porque res.render nunca es llamado ya que el cuerpo de newPost es vacío.

18        res.render = (view, vars) ->
19            expect(view).equal "add_post"
20            expect(vars.title).equal "Write New Post"
21      
22        routes.newPost(req, res)

How can we make absolutely sure the test gets called during our test run?

With this in mind, let’s modify our tests with the following:
[~/src/coffee/coffeepress(master)]$ cat test/routes-test.coffee 
chai     = require 'chai'
expect   = chai.expect
routes = require "../routes/index"

describe "routes", ->
  req = {}
  res = {}
  describe "index", ->
    it "should display index with posts", ->
      ...

  describe "new post", ->
    it "should display the add post page", (done)->
      res.render = (view, vars) ->
          expect(view).equal "add_post"
          expect(vars.title).equal "Write New Post"
          done()
    
      routes.newPost(req, res)
Now it fails since the done() callback is never called:
[~/src/coffee/coffeepress(master)]$ mocha --compilers coffee:coffee-script/register -R spec test/routes-test.coffee 


  routes
    index
      ok: should display index with posts 
    new post
      1) should display the add post page


  1 passing (2s)
  1 failing

  1) routes new post should display the add post page:
     Error: timeout of 2000ms exceeded
    at [object Object].<anonymous> (/usr/loc

Escribiendo newPost

Let’s go ahead and implement the route and connect it into our router.

~/src/coffee/coffeepress(master)]$ cat routes/index.coffee 
express = require('express')

module.exports = 
  index: (req, res) ->
    ...

  newPost: (req, res) ->
    res.render 'add_post', title:"Write New Post"
And connecting it up in the app.coffee:
[~/src/coffee/coffeepress(master)]$ sed -ne '20,21p' app.coffee 
app.get '/', routes.index
app.get '/post/new', routes.newPost

views/add_post.jade

Ademas tenemos que añadir la vista add_post:

[~/src/coffee/coffeepress(master)]$ cat views/add_post.jade 
extends layout
block content
  form(method="post", action="/post/new")
    fieldset
      legend=title
      .clearfix
        label(for="title") Title
        .input
          input.xlarge#title(type="text", name="title")
      .clearfix
        label(for="post") Post
        .input
          textarea#post.xxlarge(name="body",rows="3")
      .actions
        input.btn.primary(type="submit", value="Publish!")
        |   
        a.btn(href="/") Cancel

Should display the add post page: (true) green

Ahora podemos ejecutar las pruebas:
[~/src/coffee/coffeepress(master)]$ mocha --compilers coffee:coffee-script/register -R spec test/routes-test.coffee 


  routes
    index
      ok: should display index with posts 
    new post
      ok: should display the add post page 


  2 passing (9ms)

Gulpfile

Vamos a crear un gulpfile para ejecutar las tareas mas comunes (véase el post A gulp of coffee: your gulpfile in coffeescript):

[~/src/coffee/coffeepress(master)]$ cat gulpfile.coffee 
gulp = require('gulp')
shell = require('gulp-shell')

# run coffee server via nodemon https://github.com/remy/nodemon
gulp.task 'default', ->
  gulp.src('').pipe shell([ 'DEBUG=coffeepress:* nodemon bin/www.coffee' ])

# run coffee server
gulp.task 'cofserver', ->
  gulp.src('').pipe shell([ 'coffee app.coffee' ])

gulp.task 'test', [ 'mocha' ]
gulp.task 'mocha', ->
  gulp.src('').pipe shell(['mocha --compilers coffee:coffee-script/register -R spec'])

# run mongod server
gulp.task 'mongod', ->
  gulp.src('').pipe shell([ 'mongod' ])

Este es el correspondiente código de la version javascript:

[~/src/coffee/coffeepress(master)]$ cat gulpfile.js
// Note the new way of requesting CoffeeScript since 1.7.x
require('coffee-script/register');
// This bootstraps your Gulp's main file
require('./gulpfile.coffee');
Es necesario instalar gulp-shell:
[~/src/coffee/coffeepress(master)]$ npm install --save-dev gulp-shell
ahora podemos ejecutar las pruebas con gulp test.

Guardando el Post

Now let’s add another route to handle the post. For now, we’re just going to store each post in an array.
[~/src/coffee/coffeepress(master)]$ cat routes/index.coffee 
express = require('express')
debug = require('debug')('coffeepress:server')

posts = [ {id:0, title: "hi", body: "Hola todo el mundo"}, {id:1, title: "world", body: "I like <b>coffee</b>"}] 

module.exports = 
  index: (req, res) ->
    debug "en index"
    res.render 'index', 
          title: 'My Coffee Blog' 
          posts: posts

  newPost: (req, res) ->
    debug "en newPost"
    res.render 'add_post', title:"Write New Post"

  addPost: (req, res) ->
    debug "en addPost"
    debug req.body
    post = req.body
    # [post.title, post.body ] = [req.body.title, req.body.body]
    post.id = posts.length
    posts.push post
    res.redirect "/"

We also add a new route to app.coffee

~/src/coffee/coffeepress(master)]$ sed -ne '20,22p' app.coffee 
app.get  "/"        , routes.index
app.get  "/post/new", routes.newPost
app.post "/post/new", routes.addPost

Viewing a Single Post

Añadimos un método newPost y lo montamos en /post/:id:

[~/src/coffee/coffeepress(master)]$ sed -ne '20,23p' app.coffee 
app.get  "/"        , routes.index
app.get  "/post/new", routes.newPost
app.post "/post/new", routes.addPost
app.get  "/post/:id", routes.viewPost
Este es el código del manejador de la ruta /post/:id:
[~/src/coffee/coffeepress(master)]$ cat routes/index.coffee 
express = require('express')
debug = require('debug')('coffeepress:server')

#posts = []

module.exports = 
  index: (req, res) ->
    ...

  newPost: (req, res) ->
    res.render 'add_post', title:"Write New Post"

  addPost: (req, res) ->
    ...

   viewPost: (req, res) ->
     post = posts[req.params.id]
     res.render 'post', post: post, title: post.title, id: req.params.id
Y esta es la vista views/post.jade:
[~/src/coffee/coffeepress(master)]$ cat views/post.jade 
extends layout
block content
  h1 Post #{ id }
    .page-header
      h1= post.title
    .content!=post.body
Ahora cuando visitamos una ruta como /post/2 nos mostrará el correspondiente post.

Casiano Rodríguez León
2016-03-27