In which we go from having nothing to having feature tests and knowing what our project will look like. Make sure you're on board with where we're going after reading the overview, then pull up a shell and start reading code!
Before I even got to the first commit, I chose a couple basic approaches to getting a task runner working. My shell session went something like:
$ grunt grunt-cli: The grunt command line interface. (v0.1.9) Fatal error: Unable to find local grunt. If you're seeing this message, either a Gruntfile wasn't found or grunt hasn't been installed locally to your project. For more nformation about installing and configuring grunt, please see the Getting Started guide: http://gruntjs.com/getting-started $ npm i --save-dev grunt # Install output $ grunt A valid Gruntfile could not be found. Please see the getting started guide for more nformation on how to configure grunt: http://gruntjs.com/getting-started Fatal error: Unable to find Gruntfile. $ touch Gruntfile.coffee $ grunt Warning: Task "default" not found. Use --force to continue. Aborted due to warnings. $ vi Gruntfile.coffee $ grunt Warning: Task "test" not found. Use --force to continue. Aborted due to warnings. $ vi Gruntfile.coffee $ grunt Warning: Task "cucumberjs:Edith" not found. Use --force to continue. Aborted due to warnings.
and so on, until I had about the Gruntfile you see here.
.gitignore | 1 + Gruntfile.coffee | 14 ++++++++++++++ package.json | 17 +++++++++++++++++ test/behavior/steps/edith.coffee | 16 ++++++++++++++++ test/behavior/support/world.coffee | 14 ++++++++++++++ test/behavior/users/edith/Edith.feature | 9 +++++++++ 6 files changed, 71 insertions(+)
The first commit starts with a minimal feature to start our todo app, and just enough grunt to run some feature specs.
diff --git a/Gruntfile.coffee b/Gruntfile.coffee new file mode 100644 @@ -0,0 +1,14 @@ + module.exports = (grunt)-> + grunt.initConfig + cucumberjs: + Edith: + files: src: ['test/behavior/users/edith'] + options: + steps: 'test/behavior/steps' + + grunt.npmTasks = [ "grunt-cucumber" ] + + grunt.loadNpmTasks npmTask for npmTask in grunt.npmTasks + + grunt.registerTask "test", [ "cucumberjs:Edith" ] + grunt.registerTask "default", ["test"]
diff --git a/test/behavior/users/edith/Edith.feature b/test/behavior/users/edith/Edith.feature new file mode 100644 @@ -0,0 +1,9 @@ + Feature: Edith + As a user of our site + Edith wants to see the site + So that she knows it exists + + Scenario: Direct Browsing + Given Edith has her browser open + When Edith goes to the url directly + Then she should see "Angular JS" in the title \ No newline at end of file
This is an incredibly primitive first feature, more a hello world than an app, but it does give us experience with our first two technologies: Grunt JS for running tasks and CucumberJS for running cucumber specs in the NodeJS environment.
When we run the grunt file at this point, we get an expected error:
Running "cucumberjs:Edith" (cucumberjs) task .connect ECONNREFUSED Error: connect ECONNREFUSED at errnoException (net.js:901:11) at Object.afterConnect [as oncomplete] (net.js:892:19) F- (::) failed steps (::) Error: connect ECONNREFUSED at errnoException (net.js:901:11) at Object.afterConnect [as oncomplete] (net.js:892:19) Failing scenarios: /home/southerd/devel/southerd/tdd/angular/tdd-angular/test/behavior/users/edith/Edith.feature:6 # Scenario: Direct Browsing 1 scenario (1 failed) 3 steps (1 failed, 1 skipped, 1 passed)
Moving on, the most basic server that still returns HTML.
Gruntfile.coffee | 16 ++++++++++++++--
package.json | 7 ++++++-
server/server.coffee | 9 +++++++++
server/test/serves.coffee | 34 ++++++++++++++++++++++++++++++++++
4 files changed, 63 insertions(+), 3 deletions(-)
diff --git a/Gruntfile.coffee b/Gruntfile.coffee @@ -1,14 +1,26 @@ module.exports = (grunt)-> grunt.initConfig + mochaTest: + server: + options: + reporter: 'spec' + src: ["server/test/*coffee"] + cucumberjs: Edith: files: src: ['test/behavior/users/edith'] options: steps: 'test/behavior/steps' - grunt.npmTasks = [ "grunt-cucumber" ] + grunt.npmTasks = [ + "grunt-cucumber" + "grunt-mocha-test" + ] grunt.loadNpmTasks npmTask for npmTask in grunt.npmTasks - grunt.registerTask "test", [ "cucumberjs:Edith" ] + grunt.registerTask "test", [ + "mochaTest:server" + "cucumberjs:Edith" + ] grunt.registerTask "default", ["test"]
diff --git a/server/test/serves.coffee b/server/test/serves.coffee new file mode 100644 @@ -0,0 +1,34 @@ + should = require "should" + server = require "../server" + request = require "request" + describe "Server", -> + index = (d, cb)-> + request "http://localhost:3000/", (e, r, b)-> + cb e, r, b + d() + + before -> + server.serve() + + it "binds on a known port", (done)-> + index done, (err, response)-> + should.not.exist err, "Error when GETting (#{err})" + + it "returns 200 when requesting /", (done)-> + index done, (e, res)-> + res.statusCode.should.equal 200 + + it "returns an index page at /", (done)-> + index done, (e, r, body)-> + body.should.match /// + ^<html>.* + </html>$ + ///, + "page needs basic HTML structure." + + it "returns a page with a title", (done)-> + index done, (e, r, body)-> + body.should.match /// + <title>[^<]*Angular\sJS[^<]*</title> + ///, + "page needs a title"
diff --git a/server/server.coffee b/server/server.coffee new file mode 100644 @@ -0,0 +1,9 @@ + express = require "express" + app = express() + + app.get '/', (req, res)-> + res.send "200", "<html><title>Angular JS</title></html>" + + module.exports = + serve: -> + app.listen(3000) \ No newline at end of file
A super super simple HTTP server that just returns a hardcoded bit of HTML that meets the minimal requirements. Of course, the code was written as a back and forth between each test, in order, and the next feature of the server. So exporting app.listen(3000)
got written first, and later the app.get '/', (req, res)->
. I decided at this point to use Express instead of Node.HTTP in anticipation of needing more capabilities in the future. From a TDD standpoint, it doesn't matter - in fact, it took less code to get the express server running than using Node.HTTP, so that's a TDD win!
test/behavior/steps/browsing.coffee | 2 + -
test/behavior/steps/buildmore.coffee | 6 ++++++
test/behavior/users/edith/Edith.feature | 5 +++++
3 files changed, 12 insertions(+), 1 deletion(-)
diff --git a/test/behavior/users/edith/Edith.feature b/test/behavior/users/edith/Edith.feature --- a/test/behavior/users/edith/Edith.feature @@ -7,3 +7,8 @@ Feature: Edith Given Edith has her browser open When she goes to the site directly Then she should see "Angular JS" in the title + + Scenario: More Features! + Given Edith has her browser open + When she goes to the site + Then she wants MORE FEATURES! \ No newline at end of file
I like the always-failing "Build More!" feature as a reminder to keep on working when running the tests after coming back to the project.
client/index.html | 7 +++++++
server/server.coffee | 3 ++-
server/test/serves.coffee | 7 ++-----
3 files changed, 11 insertions(+), 6 deletions(-)
The first big refactor was pulling the hardcoded HTML into its own file. Notice it also required a change in the test.
diff --git a/client/index.html b/client/index.html new file mode 100644 @@ -0,0 +1,7 @@ + <html> + <head> + <title>Angular JS</title> + </head> + <body> + </body> + </html> \ No newline at end of file
diff --git a/server/server.coffee b/server/server.coffee --- a/server/server.coffee @@ -1,8 +1,9 @@ express = require "express" app = express() + path = require "path" app.get '/', (req, res)-> - res.send "200", "<html><title>Angular JS</title></html>" + res.sendfile path.join __dirname, "..", "client", "index.html" module.exports = serve: ->
diff --git a/server/test/serves.coffee b/server/test/serves.coffee --- a/server/test/serves.coffee @@ -20,11 +20,8 @@ describe "Server", -> it "returns an index page at /", (done)-> index done, (e, r, body)-> - body.should.match /// - ^<html>.* - </html>$ - ///, - "page needs basic HTML structure." + body.should.match /^<html>/ + body.should.match /<\/html>$/ it "returns a page with a title", (done)-> index done, (e, r, body)->
Gruntfile.coffee | 12 ++++++++++++ package.json | 3 ++- 2 files changed, 14 insertions(+), 1 deletion(-)
Watch tasks are a great way to ensure the latest code is always built and running while iterating rapidly. Of course, the build has to run very quickly.
diff --git a/Gruntfile.coffee b/Gruntfile.coffee @@ -12,9 +12,21 @@ module.exports = (grunt)-> options: steps: 'test/behavior/steps' + watch: + all: + files: [ + 'test/**/*coffee' + 'server/**/*coffee' + 'client/**/*html' + 'client/**/*coffee' + 'client/**/*less' + ] + tasks: ['default'] + grunt.npmTasks = [ "grunt-cucumber" "grunt-mocha-test" + "grunt-contrib-watch" ] grunt.loadNpmTasks npmTask for npmTask in grunt.npmTasks
client/index.html | 8 +++++-
server/test/serves.coffee | 15 ++++++++++++
test/behavior/steps/browsing.coffee | 35 ++++++++++++++++++++++-----
test/behavior/support/world.coffee | 11 +++++----
test/behavior/users/edith/Edith.feature | 43 ++++++++++++++++++++++++++++++---
5 files changed, 97 insertions(+), 15 deletions(-)
diff --git a/client/index.html b/client/index.html @@ -1,7 +1,13 @@ <html> <head> - <title>Angular JS</title> + <title>To Do - Angular JS</title> </head> <body> + <header> + <h1>To Do</h1> + </header> + <section> + <input type="text" placeholder="to-do" /> + </section> </body> </html> \ No newline at end of file
diff --git a/test/behavior/users/edith/Edith.feature b/test/behavior/users/edith/Edith.feature @@ -1,4 +1,4 @@ - Feature: Edith + Feature: Site is available As a user of our site Edith wants to see the site So that she knows it exists @@ -6,9 +6,46 @@ Feature: Edith Scenario: Direct Browsing Given Edith has her browser open When she goes to the site directly - Then she should see "Angular JS" in the title + Then she should see "Angular JS" in the "title" + + Feature: Site loads + As a fly-fishing enthusiest + Edith wants the site to load + So she can list her fly-fishing to-do list + + Scenario: Landing bootstrap + Given Edith has her browser open + When she goes to the landing page + Then she should see "To Do" in the "title" + And she should see "To Do" in the "header" + And she should be invited to enter a "to-do" item + + Scenario: First to-do + Given Edith is on the landing page + When she enters "Buy peacock feathers" into a "text" box + Then the page shows "1. Buy peackock feathres" + + # Scenario: Second to-do + # Given Edith has entered a "to-do" item + # And she is invited to enter a "to-do" item + # When she enters "Use peacock feathers to make a fly" into a "text" box + # Then the page shows "1. Buy peackock feathres" + # And the page shows "2. Use peacock feathers to make a fly" + # And she should be invited to enter another "to-do" item + + # # Edith wonders whether the site will remember her list. Then she sees + # # that the site has generated a unique URL for her -- there is some + # # explanatory text to that effect. + # Scenario: Saves list + # Given Edith has entered a "to-do" item + # And she leaves the page + # When she goes to the "saved" url + # Then the page shows "1. Buy peackock feathres" + # And the page shows "2. Use peacock feathers to make a fly" + # And she should be invited to enter another "to-do" item + # # Satisfied, she goes back to sleep Scenario: More Features! Given Edith has her browser open When she goes to the site Then she wants MORE FEATURES!
As you can see, I'm not as good a TDD as I'd like to be. This commit conflagrates finishing out the ToDo title post with the next set of scenarios. It also has a bunch of commented out scenarios - it would be better to use Cucumber attributes, instead of commenting them out. I didn't learn how to do that for another few commits.
client/index.html | 2 + - 1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/client/index.html b/client/index.html @@ -7,7 +7,7 @@ <h1>To Do</h1> </header> <section> - <input type="text" placeholder="to-do" /> + <input type="text" name="to-do" placeholder="to-do" /> </section> </body> </html> \ No newline at end of file
A quick change gets all the basic features passing, and we're ready to start writing actual dynamic application code.