Michael Whelan

behaviour driven blog

Seleno 0.4

TestStack have just released version 0.4 of Seleno, our web browser automation framework. It's a pretty big release with lots of bug fixes and enhancements and quite a number of breaking changes as we solidify the core API in preparation for a 1.0 release. We have done extensive refactoring to the core code and internal architecture to improve its testability and to lay the groundwork for new features we are wanting to provide in future releases. While we have strived to keep breaking changes to a minimum, we did choose to make a few as we saw opportunities to improve the API and make the framework easier to use. These changes will greatly reduce as Seleno matures but at this stage we feel it is good to let the API evolve.

Breaking Changes

We have introduced a Breaking Changes text file, which we will update if we ever need to make a breaking change to the API. You can find it here. Given the number of breaking changes in this release I strongly recommend that you check it out if you are upgrading from an earlier version!

New Features

SelenoHost / configuration

One of the breaking changes is that SelenoApplicationRunner has been renamed to SelenoHost. This is the entry point into Seleno that is used to run and configure it. A number of new features have been added to its application configurator (the Action<IAppConfigurator> parameter to the SelenoHost.Run method).

Namely, we have added the following new configuration features:

  • Ability to explicitly specify the initial page for a test and initialise the first page object in one line of code
  • Ability to specify a Castle.Core logger factory to intercept our internal logging
  • Ability to more easily specify a non-Firefox web browser
  • Ability to specify a deployed web application out-of-the-box
  • Ability to more explicitly specify your MVC routes
  • Ability to override the minimum (implicit) wait timeout inside of Selenium

For more information about each of these see the following sections.

Specify the initial page for a test

There is a new set of methods on SelenoHost to both ensure that tests start on an explicit page (rather than whatever page the last test finished on) as well as instantiate the initial page object:

// Navigate to the homepage of the web application (default) 
// or a URL relative to the root of the web application
SelenoHost.NavigateToInitialPage<TPage>(string relativeUrl = "")
// Navigate to a URL as determined by the referenced controller action
SelenoHost.NavigateToInitialPage<TController, TPage>(Expression<Action<TController>> action)

Example usages of these new methods:

HomePage page = SelenoHost.NavigateToInitialPage<HomePage>();
WeatherPage weatherPage = SelenoHost.NavigateToInitialPage<WeatherPage>("weather/2643743");
MovieListPage page = SelenoHost.NavigateToInitialPage<MoviesController, MovieListPage>(x => x.Index());

In order to ensure all page objects are initialised correctly, we no longer support instantiating Page Objects with the new keyword (i.e. you must use the NavigateToInitialPage method at the start of all your tests).

In order to use the MVC controller action expression overload you need to tell Seleno about your MVC routes so it can generate the URL. See the section below to find out more about how to do that.

Logging

We have introduced Castle.Core as a dependency for logging purposes. This provides great flexibility as it is easy to implement its interfaces for whatever logging framework you use and it has built in support for major logging providers such as NLog and log4net.

To configure a logging provider (by default it uses a Null logger) simply add it to the configuration when setting up Seleno. For example:

SelenoHost.Run(..., c => c.UsingLoggerFactory(new ConsoleFactory()));

Support for Internet Explorer and Chrome web drivers

We have added support for testing with Internet Explorer and Chrome. By default Seleno uses FireFox, but the fluent configuration allows for providing a delegate to create any Selenium RemoteWebDriver. For convenience Seleno provides a factory that produces a default instance of FireFox, Internet Explorer and Chrome webdrivers.

SelenoHost.Run(..., c => c.WithRemoteWebDriver(BrowserFactory.Chrome));
SelenoHost.Run(..., c => c.WithRemoteWebDriver(BrowserFactory.InternetExplorer32));
SelenoHost.Run(..., c => c.WithRemoteWebDriver(BrowserFactory.InternetExplorer64));

Both Chrome and Internet Explorer require server executables to be present in the test project bin directory in order for their WebDrivers to work. For the moment you need to manually do this, but we will hopefully be able to make this automatic in the future. You can find out more information here:

Added support for internet applications

