2013/03/25
Minimise Number of Selenium Tests
Using Selenium can be an affective way to test, however, it is complex and expensive. Most projects I have worked on use Selenium too much. It is possible to reduce the amount of Selenium tests, therefore reducing complexity and cost, without reducing the test coverage or quality.
A Selenium based approach suffers from many problems as follows:
- The application under test must be build and deployed to a web server
- The tests (usually) run against a separate browser process
- Tests can't run as part of the regular build process (i.e. directly from MVN / Gradle)
- Long round trip time from completing code to seeing results of tests
- Too complex with too many intercommunicating processes
Selenium tests are a sensible choice for testing:
- Multiple-page and end-to-end flows
- JavaScript integration with different browser features, such as History API or LocalStorage, that can't be easily tested using units tests (i.e. jasmine)
- Cross browser testing
If you are going to use Selenium the following rules help reduce the cost of testing and allow for code refactoring and bug fixing:
- Separate all page interaction into a PageObject
- Expose methods on your PageObject that make business sense and do expose technical details
- Always use ids to refer to HTML elements
- Never use XPath to refer to HTML elements
- Keep test data encapsulated to each test, never share test data between tests
- Ensure each test focuses on a specific goal and does not interact with any addition pages (i.e. where ever possible go directly to a page instead of navigate from another page)
In Process Acceptance Testing with Spring 3.2
In Spring 3.2 several new features were introduced that greatly simplify the testing of the complete MVC stack including all Spring configuration.
It is now possible to load the application context that would be loaded in the org.springframework.web.servlet.DispatcherServlet and then make requests and test expectations against the response.
mockMvc .perform(get("/login").accept(MediaType.TEXT_HTML)) .andExpect(status().isOk()) .andExpect(content().contentType("text/html;charset=UTF-8"));
In addition it is possible to run tests against the HTML produced as the result of any given request. Using a tool like jsoup it is very simple to query the HTML using CSS (or jquery) like selectors.
MvcResult response = mockMvc .perform(get("/login").accept(MediaType.TEXT_HTML)) .andExpect(status().isOk()) .andExpect(content().contentType("text/html;charset=UTF-8")) .andReturn(); Document html = Jsoup.parse(response.getResponse().getContentAsString()); Element usernameElement = html.select("input[name=j_username]").first();
To use such a technique there are some simple steps that need to be followed, as below. All the code in this blog is available as a working example on github https://github.com/jamesdbloom/base_spring_mvc_web_application
Add @RunWith(SpringJUnit4ClassRunner.class) which provides functionality of the Spring TestContext Framework used to support test annotations such as @ContextConfiguration.
Add @ContextConfiguration to load you Spring MVC application context (the one you pass to the DispatcherServlet).
Add @WebAppConfiguration to extends @ContextConfiguration by ensuring a WebApplicationContext is load with a value for the path to the root of the web application.
@RunWith(SpringJUnit4ClassRunner.class) @WebAppConfiguration @ContextConfiguration(classes = WebMvcConfiguration.class) public class LoginPageControllerIntegrationTest {
Add dependency to an WebApplicationContext to be injected by the SpringJUnit4ClassRunner
Use the WebApplicationContext to build a MockMvc object. Filters can also be added at this stage using either a Spring loaded bean (via the DelegatingFilterProxy) or a plain Filter.
@Resource private WebApplicationContext webApplicationContext; private MockMvc mockMvc; @Before public void setupFixture() { mockMvc = webAppContextSetup(webApplicationContext) .addFilter(new DelegatingFilterProxy("filterBean", webApplicationContext)) .build(); }
Write tests using the MockMvc object.
mockMvc .perform(get("/login").accept(MediaType.TEXT_HTML)) .andExpect(status().isOk()) .andExpect(content().contentType("text/html;charset=UTF-8"));
Wrap the result in a PageObject that internally uses JSoup to encapsulate interaction to the page.
MvcResult response = mockMvc .perform(get("/login").accept(MediaType.TEXT_HTML)) .andExpect(status().isOk()) .andExpect(content().contentType("text/html;charset=UTF-8")) .andReturn(); LoginPage loginPage = new LoginPage(response); loginPage.shouldHaveCorrectFields();
Interaction with the page / HTML can be encapsulated in a PageObject as follows:
public class LoginPage { private final Document html; public LoginPage(MvcResult response) throws UnsupportedEncodingException { html = Jsoup.parse(response.getResponse().getContentAsString()); } public void shouldHaveCorrectFields() { hasCorrectUserNameField(); hasCorrectPasswordField(); } public void hasCorrectUserNameField() { Element usernameElement = html.select("input[name=j_username]").first(); assertNotNull(usernameElement); assertEquals("1", usernameElement.attr("tabindex")); assertEquals("text", usernameElement.attr("type")); } public void hasCorrectPasswordField() { Element passwordElement = html.select("input[name=j_password]").first(); assertNotNull(passwordElement); assertEquals("2", passwordElement.attr("tabindex")); assertEquals("password", passwordElement.attr("type")); } }
All the code in this blog is available as a working example on github https://github.com/jamesdbloom/base_spring_mvc_web_application