FluentAutomation


Brandon Stirnaman

FluentAutomation Creator
Simple, fluent automated testing for web applications.



F14N Founder
Fast, easy automation test creation, execution and management.

Fluent v2.3 Released

I've finally had time to finish up a few features that I have had cooking for awhile and while doing that added some more!

Changelog (details after):

  • First-class support for PageObjects
  • New Expect behavior, Assert added (not immediately breaking)
  • Action and Expect chaining - More fluent!
  • Access to WebDriver/WatiN.IE
  • Drag/Drop To/From offsets
  • Selenium/ChromeDriver/IEDriver/PhantomJS version updates
  • Removed deprecated methods - .Quickly()
  • Removed legacy projects - PhantomJS standlone (replaced by PhantomJs support in Selenium), Remote, Node

Ok.. Now for the long post. Lots of features to discuss!

PageObjects

Often handled by individual developers for their projects, page objects are a common way to organize your tests for re-use of actions and selectors.

I wanted to provide a pattern to help people get started and make it part of the framework. This is the first stab at it, a bit more clunky than I'd like but its ready for use!

Wall of code:

public class BingSearchPage : PageObject<BingSearchPage>  
{
    public BingSearchPage(FluentTest test)
        : base(test)
    {
        Url = "http://bing.com/";
        At = () => I.Expect.Exists(SearchInput);
    }

    public BingSearchResultsPage Search(string searchText)
    {
        I.Enter(searchText).In(SearchInput);
        I.Press("{ENTER}");
        return this.Switch<BingSearchResultsPage>();
    }

    private const string SearchInput = "input[title='Enter your search term']";
}

public class BingSearchResultsPage : PageObject<BingSearchResultsPage>  
{
    public BingSearchResultsPage(FluentTest test)
        : base(test)
    {
        At = () => I.Expect.Exists(SearchResultsContainer);
    }

    public BingSearchResultsPage FindResultUrl(string url)
    {
        I.Expect.Exists(string.Format(ResultUrlLink, url));
        return this;
    }

    private const string SearchResultsContainer = "#b_results";
    private const string ResultUrlLink = "a[href='{0}']";
}

public class SampleTest : FluentTest  
{
    public SampleTest()
    {
        FluentAutomation.Settings.DefaultWaitUntilTimeout = TimeSpan.FromMilliseconds(1500);
        SeleniumWebDriver.Bootstrap(SeleniumWebDriver.Browser.Chrome);
    }

    [Fact]
    public void SearchForFluentAutomation()
    {
        new BingSearchPage(this)
            .Go()
            .Search("FluentAutomation")
            .FindResultUrl("http://fluent.stirno.com/blog/FluentAutomation-scriptcs/");
    }
}

Here we have two page objects and a test using them both. Let's look at the constructor:

public BingSearchPage(FluentTest test)  
    : base(test)
{
    Url = "http://bing.com/";
    At = () => I.Assert.Exists(SearchInput);
}

These base properties of Url and At are used to provide basic validation and navigation functionality. If Url is set, the Go() method can be called to navigate to that page. If the At property is set (takes an Action) when the page is navigated, it will execute as a WaitUntil call to make sure any page methods execute on the correct page and/or state.

Define any actions you need directly on your PageObject, just make sure to return this and you'll have a nice fluent page syntax.

In our example, the Search function of BingSearchPage takes us to BingSearchResultsPage. We recognize this by returning that page with:

return this.Switch<BingSearchResultsPage>();  

The goal is to make sure your pages don't include selectors or actions for other pages. You used the search box from BingSearchPage but you'll process results from BingSearchResultsPage. Nice seperation of actions/selectors. When Switch<T> is called, it will again run the validation Action (this.At) if set. It does not trigger page navigation automatically (in this example, the act of searching changed the page).
The new PageObject has one strange requirement - a generic T reference to itself! This is to allow the chaining syntax from the base methods. Returning a base PageObject wouldn't be terribly helpful.
All of this helps create much more maintainable test suites. Your tests become short chains of method calls. See our example test:

[Fact]
public void SearchForFluentAutomation()  
{
    new BingSearchPage(this)
        .Go()
        .Search("FluentAutomation")
        .FindResultUrl("http://fluent.stirno.com/blog/FluentAutomation-scriptcs/");
}

I think this is a good direction for us to grow. We want the community to help us find more common functionality to add to PageObjects. If you have ideas (about any of this stuff), lets talk!

Expect Changes

We have a non-breaking (for now) change to Expects. When I first added Expect bits, I felt it was nicer to read I.Expect vs I.Assert but over time I've seen many people implement their own passive expects. In response to this we now have:

I.Expect.Count(1).Of("#id");  
I.Assert.Count(1).Of("#id");  

