Paul Hammant's Blog: Testing Knockout and Angular with Selenium2
Selenium2 (AKA –WebDriver) is the de-facto standard web testing technology. It is good that Selenium now garners more job postings than QTP, but that’s a different topic. the Client-Side MVC frameworks are a boon to web app development, but what about writing test scripts for the finished product? In this blog entry I will show how testable AngularJS and KnockoutJS applications are. In my opinion these two are the leading client-side MVC technologies, though there are alternatives like Backbone.js that have significant market share. I prefer the two I’m highlighting over Backbone for a range of reasons.
Testing a KnockoutJS app with Selenium2
There’s a Todo app on Addy Osmani’s technology comparison site. It uses Knockout 2.0 (Knockout 2.1 is in beta-testing presently). Here’s what it looks like:
In testing the app, I’m going to try to write for an idiomatic Knockout app. Here’s the DOM as Firebug sees it:
I’ve highlighted ‘1’ - the input line where new entries are posted, and ‘2’ an attribute where Knockout notes that it is looping through todo items. Prevalent througout the DOM are many more ‘data-bind’ attributes. These are evidence for Knockout managing the page. HTML does not have that attribute, only Knockout understands it. The browser just ignores the attributes it does not understand.
Here is the Selenium2 script (Groovy) that tests this app. It is going to add and item, then mark it as ‘done’, then confirm that the UI displays it as done with a strike-through:
@Grapes([
@Grab("org.seleniumhq.selenium:selenium-java:2.20.0"),
@Grab("org.seleniumhq.selenium.fluent:fluent-selenium:1.5.1"),
@GrabExclude('xml-apis:xml-apis')
])
import org.openqa.selenium.By
import org.openqa.selenium.firefox.FirefoxDriver
import org.openqa.selenium.Keys
import org.openqa.selenium.StaleElementReferenceException
import org.seleniumhq.selenium.fluent.FluentWebDriverImpl
import static org.hamcrest.CoreMatchers.equalTo;
import static org.junit.Assert.assertThat;
import static org.seleniumhq.selenium.fluent.FluentBy.attribute
import static org.seleniumhq.selenium.fluent.Period.secs
def driver = new FirefoxDriver()
def fluent = new FluentWebDriverImpl(driver)
driver.get "http://addyosmani.github.com/todomvc/architecture-examples/knockoutjs/index.html"
def todo = "abc 123 def 456"
def start = System.currentTimeMillis()
// there are many angular examples in one page
def form = fluent.within(secs(10)).div(By.id("todoapp"))
// add an entry
form.input(By.xpath("contains(@data-bind, 'enterKey: add')")).sendKeys todo + Keys.RETURN
assertThat("no items checked initially", form.lis(By.className("done")).size(), equalTo(0))
def lis = form.ul(attribute("data-bind", "foreach: todos")).lis()
// loop through the todo items ..
for (li in lis) {
if (li.getText().contains(todo)) {
// .. click the checkbox for the matching Todo item
li.input().click()
break
}
}
assertThat("one item checked", form.lis(By.className("done")).size(), equalTo(1))
println "Test Duration: " + (System.currentTimeMillis() - start) + " milliseconds."
driver.close()
The best elapsed time for the test was 326 milliseconds. Certainly faster than a human could type.
Testing a AngularJS app with Selenium2
Angular is gearing up for a 1.0 release presently. That’s about time after 2.5 years! As such, I’ve copied their current Todo example to a stand-alone GitHub Pages site. Here is what that app looks like:
Here’s the DOM as seen in Firebug. Again the input field and looping construct are shown:
Angular uses more fine-grained attributes than Knockout. Again, attributes that only it understands. ng-repeat, ng-model, ng-click, ng-show are just some of them. This might make it slightly easier to leverage them for Selenium2 locators.
@Grapes([
@Grab("org.seleniumhq.selenium:selenium-java:2.20.0"),
@Grab("org.seleniumhq.selenium.fluent:fluent-selenium:1.5.1"),
@GrabExclude('xml-apis:xml-apis')
])
import org.openqa.selenium.By
import org.openqa.selenium.firefox.FirefoxDriver
import org.openqa.selenium.Keys
import org.openqa.selenium.StaleElementReferenceException
import org.seleniumhq.selenium.fluent.FluentWebDriverImpl
import static org.hamcrest.CoreMatchers.equalTo;
import static org.junit.Assert.assertThat;
import static org.seleniumhq.selenium.fluent.FluentBy.attribute
import static org.seleniumhq.selenium.fluent.Period.secs
def driver = new FirefoxDriver()
def fluent = new FluentWebDriverImpl(driver)
driver.get "https://paul-hammant.github.io/angular_todo_app/"
def todo = "abc 123 def 456"
def start = System.currentTimeMillis()
def form = fluent.div(attribute("ng-controller", "TodoCtrl"))
// add an entry
form.input(attribute("ng-model", "todoText")).sendKeys todo + Keys.RETURN
//form.input(attribute("type", "submit")).click()
assertThat("one item checked", form.spans(By.className("done-true")).size(), equalTo(1))
def lis = form.lis(attribute("ng-repeat", "todo in todos"))
// loop through the todo items ..
for (li in lis) {
if (li.getText().contains(todo)) {
// .. click the checkbox for the matching Todo item
li.input().click()
break
}
}
assertThat("two items checked", form.spans(By.className("done-true")).size(), equalTo(2))
println "Test Duration: " + (System.currentTimeMillis() - start) + " milliseconds."
driver.close()
The best time for the Angular/Groovy test was 330 milliseconds which is more or less the same as the Knockout one. Still faster than humans can type. It is also important to note that the Angular team have changed from ‘ng’ namespace found in the pre-1.0 releases that meant that colons were prevalent in the HTML source, to a design where a dash is used instead. What was ng:show is now ng-show (etc). This is great news for Selenium2 testing as the colons were a bit of a problem. I blogged previously on that.
Conclusion
Angular and Knockout provide some handy hooks for Selenium2 to navigate by. Though IDs managed by the framework would be better, what’s there is good enough. Angular using more fine-grained attributes rather than the little language that Knockout has in a single attribute and that is going to be easier to navigate with. That said convenience locators could be made for either:
// Angular
form.input(ng.model("todoText")).sendKeys todo + Keys.RETURN
def lis = form.lis(ng.repeat("todo in todos"))
// Knockout
form.input(ko.enterKey("add')")).sendKeys todo + Keys.RETURN
def lis = form.ul(ko.foreach("todos")).lis()
Apr 12, 2012: This article was syndicated by DZone