[~/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" } }
test
para las pruebas:
[~/src/coffee/coffeepress(master)]$ mkdir testy 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)
mocha
tiene una opción:
--compilers <ext>:<module>,...Que indica que debemos usar el
module
(s) para compilar los ficheros
con extensión ext
.
--require
option to activate your registration script.
-R
o bien --reporter
va seguida de un <name>
y especifica el
reporter a utilizar.
-R
may also be used to utilize third-party reporters. For example if you
npm install mocha-lcov-reporter
you may then do --reporter mocha-lcov-reporter
.
--reporters
nos da la lista de reporters
que
podemos utilizar:
[~/javascript/expressjs/clase-express-coffee]$ mocha --reporters dot - dot matrix doc - html documentation spec - hierarchical spec list json - single json object progress - progress bar list - spec-style listing tap - test-anything-protocol landing - unicode landing strip xunit - xunit reporter html-cov - HTML test coverage json-cov - JSON test coverage min - minimal reporter (great with --watch) json-stream - newline delimited json events markdown - markdown documentation (github flavour) nyan - nyan cat!
Vamos ahora a hacer una prueba sobre nuestro código. Nuestro objetivo es hacer que nuestra aplicación funcione como un pequeño blog.
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í:
get
a la ruta /
será procesada por el método index
get
a la ruta /post/new
será procesada por un método newPost
,
post
a la ruta /post/new
será procesada por un método addPost
,
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
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)
index
and that the variable title
is equal to what we expect to be passed in.
res.render(view [, locals] [, callback])
which renders the file whose name is given by view
and sends the rendered HTML string to the client.
Optional parameters of res.render
:
locals
, an object whose properties define local variables for the view.
callback
, a callback function.
If provided, the method returns both the possible error and rendered string, but does not perform an automated response.
When an error occurs, the method invokes next(err)
internally.
// if a callback is specified, the rendered HTML string has to be sent explicitly res.render('index', function(err, html) { res.send(html); });
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 = routercomo 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) ...
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).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 []
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)
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
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
block
and extends
keywords.
... #content.container block content
block
is simply a
block of Jade
that may be replaced within a child
template, this process is recursive.
extends
keyword allows a template to extend a layout or parent template.
It can then override certain pre-defined blocks of content.
layout
, we simply create
the index.jade
file and use the extends
directive, giving the path
(with or without the .jade
extension).
block
declaration:
block content
[~/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'
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)
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?
done
.
done
to the callback of it()
Mocha will know that it should wait for completion.
done
is called or until some time limit is exceeded
done
is a callback that we can call anywhere to indicate the test is
done.
[~/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
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
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
<fieldset>
tag is used to group related elements in a form.
input.xlarge#title(type="text", name="title") ... textarea#post.xxlarge(name="body",rows="3")using
req.body
. It
Contains key-value pairs (with keys title
, body
)
of data submitted in the request body.
By default, req.body
is undefined
,
and is populated when you use body-parsing middleware such as
body-parser
.
[~/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)
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-shellahora podemos ejecutar las pruebas con
gulp test
.
[~/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 "/"
req.body
·
req.body
is undefined
,
and is populated when you use body-parsing middleware such as
body-parser
.
input.xlarge#title(type="text", name="title") ... textarea#post.xxlarge(name="body",rows="3")using
req.body
. It
Contains key-value pairs (with keys title
, body
)
of data submitted in the request body.
post = req.bodyor
[post.title, post.body] = [req.body.title, req.body.body]
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
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.viewPostEste 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
req.params
is an object containing properties mapped to the
named route parameters.
/user/:name
, then the name
property is available as
req.params.name
req.params
object defaults to {}
// GET /user/tj req.params.name // => "tj"
req.params[n]
, where n
is the nth capture group.
For example, the following:
router.get(/^\/commits\/(\w+)(?:\.\.(\w+))?$/, function(req, res){ var from = req.params[0]; var to = req.params[1] || 'HEAD'; res.send('commit range ' + from + '..' + to); });would match
GET /commits/71dbb9c
as well as GET /commits/71dbb9c..4c084f9
/file/*
:
// GET /file/javascripts/jquery.js req.params[0] // => "javascripts/jquery.js"
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.bodyAhora cuando visitamos una ruta como
/post/2
nos mostrará el correspondiente post.
Casiano Rodríguez León