Agile Testing

Ajax testing with Selenium using waitForCondition

 

An often-asked question on the selenium-users mailing list is how to test Ajax-specific functionality with Selenium. The problem with Ajax testing is that the HTML page under test is modified asynchronously, so a plain Selenium assert or verify command might very well fail because the element being tested has not been created yet by the Ajax call. A quick-and-dirty solution is to put a pause command before the assert, but this is error-prone, since the pause might be not sufficient on a slow machine, while being unnecessarily slow on a faster one.

A better solution is to use Dan Fabulich's waitForCondition extension. But first, a word about Selenium extensions.

If you've never installed a Selenium extension, it's actually pretty easy. You should have a file called user-extensions.js.sample in the same directory where you installed the other core Selenium files (such as TestRunner.html and selenium-api.js). You need to rename that file as user-extensions.js, so that it will be automatically picked up by Selenium the next time you run a test. To install a specific extension such as waitForCondition, you need to download and unpack the extension's zip file, then add the contents of the user-extensions.js.waitForCondition file to user-extensions.js. That's all there is to it.

Now back to testing Ajax functionality. For the MailOnnaStick application, Titus and I used Ian Bicking's Commentary application as an example of Ajax-specific functionality that we wanted to test with Selenium. See this post of mine for details on how Commentary works and how we wrote our initial tests. The approach we took initially was the one I mentioned in the beginning, namely putting pause commands before the Ajax-specific asserts. Interestingly enough, this was the only Selenium test that was breaking consistently in our buildbot setup, precisely because of speed differences between the machines that were running buildbot. So I rewrote the tests using waitForCondition.

What does waitForCondition buy you? It allows you to include arbitrary Javascript code in your commands and assert that a condition (written in Javascript) is true. The test will not advance until the condition becomes true (hence the wait prefix). Or, to put it in the words of Dan Fabulich:

waitForCondition: Waits for any arbitrary condition, by running a JavaScript snippet of your choosing. When the snippet evaluates to "true", we stop waiting.

Here's a quick example of a Selenium test table row that uses waitForCondition (note that the last value in the 3rd cell is a timeout value, in milliseconds):

waitForConditionvar value = selenium.getText("//textarea[@name='comment']"); value == "" 10000

What I'm doing here is asserting that a certain HTML element is present in the page under test. For the Commentary functionality, the element I chose is the text area of the form that pops up when you double-click on the page. This element did not exist before the double-click event, so by asserting that its value is empty, I make sure that it exists, which means that the asynchronous Ajax call has completed. If the element is not there after the timeout has expired (10 seconds in my case), the assertion is marked as failed.

To get to the element, I used the special variable selenium, which is available for use in Javascript commands that you want to embed in your Selenium tables. The methods that you can call on this variable are the same methods that start with Selenium.prototype in the file selenium-api.js. In this case, I called getText, which is defined as follows in selenium-api.js:

Selenium.prototype.getText = function(locator) {
var element = this.page().findElement(locator);
return getText(element).trim();
};

This function gets a locator as its only argument. In the example above, I used the XPath-style locator "//textarea[@name='comment']" -- which means "give me the HTML element identified by the tag textarea, and whose attribute name has the value 'comment'". The value of this HTML element is empty, so this is exactly what I'm asserting in the test table: value == "".

You might wonder how I figured out which element to use in the assertion. Easy: I inspected the HTML source of the page under test before and after I double-clicked on the page, and I identified an element which was present only after the double-click event.

The other scenario I had to test was that the Commentary post-it note is not present anymore after deleting the commentary. Again, I looked at the HTML page under test before and after clicking on the Delete link, and I identified an element which was present before, and not present after the deletion. Here is the waitForCondition assertion I came up with:

waitForConditionvar allText = selenium.page().bodyText(); var unexpectedText = "hello there from user${var}" allText.indexOf(unexpectedText) == -1; 10000

Here I used selenium.page() to get to the HTML page under test, then bodyText() to get to the text of the body tag. I then searched for the text that I was NOT expecting to find anymore, and I asserted that the Javascript indexOf() method returned -1 (i.e. the text was indeed not found.)

Here is the test table for the Commentary functionality in its entirety:

TestCommentary
open/message/20050409174524.GA4854@highenergymagic.org
dblclick//blockquote
waitForConditionvar value = selenium.getText("//textarea[@name='comment']"); value == "" 10000
storejavascript{Math.round(1000*Math.random())}var
typeusernameuser${var}
typeemailuser${var}@mos.org
typecommenthello there from user${var}
click//form//button[1]
waitForConditionvar value = selenium.getText("//div[@class='commentary-comment commentary-inline']"); value.match(/hello there from user${var}/);10000
verifyText//div[@class="commentary-comment commentary-inline"]regexp:hello there from user${var}
clickAndWait//div/div[position()="1" and @style="font-size: 80%;"]/a[position()="2" and @href="/search"]
typequser${var}
clickAndWait//input[@type='submit' and @value='search']
verifyValuequser${var}
assertTextPresentQuery: user${var}
assertTextPresentin Re: [socal-piggies] meeting Tues Apr 12th: confirmed
open/message/20050409174524.GA4854@highenergymagic.org
assertTextPresenthello there from user${var}
assertTextPresentdelete
clicklink=delete
waitForConditionvar allText = selenium.page().bodyText(); var unexpectedText = "hello there from user${var}" allText.indexOf(unexpectedText) == -1; 10000
assertTextNotPresenthello there from user${var}
assertTextNotPresentdelete
clickAndWait//div/div[position()="1" and @style="font-size: 80%;"]/a[position()="2" and @href="/search"]
typequser${var}
clickAndWait//input[@type='submit' and @value='search']
verifyValuequser${var}
assertTextPresentQuery: user${var}
assertTextPresentno matches


