Anthony Chu Contact Me

End-to-End Testing Angular Apps with NUnit and SpecFlow using Protractor.NET

Saturday, November 15, 2014

Protractor is an end-to-end test framework for AngularJS. The official version is built on Node.js and Selenium WebDriverJS. While there is great support for NPM and task runners like Grunt and Gulp in Visual Studio (via extensions in VS 2013, and built into VS 2015), there is currently no way to integrate Protractor tests into Test Explorer (that I know of).

Thankfully there is Protractor.NET, a .NET port of Protractor built on top of Selenium WebDriver for .NET. It allows us to write Angular UI tests using .NET testing frameworks such as NUnit, and arguably produces more readable tests because there’s no need to use promises.

It’s worthwhile to mention that we can write tests for Angular apps using WebDriver alone. Often this is done by inserting waits to pause execution until Angular is finished doing its work. The advantage of using Protractor (and Protractor.NET) is that it understands Angular’s digest cycle and blocks until the digest cycle has finished. This allows us to write much cleaner test code.

To use Protractor.NET, install the Protractor NuGet package.

Install-Package Protractor

There is a sample calculator at https://github.com/juliemr/protractor-demo that we’ll be writing our tests against.

We’ll first create the setup and teardown methods in our test…

[TestFixture]
public class BasicTests
{
    const string URL = "http://juliemr.github.io/protractor-demo/";

    IWebDriver driver;
    NgWebDriver ngDriver;

    [SetUp]
    public void Setup()
    {
        driver  = new ChromeDriver();
        driver.Manage().Timeouts().SetScriptTimeout(TimeSpan.FromSeconds(10));
        ngDriver = new NgWebDriver(driver);
    }

    [TearDown]
    public void Teardown()
    {
        ngDriver.Quit();
    }
}

It looks a lot like the same NUnit WebDriver tests we’re used to writing. The only addition is we’re creating an NgWebDriver that wraps the IWebDriver (ChromeDriver, in this case). This provides the extra Angular-specific functionality from Protractor.NET.

All we have to do now is write the test. Let’s test adding two numbers…

[Test]
public void Basic_AddOneAndTwo_ShouldBeThree()
{
    ngDriver.Url = URL; // navigate to URL

    ngDriver.FindElement(NgBy.Input("first")).SendKeys("1");
    ngDriver.FindElement(NgBy.Input("second")).SendKeys("2");
    ngDriver.FindElement(By.Id("gobutton")).Click();

    var latestResult = ngDriver.FindElement(NgBy.Binding("latest")).Text;
    latestResult.Should().Be("3");
}

ngDriver.Url = URL; navigates to the calculator page and waits until Angular is loaded. The rest is standard WebDriver code, with the exceptions of NgBy.Input() and NgBy.Binding(). These allow us to target inputs and bindings bound to scope properties on the page.

Note that I’m using the Fluent Assertions library.

Run the test. Chrome should fire up and the test should pass in a few seconds…

image

That’s all there is to it. We can then refactor our tests to use the Page Object pattern. We’ll create a class called SuperCalculatorPage that encapsulates interactions with the page…

public class SuperCalculatorPage
{
    private NgWebDriver _ngDriver;

    public SuperCalculatorPage(IWebDriver driver, string url)
    {
        _ngDriver = new NgWebDriver(driver);
        _ngDriver.Url = url;
    }

    public string LatestResult
    {
        get { return _ngDriver.FindElement(NgBy.Binding("latest")).Text; }
    }

    public void Add(string first, string second)
    {
        DoMath(first, second, "+");
    }

    public void Subtract(string first, string second)
    {
        DoMath(first, second, "-");
    }

    public void Multiply(string first, string second)
    {
        DoMath(first, second, "*");
    }

    public void Divide(string first, string second)
    {
        DoMath(first, second, "/");
    }

    private void DoMath(string first, string second, string op)
    {
        SetFirst(first);
        SetSecond(second);
        SetOperator(op);
        ClickGo();
    }

    private void SetFirst(string number)
    {
        var first = _ngDriver.FindElement(NgBy.Input("first"));
        first.Clear();
        first.SendKeys(number);
    }

    private void SetSecond(string number)
    {
        var second = _ngDriver.FindElement(NgBy.Input("second"));
        second.Clear();
        second.SendKeys(number);
    }

    private void SetOperator(string op)
    {
        var operatorSelect = new SelectElement(_ngDriver.FindElement(NgBy.Select("operator")));
        operatorSelect.SelectByText(op);
    }

    private void ClickGo()
    {
        _ngDriver.FindElement(By.Id("gobutton")).Click();
    }
}

And the tests are now even more readable…

[TestFixture]
public class PageObjectTests
{
    const string URL = "http://juliemr.github.io/protractor-demo/";

    IWebDriver driver;
    SuperCalculatorPage page;

    [SetUp]
    public void Setup()
    {
        driver = new ChromeDriver();
        driver.Manage().Timeouts().SetScriptTimeout(TimeSpan.FromSeconds(10));
        page = new SuperCalculatorPage(driver, URL);
    }

    [TearDown]
    public void Teardown()
    {
        driver.Quit();
    }

    [Test]
    public void PageObject_AddOneAndTwo_ShouldBeThree()
    {
        page.Add("1", "2");
        page.LatestResult.Should().Be("3");
    }

    [Test]
    public void PageObject_ThreeSubtractOne_ShouldBeTwo()
    {
        page.Subtract("3", "1");
        page.LatestResult.Should().Be("2");
    }
}

We can take this even further by using SuperCalculatorPage in a SpecFlow test. Here’s a feature file in Gherkin syntax…

Feature: Simple Math
    In order to solve simple math problems
    As a user
    I want to perform simple arithmetic on two numbers

Scenario Outline: Add two numbers
    Given I have a new calculator
    When I add <first> and <second>
    Then the latest result should be <expectedresult>

    Examples:
    | first | second | expectedResult |
    | 2     | 3      | 5              |
    | -1    | 1      | 0              |

Scenario Outline: Divide two numbers
    Given I have a new calculator
    When I divide <first> by <second>
    Then the latest result should be <expectedresult>

    Examples:
    | first | second | expectedResult |
    | 2     | 2      | 1              |
    | 1     | 0      | Infinity       |

And here is the C# steps implementation for the feature above…

[Binding]
public class SimpleMathSteps
{
const string URL = "http://juliemr.github.io/protractor-demo/";

IWebDriver driver;
SuperCalculatorPage page;

[Before]
public void Setup()
{
    driver = new ChromeDriver();
    driver.Manage().Timeouts().SetScriptTimeout(TimeSpan.FromSeconds(10));
}

[After]
public void Teardown()
{
    driver.Quit();
}

[Given(@"I have a new calculator")]
public void GivenIHaveANewCalculator()
{
    page = new SuperCalculatorPage(driver, URL);
}

[When(@"I add (.*) and (.*)")]
public void WhenWhenIAddAnd(string first, string second)
{
    page.Add(first, second);
}

[When(@"I divide (.*) by (.*)")]
public void WhenIDivideBy(string first, string second)
{
    page.Divide(first, second);
}

[Then(@"the latest result should be (.*)")]
public void ThenTheResultShouldBe(string expectedResult)
{
    page.LatestResult.Should().Be(expectedResult);
}

The full source code can be found at https://github.com/anthonychu/Protractor-Net-Demo.