Wednesday 29 April 2009

Selenium CSS locators

We've got a project with quite a large set of tests, and we have both Firefox and IE7 continuous builds using selenium. The annoying thing is that something that takes 5 minutes in Firefox can take 30+ minutes with IE (I kid you not!).

It turns out this is a common problem and is due to the lack for native xpath support in IE. So for all the selenium tests that use xpath=//..... in IE the xpath is actually being evaluated using javascript ... ouch. An alternative is to use css locators, you can't do this everywhere but as well as improving the IE performance it can also provide some rather tidy rules. For example


xpath=//div[contains(@class,'balance')]
or
css=.balance

xpath=//div[@id,'topLeft')//span[contains(@class,'name')]
or
css=#topLeft .name



So even if you dont' care about IE using css selectors might be a lot nicer. (It might also improve your css skills!)

Thursday 23 April 2009

Wednesday 15 April 2009

instanceof vs. HibernateProxy and inheritance

I posted something over on my blog about the exciting things that can happen when Hibernate proxies your base class to wrap an instance of your sub-class.

Sunday 12 April 2009

IE hangs with "Waiting for 1 resource to load"

Turns out this is a common problem with libraries like prototype and jquery. There's a good discussion of it here...

http://groups.google.com/group/prototype-scriptaculous/browse_thread/thread/a1c745463251a95d?pli=1

The suggested solution of replacing src=:// with src=blank.js worked for us.

Friday 3 April 2009

Timing Selenium

We finally figured a pain free way of profiling our selenium test suite. It could use some refinement, but we were only looking for a quick win.

Add the following test to the start of your suite

open|/|
storeEval|{lastTime = new Date().getTime(); 'x'}|x
storeEval|{LOG.info = function(message) { var now = new Date().getTime(); var duration = now - lastTime; this.warn(message + duration); lastTime = now}; 'x';}|x


When your run your suite the log file file contain lines like...


warn: Starting test /selenium-server/tests/suite-tests/conversation/SearchConversationTest.html119
warn: Executing: |open | /fixture/tearDownTestData | |47
warn: Executing: |assertTitle | Fixture | |239
warn: Executing: |invokeUrl | /fixture/reindexSearch | |28
warn: Executing: |invokeUrl | /fixture/setUpPersons | |121
warn: Executing: |invokeUrl | /fixture/setUpConversationsForSearch | |212
warn: Executing: |createCookie | splash=false | path=/ |16638


The duration for each step has been appended to the NEXT log line, i.e. setUpConversationsForSearch took 16638ms
The next step is to parse this file into something readable. We wrote a groovy script (see below), which generates output like:


===SELENIUM TEST TIMINGS===
69763=HomepageTest
54106=SubscribeToTribeConversationTest
50902=SubscribeToTagTest
48734=TagDedupingTest
44614=SubscribeToPrivateConversationTest

===INDIVIDUAL COMMAND TIMINGS===
153127=loginAndWait hits=291 avg=526.2096219931
103428=open /person/ hits=241 avg=429.1618257261
90431=open /logout hits=224 avg=403.7098214286
70059=invokeUrl /fixture/setUpConversationsForSearch hits=4 avg=17514.75
61926=open /login hits=246 avg=251.7317073171
61851=open / hits=133 avg=465.045112782
61034=createMessageAndWait hits=114 avg=535.3859649123


So now it's trivial to identify slow running tests / test steps when optimising our build. It's also useful to spot slow loading pages.


Here's the groovy script, it's tailored to our environment but a good starting point.

import org.codehaus.groovy.tools.LoaderConfiguration
import org.codehaus.groovy.tools.RootLoader
import org.codehaus.groovy.grails.commons.cfg.ConfigurationHelper
import org.codehaus.groovy.grails.commons.ConfigurationHolder
import groovy.text.GStringTemplateEngine
import org.apache.commons.lang.StringUtils

Ant.property(environment:'env')
grailsHome = Ant.antProject.properties.'env.GRAILS_HOME'
grailsEnv = System.properties.'grails.env';
pluginDir = this.binding['seleniumPluginDir']

includeTargets <<> timings = []

int nextId = 0
String currentTest = 'anonymous-test'
SeleniumTiming previousTiming
SeleniumTiming currentTiming

class SeleniumTiming {
String id
String test
String command
String target
String value
int duration = 0

String getTargetKey() {

def trimmedTarget = target?.trim()
def trimmedCommand = command?.trim()

if (trimmedCommand == 'open' && trimmedTarget ==~ '/person/\\w+') {
return '/person/'
} else if (trimmedCommand == 'open' && trimmedTarget ==~ '/tag/[\\w-]+') {
return '/tag/'
} else if (trimmedCommand == 'open' && trimmedTarget ==~ '/station/[\\w-]+') {
return '/station/'
} else if (trimmedCommand == 'open' && trimmedTarget ==~ '/line/[\\w-]+') {
return '/line/'
} else if (trimmedCommand == 'open' && trimmedTarget ==~ '/conversation/subscribe?.*') {
return '/conversation/subscribe'
} else if (trimmedCommand == 'open' && trimmedTarget ==~ '/conversation/unsubscribe?.*') {
return '/conversation/unsubscribe'
} else if (trimmedCommand == 'loginAndWait') {
return ''
} else if (trimmedCommand == 'sendMessageAndWait') {
return ''
} else if (trimmedCommand == 'createMessageAndWait') {
return ''
} else if (trimmedCommand == 'reply') {
return ''
} else if (trimmedCommand == 'replyAndWait') {
return ''
} else if (trimmedCommand == 'registerAndWait') {
return ''
} else if (trimmedCommand == 'reportThisAndWait') {
return ''
} else {
return target
}
}

String toString() {
"[$test] $command($target,$value):$duration"
}
}

def parseTestLine = { String input ->
currentTest = input.substring(input.lastIndexOf(File.separator)+1, input.lastIndexOf('.html'))
}

def parseCommandLine = { String input ->
List data = input.tokenize('|')
if(data.size() == 5) {
previousTiming = currentTiming
currentTiming = new SeleniumTiming(id:nextId++, test: currentTest, command:data[1],
target:data[2], value:data[3])
timings << duration =" Integer.parseInt(data[4])" parsefile =" {">
input.eachLine { String line ->
if(line.startsWith('warn: Starting test')) {
parseTestLine line
} else if(line.startsWith('warn: Executing: |')) {
parseCommandLine line
}
}
}

target ('default': 'Generate Selenium Timings Report') {
loadSeleniumConfig seleniumConfig
new File(seleniumConfig.reportdir).eachFileMatch(~/.*\.html/, parseFile)

def longestFirst = [ compare: { a, b -> a.equals(b) ? 0: Math.abs(a) < overalltimings =" new">
overallTimings[(values*.duration).sum()] = key
}

Map commandTimings = new TreeMap(longestFirst)
timings.groupBy({ "${it.command} ${it.targetKey}" }).each { key, values ->
int totalDuration = (values*.duration).sum()
int noOfValues = values.size()
commandTimings[totalDuration] = "$key hits=${noOfValues} avg=${totalDuration / noOfValues}"

}

new File(seleniumConfig.reportdir, 'timings.txt').withPrintWriter { writer ->
writer.println '===SELENIUM TEST TIMINGS==='
overallTimings.each { writer.println it }
writer.println '===INDIVIDUAL COMMAND TIMINGS==='
commandTimings.each { writer.println it }
}

commandTimings.each { println it }

}