I've been using Selenium for web application testing for over a year now, on a couple of different projects. This talk is based on my experiences.
What is Selenium?
Selenium is actually an umbrella term for a suite of tools. The two tools in the suite I have been using are Selenium Remote Control (RC) ,which allows you to test your webapp as it runs in a broswer, from Java code., and Selenium IDE, which is a Firefox plugin that allows you to record tests.
Selenium's killer feature is that it allows you to test your app while it is running in a browser, with a real browser javascript engine. This is particularly important for web applications with a lot of javascript and ajax. Selenium is also actively maintained, with version 2.0 having recently arrived at alpha release status.
I showed a demo of how to write a test using Selenium IDE to record browser interaction, then pasting that into Java code. The test doesn't pass the first time you execute it, you have to tweak the code to replace xpaths with ids, and add a "wait" for the page to load at one point in the test. I find this is typical - Selenium IDE helps you to get started with your test, but there is still a lot of manual work to get it to pass.
The code you end up with is rather widget-oriented and low level, this is what I end up with in my example:
@Test
public void createNewPublisher() {
selenium.click("menuItemPNewPublisher");
selenium.waitForPageToLoad("30000");
selenium.type("name", "Disney");
selenium.type("address", "24 Stars Ave");
selenium.type("zipCode", "91210");
selenium.type("city", "Hollywood");
selenium.type("country", "USA");
selenium.type("contactPersonName", "Emily");
selenium.type("contactPersonEmail", "emily@test.com");
selenium.type("contactPersonPassword", "123456");
selenium.type("contactPersonRetypePassword", "123456");
selenium.click("save");
selenium.waitForPageToLoad("30000");
verifyEquals(selenium.getTable("Users.1.1"), "Emily");
}
This is all very well, you can basically read what it is doing, (open the "New publisher" page, fill in a form, then check the user table displays correctly), but I think we can do better.
Agile Testing Theory
Brian Marick came up with this idea of "Testing Quadrants", which helps us to reason about what kinds of tests we need in an agile project. Selenium tests generally fall into the second quadrant - business facing tests that support the team. This implies they should be written in the language of the business, to help developers to understand what they are building. This is in contrast to the low-level, widget oriented script that selenium IDE produces.
It is better to discover a bug as soon after insertion as possible, and the faster the tests run, the more often you will run them. The usual rule of thumb says your tests must take less than a minute for you to run them as part of a TDD cycle, and less than 10 minutes for you to run them in the CI server. For a suite of selenium tests, even 10 minutes can be ambitious.
Selenium tests are fundamentally slow. They are slow because they run inside the browser using its javascript motor.
WebDriver
WebDriver (new in Selenium 2.0) uses a different model of interacting with your application under test. It uses the browser's api instead of its javascript motor. This means tests execute faster and more reliably, at the cost of supporting fewer browsers. In addition, WebDriver has a well thought out, clean api, and built in support for the PageObject pattern. Having said that, WebDriver is a very recent addition to selenium, and currently has poor or no integration with Selenium IDE and Selenium Grid.
the Page Object pattern
The PageObject pattern is a way of organizing your test code. It tells you to model your test code after the page structure of your application. The test I listed earlier could be rewritten using this pattern, and might look something like this:
@Test
public void createNewPublisher() {
PublisherData newPublisher = new PublisherData("Disney")
.withAddress(
new AddressData("24 Stars Ave",
"91210", "Hollywood", "USA"))
.withContactPerson(
new UserData("Emily", "emily.bache@test.com",
"123456"));
NewPublisherPage page = new NewPublisherPage(selenium);
page.enterPublisherDetails(newPublisher);
ViewPublisherPage viewPage = (ViewPublisherPage)page.save();
viewPage.verifyPublisherDetails(newPublisher);
}
Here we are using a data struct to hold the form data, which tells you quite obviously that a PublisherData has a name, address and contact person. Then we have the Page Object for the NewPublisherPage, which uses selenium to navigate to the relevant page in the constructor, then has methods which allow us to interact with that page. When we as it to "save", we navigate to a new page, in this case the ViewPublisherPage, which we can ask to validate the new publisher is displayed correctly.
This is an improvement over the original test in several ways. I think it is much more readable, we have a certain amount of type safety from the data struct, and if the page flow, or form data changes, there are obvious central places in the code to update all the tests. We can easily vary the form data in different tests in order to do data driven testing.
Scenario classes
However, in the application I've been working on, we havn't used this pattern in quite this way. The application uses a lot of frames and ajax, and a page based model didn't really fit. So we've been using the idea of "Scenario" classes instead of Page Objects. They fulfill the same purpose, but instead of modelling pages in your app, these classes group together domain actions, or services that your application provides. The Scenario classes hide all the details of how to use selenium to interact with the GUI and achieve an outcome a user is interested in. In this case, creating a publisher.
Domain Specific Languages
@Test
public void createNewPublisher() {
PublisherData newPublisher = new PublisherData("Disney")
.withAddress(new AddressData("24 Stars Ave",
"91210", "Hollywood", "USA"))
.withContactPerson(
new UserData("Emily", "emily@test.com",
"123456"));
PublisherScenarios scenarios =
new PublisherScenarios(selenium);
scenarios.createPublisher(newPublisher);
scenarios.verifyPublisherDetails(newPublisher);
}
So with the PageObject or Scenario patterns, you could say that my tests are written in a kind of Domain Specific Language, in that they are written in terms close to the domain of the application they are testing. The trouble is, this is still Java code, and business people generally can't read Java. A more interesting DSL would enable communication between developers and business people.
This is where I turned to Fitnesse to help me. I found it pretty straightforward to put some Fitnesse tables over the top of this kind of Java code. Since the Java code is already organized in terms of the domain, it becomes natural to offer the same functionality in tables with meaningful names.
I chose Fitnesse because I wanted to try it out, but I think it would be equally easy to put Cucumber or Robot Framework or something like that on top, too. The whole point of these tools is that they allow you to easily write executable tests in a language anyone with knowledge of the domain can read. This is a huge benefit to developers when talking to business people.
Closing advice
Having said all this about how to make your Selenium tests more readable, robust and faster, I still find my Selenium test suite frustratingly slow and fragile (I havn't used WebDriver enough yet to know if that is the case there too). Compared to PyUseCase, it is in the dark ages. (Unfortunately PyUseCase has no support for web applications, so there's probably no point in mentioning it, actually. But it is a great tool!).
The difference between PageObjects and Scenario classes is not huge, but I like the latter, because they allow me to easily exchange Selenium for a WebService. My tests are written in terms of the services the application offers, and if I indeed implement those services as WebServices, I can call them directly from the tests. Using a WebService instead of selenium immediately makes my tests much faster and more robust.
The problem with testing against a WebService is the reason we chose Selenium in the first place - ajax and browsers are not covered.
So for web 2.0 application testing, I recommend both API tests and Selenium tests. Use Selenium to test the crucial parts of your GUI, but write most of your tests against some kind of WebService or API. Write your tests in terms a business user would understand, even in the Java code, and then it is easy to create a Domain Specific Language on top using another tool.
Nice overview of browser-testing. We're using WebDriver on my project, and I'd like to add one tip for your users: Almost all your WebDriver tests can be trivially made to run with webdriver-htmlunit, which runs about 5 times faster than e.g. webdriver-firefox.
ReplyDeleteLike you, I find that moving a large part of the testing to API really improves the value of the whole test suite (in my case, no web service, though).
Thanks for your comment!
ReplyDeleteI guess if most of your tests don't test ajax/browser then webdriver-htmlunit could help you to get faster feedback. I think if you're going to skip the ajax and browser, then API tests would be cheaper to maintain and faster to run. Having the ability to run the same tests via webdriver-htmlunit and webdriver-firefox might outweigh those advantages though.
Little known fact: htmlunit supports JavaScript! (Actually, there are some JavaScript that will not run well in HtmlUnit, but in theory, it's all supported)
ReplyDeleteAlso, since the difference is only in the instance creation, all you need is:
WebDriver driver = (WebDriver) Class.forName(System.getProperty("WebDriver", "HtmlUnitDriver")).newInstance();
This will use a system property to determine what browswer to use.