Obey the Testing Goat NodeJS edition!

The first actual features

In the second part of this walkthrough, we implement the first features of the todo app.

89ea624 Added bundle.

			.gitignore                |   1 +
			Gruntfile.coffee          |  22 +++++++++-
			client/.gitignore         |   1 +
			client/app/todo.coffee    |   4 ++
			client/main.coffee        |   1 +
			package.json              |   8 +++-
			server/server.coffee      |   8 +++-
			server/test/serves.coffee | 100 ++++++++++++++++++++++++++++------------------
			8 files changed, 102 insertions(+), 43 deletions(-)
		

I am really bad at this. This is another two commits in one. The first half is using Browserify to build a javascript bundle during the build process. It works by taking a path to a single file, which requires() its dependencies. The second part is extending the server to return the compiled bundle on request.

			diff --git a/Gruntfile.coffee b/Gruntfile.coffee
			@@ -1,5 +1,16 @@
			module.exports = (grunt)->
				grunt.initConfig
			+		browserify:
			+			dev:
			+				files: "client/bundle.js": ["client/main.coffee"]
			+				options:
			+					shim:
			+						angular:
			+							path: 'bower_components/angular/angular.js'
			+							exports: 'angular'
			+					transform: [
			+						"caching-coffeeify"
			+					]
					mochaTest:
						server:
							options:
			@@ -25,14 +36,23 @@ module.exports = (grunt)->
			
			grunt.npmTasks = [
					"grunt-cucumber"
			+		"grunt-browserify"
					"grunt-mocha-test"
					"grunt-contrib-watch"
				]
			
			grunt.loadNpmTasks npmTask for npmTask in grunt.npmTasks
			
			+	grunt.registerTask "build", [
			+		"browserify:dev"
			+	]
			+
				grunt.registerTask "test", [
					"mochaTest:server"
					"cucumberjs:Edith"
				]
			-	grunt.registerTask "default", ["test"]
			+
			+	grunt.registerTask "default", [
			+		"build"
			+		"test"
			+	]
		
			diff --git a/client/main.coffee b/client/main.coffee
			new file mode 100644
			@@ -0,0 +1 @@
			+todo = require "./app/todo.coffee"
		
			diff --git a/client/app/todo.coffee b/client/app/todo.coffee
			new file mode 100644
			@@ -0,0 +1,4 @@
			+angular =
			+	module: (name, deps)->
			+
			+module.exports = todo = angular.module "todo", []
		