We had a Seleno user get in touch with us to ask how she could test a deployed website, rather than a Visual Studio project. Seleno could already achieve that with the WithWebServer configuration method, but we thought it was a useful scenario to support out of the box. To test deployed applications you just need to use the InternetWebServer class in the configuration method and provide the base URL for the website.

SelenoHost.Run(..., c => c.WithWebServer(new InternetWebServer("http://www.bbc.co.uk")));

Specifying MVC routes

Previously Seleno used the global RouteTable.Routes object when determining the URLs for MVC routes. We didn't like this because using the global object might affect other code you have running your test assembly. Furthermore, providing a configuration method to specify the routes is more in keeping with how the rest of Seleno is globally configured and is thus more discoverable to new users. To use the configuration you need to use the new WithRouteConfig method:

SelenoHost.Run(..., c => c.WithRouteConfig(RouteConfig.RegisterRoutes));

Overriding minimum (implicit) wait timeout

When looking for an element on the page that you know has a likelihood of not being present it can be really annoying to see the test hang for 10 seconds while the Selenium Driver waits for its implicit timeout. There is a way to override this timeout, so we thought we would give you the flexibility to easily accomplish this to give you more control. This, combined with the fact that all Seleno methods that might need to wait have an optional maxWait parameter, gives you a lot of flexibility to tweak your tests to be more reliable, but also as fast as possible. You can change from the default 10s implicit timeout by using the new WithMinimumWaitTimeoutOf configuration method:

SelenoHost.Run(..., c => c.WithMinimumWaitTimeoutOf(TimeSpan.FromSeconds(2)));

Continuous Integration support

Previously, there were problems running Seleno tests in TeamCity (in particular if you had dotCover code coverage running) whereby the test run would hang indefinitely. These problems have been resolved and we can now comfortably say that TeamCity (and by association other CI servers) are supported for Seleno. In fact, we have a TeamCity server that builds Seleno and publishes it to NuGet (this includes running a bunch of acceptance tests) so if it breaks in the future we will know!

HTML Control Model

One of our main contributors, Franck Theolade, has put a lot of work into an HTML control model which supports all of the W3C form controls. The Page Object design pattern is not limited to modelling whole pages, but can also include modelling parts of a page, such as menus, or even controls, as is the case here. Just like with page objects, the control implementation is responsible for interacting with the page and retrieving and setting its value.

For example you can make use of the controls directly within your page objects by using code like this:

var textboxValue = HtmlControlFor<TextBox>("StringField").Value;
// Or, if you are using strongly-typed view models
var textboxValue = HtmlControlFor<TextBox>(m => m.StringField).Value;

The following implementations ship with Seleno 0.4 (and can be used with the HtmlControlFor method):

  • TextBox
  • TextArea
  • DropDown
  • CheckBox
  • RadioButtonGroup

We cannot possibly support every vendor control, so the goal was to support standard controls out-of-the-box and then make the control model easy to extend for custom controls by implementing the IHtmlControl interface.

public interface IHtmlControl
{
    string Id { get; }
    string Name { get; }
    string Title { get; set; }
    bool ReadOnly { get; set; }
    bool Disabled { get; set; }

    TReturn AttributeValueAs<TReturn>(string attributeName);
    void SetAttributeValue<TValue>(string attributeName, TValue value);
}

The easiest way to do this is to inherit from the HtmlControl abstract class, but if you need more flexibility then you can inherit from the UiComponent class and implement the IHtmlControl interface. To illustrate, here is the TextArea implementation:

public class TextArea : HTMLControl
{
    public string Content
    {
        get
        {
            return Find().Element(By.Id(Id)).GetAttribute("value");
        }
        set
        {
            var scriptToExecute = string.Format(@"$(""#{0}"").text(""{1}"")", Id, value.ToJavaScriptString());
            Execute().ExecuteScript(scriptToExecute);
        }
    }
}