The I.Expect.* methods will log messages and let the test continue. I.Assert.* methods will actively throw and stop tests. They have exactly the same methods.

This behavior change is enabled via:

FluentAutomation.Settings.ExpectIsAssert = true;  

For now the default value is false but in the next release it will be true. To be entirely clear: When set to true Expects will stop throwing exceptions. We didn't want to break existing test suites before people had time to add this setting. Set this in your project to make sure your tests continue to work as you expect in the future!

The method of logging is controlled by providing an Action<FluentExpectFailedException>. This is the default callback:

FluentAutomation.Settings.ExpectFailedCallback = (ex) =>  
{
    System.Diagnostics.Trace.WriteLine(ex);
};

Overall this should provide a standard way of getting test-level Expect logging as well as proper Assert behavior that many users desire.

Fluent Action/Expect Chaining

I'm not sure why I haven't done this earlier but.. FluentAutomation can now provide more fluent tests! Inspired by the way we setup the PageObjects, all terminating methods (take an action directly rather than returning an object) have been changed to return the appropriate syntax provider to allow chaining.

This gets us to tests that can look more like this:

I.Open("http://automation.apphb.com/forms")  
    .Select("Motorcycles").From(".liveExample tr select:eq(0)")
    .Select(2).From(".liveExample tr select:eq(1)")
    .Enter(6).In(".liveExample td.quantity input:eq(0)")
    .Expect
        .Text("$197.72").In(".liveExample tr span:eq(1)")
        .Value(6).In(".liveExample td.quantity input:eq(0)");

Terminating actions return the top level syntax provider, allow you to chain more complex actions. Termination Expects or Asserts return the Expect syntax provider to chain more expects. This helps lead down the Arrange/Act/Assert test workflow nice and cleanly.

This is a non-breaking change that just provides some syntactical sugar for those who decide to use it -- I happen to really like it. What do you think?

Access to WebDriver/WatiN.IE

The top issue on our UserVoice site -- users want access to the underlying provider. Due to some other decisions made to help provide features like our session support, this was more difficult than most would expect.

Despite this, we have managed to get it in this release! It is now accessible via the FluentTest class like so:

public class SampleTest : FluentTest  
{
    public void TestMethod()
    {
        var webDriver = (IWebDriver)this.Provider;
    }
}

I threw in some sugar for those who might access it alot, across many methods:

public class SampleTest : FluentTest<IWebDriver>  
{
    public void TestMethod()
    {
        var webDriver = this.Provider;
    }
}

Just provide the class/interface via the generic version of FluentTest -- this.Provider will be cast (or null if the cast fails) and available for you to do your worst.

It should be noted that this feature is basically incompatible with the multi-browser testing that is enabled when you pass multiple Browser.* values to the Bootstrap method. We will throw an exception if you attempt to access this.Provider in that scenario. This is because there are multiple drivers in play and the complexity of providing that currently outweighs the benefits.

Requested by Antoine: http://fluentapi.uservoice.com/forums/182790-general/suggestions/3376567-allow-access-to-the-driver-under-fluent-automation

Drag/Drop changes

Both sides of the Drag/Drop functionality (Drag and To) now accept offset coordinates to make it easier to handle any operation necessary.

Requested by Troy: http://fluentapi.uservoice.com/forums/182790-general/suggestions/3327831-support-source-destination-offsets-for-drag-and-dr

Selenium Updates

We're now on Se 2.39 in the package. You can usually update this without issue on your own but I move us to the newest stable version with each update.

Also included are the latest ChromeDriver.exe, IEDriverServer32/64.exe and PhantomJS.exe to fix some issues with the latest chrome and get us the best bits.

Cleanup

We have stopped supporting the standalone FluentAutomation.PhantomJs implementation as it was incomplete and there is official support in the Selenium package for Phantom now. It can be used by bootstrapping appropriately:

FluentAutomation.SeleniumWebDriver.Bootstrap(SeleniumWebDriver.Browser.PhantomJs);  

Long long ago, I.Enter("wat").Quickly() was the way to enter values in form fields without emulating keypresses. We introduced .WithoutEvents() to be more obvious about what was happening. Users were just adding .Quickly to everything causing tests to not act as expected. I marked it as deprecated when the replacement went in. Now its gone completely.

There were a few other projects that were remnants of some server-side execution we had been working on (Remote, Node, etc) that were never released but lingering in the repo. These are gone now but will be replaced with our newer solutions in this space soon.

Documentation!

The docs are a bit out of date at the moment. I will be updating them this week as time allows. Hit me up on twitter (@stirno) for any questions. I'd be glad to help wherever possible!

Thanks for reading