The second half of the commit is configuring the server to return the compiled bundles on request.

			diff --git a/server/test/serves.coffee b/server/test/serves.coffee
			@@ -1,46 +1,70 @@
			should = require "should"
			server = require "../server"
			request = require "request"
			+
			+get = (path, d, cb)->
			+	request path, (e, r, b)->
			+		cb e, r, b
			+		d()
			+
			+index = (d, cb)-> get "http://localhost:3000/", d, cb
			+bundle = (d, cb)-> get "http://localhost:3000/bundle.js", d, cb
			+
			+Callbacks =
			+	OK: (e, res)->
			+		res.statusCode.should.equal 200
			+
			describe "Server", ->
			-	index = (d, cb)->
			-		request "http://localhost:3000/", (e, r, b)->
			-			cb e, r, b
			-			d()
			
			before ->
					server.serve()
			
			[...]
			+	describe "bundle.js", (done)->
			+		it "returns a bundle", (done)->
			+			bundle done, Callbacks.OK
			+
			+		it "returns a js bundle", (done)->
			+			bundle done, (e, res)->
			+				res.headers["content-type"].should.equal "text/javascript"
			+
			+		it "defines an Angular module", (done)->
			+			bundle done, (e, r, body)->
			+				body.should.match ///
			+					angular\.module\("todo"
			+				///
		
			diff --git a/server/server.coffee b/server/server.coffee
			@@ -1,10 +1,14 @@
			+path = require "path"
			express = require "express"
			app = express()
			-path = require "path"
			+
			+app.get '/bundle.js', (req, res)->
			+	res.set "content-type", "text/javascript"
			+	res.sendfile path.join __dirname, "..", "client", "bundle.js"
			
			app.get '/', (req, res)->
				res.sendfile path.join __dirname, "..", "client", "index.html"
		

ae98607 Basic Karma test.

			Gruntfile.coffee       | 21 +++++++++++++++++----
			client/app/test.coffee | 10 ++++++++++
			client/app/todo.coffee |  7 ++++---
			package.json           |  4 +++-
			4 files changed, 34 insertions(+), 8 deletions(-)
		

An Angular test, and Angular code! In grunt, we switch from compiling Angular into the bundle to including Angular from CDN on its own (in this case, "CDN" is our bower install directory). The test is in Karma, and runs in the browser. That said, these are Angular unit tests, working each piece of the module in isolation. This first commit is just enough code to show that Karma runs reasonably.

			diff --git a/Gruntfile.coffee b/Gruntfile.coffee
			@@ -4,13 +4,24 @@ module.exports = (grunt)->
						dev:
							files: "client/bundle.js": ["client/main.coffee"]
							options:
			-					shim:
			-						angular:
			-							path: 'bower_components/angular/angular.js'
			-							exports: 'angular'
			+					debug: true
								transform: [
									"caching-coffeeify"
								]
			+
			+		karma:
			+			unit:
			+				options:
			+					browsers: ["PhantomJS"]
			+					frameworks: ["jasmine"]
			+					singleRun: true
			+					files: [
			+							"bower_components/angular/angular.js"
			+							"bower_components/angular-mocks/angular-mocks.js"
			+							"client/bundle.js"
			+							"client/app/**/test.coffee"
			+						]
			+
					mochaTest:
						server:
							options:
			@@ -36,6 +47,7 @@ module.exports = (grunt)->
			
			grunt.npmTasks = [
					"grunt-cucumber"
			+		"grunt-karma"
					"grunt-browserify"
					"grunt-mocha-test"
					"grunt-contrib-watch"
			@@ -49,6 +61,7 @@ module.exports = (grunt)->
			
			grunt.registerTask "test", [
					"mochaTest:server"
			+		"karma:unit"
					"cucumberjs:Edith"
				]
		
			diff --git a/client/app/test.coffee b/client/app/test.coffee
			new file mode 100644
			@@ -0,0 +1,10 @@
			+describe "todo bootstrap", ->
			+	beforeEach module "todo"
			+
			+	$scope = null
			+	beforeEach inject ($rootScope, $controller)->
			+		$scope = $rootScope.$new()
			+		$controller "todo", {$scope}
			+
			+	it "exposes todo lists", ->
			+		expect($scope.todos).toEqual([])
		
			diff --git a/client/app/todo.coffee b/client/app/todo.coffee
			@@ -1,4 +1,5 @@
			-angular =
			-	module: (name, deps)->
			-
			module.exports = todo = angular.module "todo", []
			+
			+angular.module("todo")
			+.controller "todo", ($scope)->
			+	$scope.todos = []
		

41b7f98 Serves visibly working client...

			Gruntfile.coffee          | 10 +++++-----
			client/app/test.coffee    | 10 ----------
			client/app/todo.coffee    |  5 -----
			client/index.html         | 13 ++++++++++---
			client/main.coffee        |  2 +-
			client/todo/module.coffee |  9 +++++++++
			client/todo/unit.coffee   | 20 ++++++++++++++++++++
			package.json              |  4 +---
			server/server.coffee      |  4 ++++
			server/test/serves.coffee |  8 +++++++-
			10 files changed, 57 insertions(+), 28 deletions(-)
		

A three-for-one! Expanding the index.html to include the angular view code, tests for that code (I promise the tests were written first), and the server code to return bundles and bower resources. And a refactor. So four commits in one.

			diff --git a/client/index.html b/client/index.html
			@@ -1,13 +1,20 @@
			<html>
			<head>
				<title>To Do - Angular JS</title>
			+	<script src="bower/angular/angular.js"></script>
			+	<script src="bundle.js"></script>
			</head>
			-<body>
			+<body ng:app="todo">
				<header>
					<h1>To Do</h1>
				</header>
			-	<section>
			-		<input type="text" name="to-do" placeholder="to-do" />
			+	<section ng:controller="todo">
			+		<form ng:submit="Todos.add()">
			+			<input type="text" name="to-do" placeholder="to-do" ng:model="Todos.current" ng: />
			+		</form>
			+		<ol>
			+			<li ng:repeat="todo in todos">{{todo}}</li>
			+		</ol>
				</section>
			</body>
			</html>
			\ No newline at end of file
		
			diff --git a/client/main.coffee b/client/main.coffee
			@@ -1 +1 @@
			-todo = require "./app/todo.coffee"
			+todo = require "./todo/module.coffee"
		
			diff --git a/client/todo/module.coffee b/client/todo/module.coffee
			new file mode 100644
			@@ -0,0 +1,9 @@
			+module.exports = todo = angular.module "todo", []
			+
			+todo.controller "todo", ($scope)->
			+	$scope.todos = []
			+	Todos = $scope.Todos =
			+		current: ""
			+		add: ->
			+			$scope.todos.push Todos.current
			+			Todos.current = ""
		
			diff --git a/client/todo/unit.coffee b/client/todo/unit.coffee
			new file mode 100644
			@@ -0,0 +1,20 @@
			+describe "todo controller", ->
			+	beforeEach module "todo"
			+
			+	$scope = null
			+	beforeEach inject ($rootScope, $controller)->
			+		$scope = $rootScope.$new()
			+		$controller "todo", {$scope}
			+
			+	it "exposes todos", ->
			+		expect($scope.todos).toEqual([], "todos should be array")
			+		expect($scope.Todos).toBeDefined("Todos should be object")
			+		expect($scope.Todos.current).toBe("", "Todos should have current (empty) reference")
			+		expect(typeof $scope.Todos.add).toBe("function", "Todos.add should be function")
			+
			+	it "can add todo", ->
			+		$scope.Todos.current = "Todo 1"
			+		$scope.Todos.add()
			+		expect($scope.todos.length).toEqual(1, "After adding, todos has single todo")
			+		expect($scope.todos[0]).toEqual("Todo 1", "Correct Todo was added")
			+		expect($scope.Todos.current).toEqual("", "Resets current todo")
			\ No newline at end of file
		
			diff --git a/server/server.coffee b/server/server.coffee
			@@ -6,6 +6,10 @@ app.get '/bundle.js', (req, res)->
				res.set "content-type", "text/javascript"
				res.sendfile path.join __dirname, "..", "client", "bundle.js"
			+app.get '/bower/:module/:file', (req, res)->
			+	res.set "content-type", "text/javascript"
			+	res.sendfile path.join __dirname, "..", "bower_components", req.params.module, req.params.file	
			+
			app.get '/', (req, res)->
				res.sendfile path.join __dirname, "..", "client", "index.html"
		
			diff --git a/server/test/serves.coffee b/server/test/serves.coffee
			@@ -55,7 +55,7 @@ describe "Server", ->
							should.exist matches
							matches[1].should.match /to-do/
			-	describe "bundle.js", (done)->
			+	describe "bundle.js", ->
					it "returns a bundle", (done)->
						bundle done, Callbacks.OK
			@@ -68,3 +68,9 @@ describe "Server", ->
							body.should.match ///
								angular\.module\("todo"
							///
			+
			+	describe "bower", ->
			+		it "serves Bower resources", (done)->
			+			request "http://localhost:3000/bower/angular/angular.js", (e, res)->
			+				res.statusCode.should.equal 200
			+				done()
		

5981165 Support for running selenium in cucumber.

			.gitignore       |  1 +
			Gruntfile.coffee | 29 ++++++++++++++++++++++++++++-
			package.json     |  7 ++++++-
			3 files changed, 35 insertions(+), 2 deletions(-)
		

This is where things got interesting again from the standpoint of refactoring the Grunt file. I hadn't looked at the gherkin world support yet, because it was written using Zombie. Zombie seemed to work great for basic HTML applications, but I couldn't get it to work with AngularJS apps. Instead, I am switching to Angular Protractor, which is built on top of WebdriverJS for the Selenium browser automation framework.

			diff --git a/Gruntfile.coffee b/Gruntfile.coffee
			@@ -28,6 +28,22 @@ module.exports = (grunt)->
								reporter: 'spec'
							src: ["server/test/*coffee"]
			
			+		shell:
			+			kill:
			+				command: "lsof -i TCP -P | grep 4444 | awk '{print $2}' | xargs -I{} kill {} >/dev/null  || exit 0"
			+			selenium:
			+				command: [
			+					"java -jar ./selenium/selenium-server-standalone-2.34.0.jar "
			+					"-Dwebdriver.chrome.driver=./selenium/chromedriver "
			+					">/dev/null"
			+				].join ' '
			+				options:
			+					async: true
			+					kill: true
			+			sleep:
			+				command:
			+					"sleep 1"
			+
					cucumberjs:
						Edith:
							files: src: ['test/behavior/users/edith']
			@@ -38,6 +54,7 @@ module.exports = (grunt)->
						all:
							files: [
								'test/**/*coffee'
			+					'test/**/*feature'
								'server/**/*coffee'
								'client/**/*html'
								'client/**/*coffee'
			@@ -48,6 +65,7 @@ module.exports = (grunt)->
				grunt.npmTasks = [
					"grunt-cucumber"
					"grunt-karma"
			+		"grunt-shell-spawn"
					"grunt-browserify"
					"grunt-mocha-test"
					"grunt-contrib-watch"
			@@ -59,10 +77,19 @@ module.exports = (grunt)->
					"browserify:dev"
				]
			
			+	grunt.registerTask "features", [
			+		"shell:kill" # Clean up any old selenium servers, or other programs who may be hogging 4444
			+		"shell:selenium"
			+		"shell:sleep"
			+		"cucumberjs:Edith"
			+		"shell:selenium:kill" # Stop the selenium server
			+		"shell:kill" # Also has the effect of killing driven browsers.
			+	]
			+
				grunt.registerTask "test", [
					"mochaTest:server"
					"karma:unit"
			-		"cucumberjs:Edith"
			+		"features"
				]
			
			grunt.registerTask "default", [
		

Holy shell spawn, Batman! The Selenium Server is a Java program, so running it is a lot of java commands through the shell, along with some process control for when things start getting unruly.

a976bbd Updated world tests.

			test/behavior/steps/browsing.coffee            | 87 +++++++++++++++++---------
			test/behavior/support/world.coffee             | 17 -----
			test/behavior/support/worlds.coffee            |  2 +
			test/behavior/support/worlds/protractor.coffee | 40 ++++++++++++
			test/behavior/support/worlds/zombie.coffee     | 21 +++++++
			test/behavior/users/edith/Edith.feature        | 45 ++++++-------
			6 files changed, 144 insertions(+), 68 deletions(-)
		

And here is the Protractor world!

			diff --git a/test/behavior/steps/browsing.coffee b/test/behavior/steps/browsing.coffee
			@@ -1,43 +1,70 @@
			+Q = require 'q'
			should = require "should"
			-World = require "../support/world"
			+World = require "../support/worlds"
			module.exports = ->
			-	@Given /has (?:his|her|a) browser open$/, (cb)->
			+	@Before (done)->
					# opening a browser is like entering a whole new world...
					@world = new World()
			-		cb()
			+		done()
			
			-	@Given /on the landing page/, (cb)->
			-		@world = new World()
			+	@After (done)->
			+		@world?.destroy()
			+		done()
			+
			+	@Given /has (?:his|her|a) browser open$/, (done)->
			+		done()
			+
			+	@Given /on the landing page/, (done)->
					@world.visit("http://localhost:3000/")
			-		.then cb
			+		.then(done)
			
			-	@When /goes to the (?:site|landing page)(?: directly)?/, (callback) ->
			+	@When /goes to the (?:site|landing page)(?: directly)?/, (done) ->
					@world.visit("http://localhost:3000/")
			-		.then callback
			+		.then(done)
			
			-	@When /enters "([^"]+)" into the "([^"]+)" (?:box|input|field)/, (value, field, cb)->
			-		@world.fill(field, value)
			-		cb()
			+	@When /enters "([^"]+)" into (?:the|a) "([^"]+)" (?:box|input|field)/, (value, field, done)->
			+		@world.fill(field, value, true)
			+		.then(->done())
			+		.catch(done)
			+
			+	@When /enters into (?:the|a) "([^"]+)" (?:box|input|field)/, (field, lines, done)->
			+		Q.all([@world.fill(field, line, true) for line in lines.split '\n'])
			+		.then(->done())
			+		.catch(done)
			+
			+	###
			+	Check for existance of a string in the title
			+	###
			+	@Then /should see "([^"]*)" in (?:the )title$/, (what, done) ->
			+		@world.title()
			+		.then (text)->
			+			text.indexOf(what).should.be.greaterThan -1,
			+				"'#{what}' expected in title"
			+			done()
			+		.catch done
			
			###
				Check for existance of a string in a selector
				###
			-	@Then /should see "([^"]*)" in (?:the )"([^"]*)"$/, (what, where, callback) ->
			-		@world.text(where).indexOf(what).should.be.greaterThan -1,
			-			"'#{what}' expected in '#{where}'"
			-		callback()
			-
			-	@Then /invited to enter a "([^"]*)"/, (value, callback)->
			-		inputs = @world.find("input[type=text]")
			-		inputs.length.should.equal 1,
			-			"One text input must exist."
			-		placeholder = inputs[0].getAttribute('placeholder')
			-		should.exist placeholder,
			-			"Input should have placeholder."
			-		placeholder.should.match ///#{value}///,
			-			"Placeholder should be inviting."
			-		callback()
			-
			-	@Then /page shows "([^"]+)"/, (content)->
			-		text = @world.text("body")
			-		text.should.match ///#{content}///
			+	@Then /should see "([^"]*)" in (?:the )"([^"]*)"$/, (what, where, done) ->
			+		@world.text(where)
			+		.then (text)->
			+			text.indexOf(what).should.be.greaterThan -1,
			+				"'#{what}' expected in '#{where}'"
			+			done()
			+		.catch done 
			+
			+	@Then /invited to enter a(?:nother)? "([^"]*)"/, (value, done)->
			+		@world.placeholder("input[type=text]")
			+		.then (placeholder)->
			+			placeholder.should.match ///#{value}///,
			+				"placeholder should be inviting."
			+			done()
			+		.catch done
			+
			+	@Then /page shows "([^"]+)"/, (content, done)->
			+		@world.text("body")
			+		.then (text)->
			+			text.should.match ///#{content}///
			+			done()
			+		.catch done
		