For more details on how to identify HTML elements that you want to test using Selenium test tables, see this post on useful Selenium tools, and this post on using the Selenium IDE.

Update: Since images are worth thousands and thousands of words, here are 2 screencasts (no sound) of running the Commentary test with Selenium: one in Windows AVI format, and the other one in Quicktime MOV format (you might be better off saving the files to your local disk before viewing them.)

 

13 Comments:
  • As usual, great work, Grig! :-)

    The screencasts truly are worth a thousand words. :-) The syntax of the test (especially the waitForCondition command) is starting to look scary, though. Very powerful, but scary looking-- like PHP or Perl. :-) I'm kinda craving a mini-port of Ruby for the JavaScript "platform" so we can get some of its DSL and "anonymous code block" goodness.

    By Jason Huggins, at 6:13 PM  

  • Jason,

    Thanks for the comments. Yeah, it is kind of scary to see all that Javascript code in there, but on the other hand it does offer you tremendous flexibility (so you can shoot yourself in the foot at your ease :-)

    Ruby-in-Javascript sounds pretty enticing to me :-)

    By Grig Gheorghiu, at 6:18 PM  

  • On the first example you need to place a ';' at the end of the first example or it doesn't work.

    var value = selenium.getText("//textarea[@name='comment']"); value == "";

    By Anonymous, at 12:28 PM  

  • Fascination blog..u made my life simpler..
    I have a requirement to test the AJAX pages in the web app.

    Scenario :

    1)Enter a value in a edit box . (Hello)
    2) Click on a button which does a partial page refresh or a partial post back.
    3)The value entered in the edit box will show up in the page on step (2).
    4)Need to check or read this value.(Check for Hello)

    sel.waitForCondition("String str=sel.getBodyText();str.indexOf(Hello)!=-1;","30000");
    error on running:
    com.thoughtworks.selenium.SeleniumException: missing ; before statement

    Whts to be done..??

    By Hari, at 7:55 AM  

  • In my next project I have to test an ajax application. I am trying to find out the challenges posed by ajax applications w.r.t testing. Will the normal automation tools work or are there any specific tools to test ajax apps? I am also trying to findout why these specific tools are required? Why the already existing tools cant do the job?
    Any useful links in this regard will be helpful.

    By Nagendra, at 1:35 AM  

  • So waitForCondition works by polling for a condition, but could some method of hooking in to the AJAX success callback or the general AJAX response handler be feasible?

    -Trey

    By Anonymous, at 10:12 PM  

  • At my previous company we were using Selenium to test an AJAX-y app.

    In the end we put in some code that extended the existing xxxAndWait commands to check that any outstanding requests had been serviced (a la COMs ->AddReference model).

    It worked pretty well, and didn't need us to re-implement too much code. Although it was pretty ugly code :)

    By Paul Ingles, at 1:39 PM  

  • Thanks, this is good, I was able to use the WaitForCondition command right away after reading your post..


    Dr. Java

    By Dr. Java, at 12:52 PM  

  • Hi,

    In my version of selenium (0.8.1) the function call getText(..) will fail with an error.

    I browsed a little through the selenium-browserbot.js and noticed that the function PageBot.findElement() will throw a SeleniumError if the element is not found. Probably this is a change in the newer versions of Selenium.

    My quick and dirty fix was to surround the getText() and findElement() calls with try..catch blocks.

    Thank you for this usefull post,
    Stefan Hornea

    By Anonymous, at 9:58 AM  

  • Check out WebAii automation at www.artoftest.com. Seems like a new Ajax framework for anyone doing .NET testing. Looks like the beta is out and the final release will be sometime mid year...

    By Anonymous, at 8:51 PM  

  • Wow, people love to make thinks more and more complicated :)

    An simpler solution would be to pass your XMLHTTPRequest
    from async to sync mode for in-browser testing session. Of course the ajax framework needs to support this some.

    By Vitaliy Shevchuk, at 1:51 AM  

  • Very useful, Grig!. I am having trouble with mouseover in selenium.
    The page that I am testing has a AJAX call only when you go to the bottom of the page. I get that with mouseover I can do that in selenium, but havent been able to successfully do it.

    Any pointers?

    By Anonymous, at 11:46 AM  

  • The AJAX creates problems for all testing tools, because the calls to XMLHTTPRequest do not generate Browser events - there are no “complete” events for the AJAX and there is no way to get real browser ready status. Without the "complete" events it is hard to create reliable automation solution for AJAX.
    To my knowledge only SWExplorerAutomation (SWEA) from Webius(http://webiussoft.com) monitors the Browser network activity to get true browser ready status.

    By alex_f_il, at 9:21 AM  

Post a Comment

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值