Title: Does my Button Look Big in This
1Does my Button Look Big in This?
- Building Testable AJAX Applications
Adam Connors and Joe Walnes
2Agenda
- Demonstrate a typical (hard to test) AJAX
application. - Refactor this application into distinct
components. - Test these.
3Iteration 1 A Simple Web Dictionary
DEMO
4Iteration 1 A Simple Web Dictionary
ltinput id"input" type"text" onkeyup"inputChange
d()"/gt
function inputChanged() ...
xmlHttpRequest.onreadystatechange function()
... for (var i 0 i lt results.length
i) var listItem
document.createElement("li")
listItem.onClick function()
previewFrame.src ...
resultsList.appendChild(listItem)
xmlHttpRequest.open("GET", "../words?word"
encodeURIComponent(input.value), true)
xmlHttpRequest.send(null)
index.js
5Iteration 1 Problems
- function inputChanged() contains all the
intelligence - Very difficult to test each step in isolation.
6Tackle complexity by breaking things down
- Refactor code into separate concerns.
- Use the most suitable approach to test each
component. - Some components are easier to test than others.
7Iteration 2 Extracting the View
view.js
function View() this.input
document.getElementById(input)
this.resultsList document.getElementById(result
sList) this.previewFrame document.getElement
ById(previewFrame) ...
8Iteration 2 Extracting the View
view.js
View.prototype.getInput function() return
this.input.value
View.prototype.setInput function(newInput)
this.input.value newInput
View.prototype.showPreview function(url)
this.previewFrame.src url
View.prototype.showResults function(results)
var self this for (var i 0 i lt
results.length i) var listItem
document.createElement("li")
listItem.appendChild(document.createTextNode(resul
tsi)) listItem.onclick function()
self.onPickResult(this.innerHTML)
this.resultsList.appendChild(listItem)
View.prototype.onChangeInput
function(newInput)
View.prototype.onPickResult function(result)
9Iteration 2 Extracting the View
view.js
getInput() setInput() showResults() showPreview()
onChangeInput() onPickResult()
function init() view new View(...)
view.onChangeInput function(newInput)
... xmlHttpRequest.onreadystatechange
function() view.showResults(results)
// Make HTTP Request ...
view.onPickResult function(result)
view.setInput(result) view.showPreview(...)
index.js
10The Humble Object
- Any object that is difficult to test should have
minimal behaviour. That way, if we are unable to
include it in our test suite we minimise the
chances of an undetected failure. - Martin Fowler
11Passive View
- A Passive View reduces the behaviour of the UI
components to the absolute minimum by using a
controller that not just handles responses to
user events, but also does all the updating of
the view.
12Iteration 2 Testing the View
DEMO
13Iteration 2 Testing the View
ltscript type"text/javascript"gt var operations
name "setInput", args"'word'",
name "showResults", args"'apple','aardvark','
ant','arm'", name "showPreview",
args"'http//www.google.com/'", name
"hidePreview" var events
name "onChangeInput", name
"onPickResult" function init()
var view window.frames'ui'.view
window.frames'panel'.setup(view, operations,
events) lt/scriptgt
14Iteration 3 Extracting the Datasource
- Requesting data from the web server is another
distinct responsibility. - By separating these concerns we can test these in
insolation.
15Iteration 3 Extracting the Datasource
var view function init() view new
View() view.onChangeInput
function(newInput) var xmlHttpRequest
createXmlHttpRequest() xmlHttpRequest.onready
statechange function() // Check
readyState. // Parse response.
view.showResults(results) //
Make HTTP request. xmlHttpRequest.send(null)
index.js
BEFORE
AFTER
var view var dataSource function init()
view new View() dataSource new
DataSource() view.onChangeInput
function(newInput) dataSource.request(url,
function(response) view.showResults(respon
se.results) )
index.js
DataSource.prototype.request function(url,
callbackFunction) ...
datasource.js
16Iteration 4 Extracting the Controller
- The role of the Controller is to plumb the View
and Datasource together in such a way that it
encapsulates the flow of the application.
17Iteration 4 Extracting the Controller
var view var dataSource var controller functio
n init() view new View() dataSource
new DataSource() controller new
Controller(view, dataSource)
index.js
view.js
getInput() setInput() showResults() showPreview()
onChangeInput() onPickResult()
datasource.js
function Controller(view, dataSource)
view.onChangeInput function(newInput)
view.hidePreview() dataSource.request(url,
function(response) view.showResults(result
s) ) view.onPickResult
function(result) view.setInput(result)
view.showPreview(...)
controller.js
request()
18Recap Iteration 1
function inputChanged() var input
document.getElementById('input') var
xmlHttpRequest createXmlHttpRequest() var
previewFrame document.getElementById("previewFra
me") previewFrame.src "aboutblank"
xmlHttpRequest.onreadystatechange function()
if (...) var response eval("("
xmlHttpRequest.responseText ")") var
results response.results var resultsList
document.getElementById("resultsList")
if (input.value response.query)
while (resultsList.hasChildNodes())
resultsList.removeChild(resultsList.firstChild)
for (var i 0 i lt
results.length i) var listItem
document.createElement("li")
listItem.appendChild(document.createTextNode(resul
tsi)) listItem.onclick function()
input.value this.innerHTML
previewFrame.src ...)
resultsList.appendChild(listItem)
xmlHttpRequest.open("GET",...,
true) xmlHttpRequest.send(null)
index.js
ITERATION 1
Backend Interaction
DOM Interaction
19Recap Iteration 4
var view var dataSource var controller functio
n init() view new View() dataSource
new DataSource() controller new
Controller(view, dataSource)
index.js
DOM Interaction
ITERATION 4
view.js
getInput() setInput() showResults() showPreview()
onChangeInput() onPickResult()
function Controller(view, dataSource)
view.onChangeInput function(newInput)
view.hidePreview() dataSource.request(url,
function(response) view.showResults(result
s) ) view.onPickResult
function(result) view.setInput(result)
view.showPreview(...)
controller.js
datasource.js
request()
Backend Interaction
20Testing the Controller
- The Controller wires all the events together
The flow of the application. - As an application grows, the flow logic can get
complicated. - With the hard to test bits in other places, its
now easy to test this part in isolation.
21Testing the Controller (2)
var view var dataSource function setUp()
view makeRecorder(...) dataSource
makeRecorder(...) new Controller(view,
dataSource)
controller-test.html
function testRequestsDataFromUrlWhenInputChanges
() view.onChangeInput("hello")
assertEquals("../words?wordhello",
dataSource.request.lastCall.url)
function testHidesPreviewWhenInputChanges()
assertUndefined(view.hidePreview.wasCalled)
view.onChangeInput("stuff")
assertNotUndefined(view.hidePreview.wasCalled)
function testShowsPreviewWhenResultPicked()
assertUndefined(view.showPreview.wasCalled)
view.onPickResult("something")
assertNotUndefined(view.showPreview.wasCalled)
assertEquals("http//www.google.com/search?qdef
inesomething", view.showPreview.calls0.url
)
function testShowsResultsWhenDataSourceFiresCallba
ck() view.onChangeInput("hel")
assertUndefined(view.showResults.wasCalled)
dataSource.request.lastCall.callback(...)
assertNotUndefined(view.showResults.wasCalled)
assertNotUndefined(view.showResults.lastCall.resu
lts) assertObjectEquals(...,
view.showResults.calls0.results)
22Testing the Controller (3)
DEMO
23Testing the Datasource
var url '../words?wordcheesemak' var
expectedResponse '"query""cheesemak",
"results""cheesemaker","cheesemakers","cheesemak
ing"' var actualResponse function
setUpPage() var dataSource new
DataSource() dataSource.request(url,
function(response) actualResponse
response.toJSONString() setUpPageStatus
'complete' )
datasource-test.html
function testReturnsExpectedSpellingSuggestions()
assertEquals(expectedResponse,
actualResponse)
24Summary
- Decompose code into manageable components with
distinct responsibilities. - Minimise the behaviour of components that are
difficult to test. - Use appropriate techniques and tools to test each
in isolation. (Sometimes automated, sometimes
manual.)
25Links
Adam Connors adamconnors_at_google.com Joe Walnes
joejoejoe_at_google.com
- passive view
- humble dialog box
- gui architectures
- jsunit
- selenium testing
- watir
- javascript language survey
- Json
- (Details of code samples will come with follow-up
email)