The biggest change is the handling of the callback. The Cucumber signature is callback([err]), where passing anything to the callback results in the step failing, and calling the callback with no arguments is a success. I prefer a deferred async style, so massaging the Q callbacks around the cucumber callback takes some jumping though hoops. I would like to make this more sensible in the future.

			diff --git a/test/behavior/support/worlds/protractor.coffee b/test/behavior/support/worlds/protractor.coffee
			new file mode 100644
			@@ -0,0 +1,40 @@
			+Q = require "q"
			+should = require "should"
			+webdriver = require "selenium-webdriver"
			+Protractor = require "protractor"
			+By = Protractor.By
			+
			+module.exports = class World
			+	constructor: ->
			+		@driver = new webdriver.Builder().
			+			usingServer('http://localhost:4444/wd/hub').
			+			withCapabilities(webdriver.Capabilities.firefox()).build();
			+
			+		@driver.manage().timeouts().setScriptTimeout(10000);
			+		@ptor = Protractor.wrapDriver(@driver);
			+
			+	visit: (url)->
			+		Q @ptor.get(url)
			+
			+	find: (selector)->
			+		@ptor.findElement By.css selector
			+
			+	text: (where)->
			+		Q @find(where).getText()
			+
			+	title: ->
			+		Q @ptor.getTitle()
			+
			+	placeholder: (where)->
			+		Q @find(where).getAttribute 'placeholder'
			+
			+	fill: (what, value, submit = false)->
			+		input = @ptor.findElement(By.name(what))
			+		deferred = input.sendKeys(value)
			+		if submit
			+			deferred.then ->
			+				input.submit()
			+		Q deferred
			+
			+	destroy: (done)->
			+		@driver.quit()
		

The underlying world is pretty straight forward, connecting to Selenium on the port the Gruntfile launched it on, and passing the various step commands through sensibly. Facade would be the design patterns phrase.

Onward, to an MVP!