Common usage scenarios with HTML controls have been catered for by using the new HTML control model internally to provide the following methods within page objects with a strongly-typed view model (i.e. Page<T>):

  • Read().
    • bool CheckBoxValue<TProperty>(Expression<Func<TViewModel, TProperty>> checkBoxPropertySelector, TimeSpan maxWait = default(TimeSpan))
    • string TextboxValue<TProperty>(Expression<Func<TViewModel, TProperty>> textBoxPropertySelector, TimeSpan maxWait = default(TimeSpan))
    • string SelectedOptionTextInDropDown<TProperty>(Expression<Func<TViewModel, TProperty>> dropDownSelector, TimeSpan maxWait = default(TimeSpan))
    • TProperty SelectedOptionValueInDropDown<TProperty>(Expression<Func<TViewModel, TProperty>> dropDownSelector, TimeSpan maxWait = default(TimeSpan))
    • TProperty SelectedButtonInRadioGroup<TProperty>(Expression<Func<TViewModel, TProperty>> radioGroupButtonSelector, TimeSpan maxWait = default(TimeSpan))
    • bool HasSelectedRadioButtonInRadioGroup<TProperty>(Expression<Func<TViewModel, TProperty>> radioGroupButtonSelector, TimeSpan maxWait = default(TimeSpan))
    • string TextAreaContent(Expression<Func<TViewModel, string>> textAreaPropertySelector, TimeSpan maxWait = default(TimeSpan))
  • Input().
    • void ReplaceInputValueWith<TProperty>(Expression<Func<TModel, TProperty>> propertySelector, TProperty inputValue)
    • void ReplaceInputValueWith(string inputName, string value)
    • void TickCheckbox(Expression<Func<TModel, bool>> propertySelector, bool isTicked)
    • void UpdateTextAreaContent(Expression<Func<TModel, string>> textAreaPropertySelector, string content, TimeSpan maxWait = default(TimeSpan))
    • void SelectByOptionValueInDropDown<TProperty>(Expression<Func<TModel, TProperty>> dropDownSelector,TProperty optionValue)
    • void SelectByOptionTextInDropDown<TProperty>(Expression<Func<TModel, TProperty>> dropDownSelector,string optionText)
    • void SelectButtonInRadioGroup<TProperty>(Expression<Func<TModel, TProperty>> radioGroupButtonSelector, TProperty buttonValue)

Internal improvements

Continuous Integration

Since Rob Moore joined TestStack he has had a major impact on Seleno. In particular, he has done some very cool things with continuous integration and automated deployments to allow our TeamCity continuous integration server to integrate with our github repository and to give us the ability to safely, confidently and quickly deploy new packages to NuGet - we are hoping to have a much tighter release cycle for NuGet packages going forward. Many thanks also need to go to Jake Ginnivan for all his help with this given he has graciously set up the CI infrastructure! One really big time saver is that we are now taking advantage of TeamCity's ability to monitor github for pull requests and automatically build and report whether or not it is OK to merge the changes:

TeamCity github integration

Inversion of Control

We have replaced the Funq inversion of control container with Autofac. We are just using it for our internal configuration currently and have not had the need to expose it externally. We use ILMerge so that it is not a dependency when you download the NuGet package. This allows us to manage lifetimes much more easily within the core code and leaves us with a more maintainable code base.

Public API

We have tweaked our public API significantly in order to make it less confusing and more intention revealing. This involes setting a lot of classes that contain internal implementations to internal. For a full list, please see the BREAKING_CHANGES.md file.

Extensive test coverage

Our unit and integration test coverage of the codebase has been significantly improved. This gives us greater confidence n the codebase. We strongly feel that Seleno is "production-ready" now and as such will be swiftly moving towards a 1.0 release.

Go get it!

As usual, you can get the latest version of Seleno on NuGet, or check out our github repository for the latest source code. Let us know what you think, or if there are any features that you would like to see. Feel free to add an issue or pull request - the more community interaction we get the better we can make Seleno!

About Michael Whelan

Michael Whelan is a Technical Lead with over 20 years’ experience in building (and testing!) applications on the Microsoft stack. He is passionate about applying agile development practices, such as BDD and continuous delivery, to agile processes. These days his primary focus is ASP.Net MVC Core and Azure. He contributes to a number of open source frameworks through TestStack.

comments powered by Disqus
Google

Google