End 2 End test automation framework using C# dot net core with GitHub Actions & Azure DevOps Pipeline

Get started with end 2 end automation framework using dot net core, C#, Specflow, nunit, GitHub Actions for your next UI based test automation project.

Website used in this example - http://automationpractice.com/

Download complete code from GitHub repo <<HERE>>

Final project structure -

  • Base
    • AppSettings.cs
    • Settings.cs
  • Features
    • Order_TShirt.feature
    • Update_Profile.feature
  • Helpers
    • GenericHelper.cs
  • Hooks
    • TestInitialize.cs
  • PageObject
    • CategoryPage.cs
    • IndexPage.cs
    • MyAccountPage.cs
  • Steps
    • CategorySteps.cs
    • CommonSteps.cs
    • IndexSteps.cs
    • MyAccountSteps.cs
  • appSettings.json
  • dotnet.yml
  • azure-pipelines.yml

Base Files

Contains all the functionality which is required to run tests, like setting up of test data, configurations, pre-conditions, starting browsers with options, timeouts

AppSettings.cs

using Newtonsoft.Json;
using OpenQA.Selenium;
using OpenQA.Selenium.Chrome;
using System;
using System.IO;

namespace Com.Test.AbhinavSharma.Base

{
    public class AppSettings
    {
        public string baseUrl { get; set; }
        public string userName { get; set; }
        public string password { get; set; }
        public IWebDriver driver { get; set; }
     }
}

Settings.cs 

using Com.Test.AbhinavSharma.PageObject;
using Com.Test.AbhinavSharma.Steps;
using Newtonsoft.Json;
using OpenQA.Selenium;
using OpenQA.Selenium.Chrome;
using System;
using System.IO;

namespace Com.Test.AbhinavSharma.Base

{
    public class Settings
    {
        public AppSettings configurations { get; set; }
        public IndexPage indexPage;
        public MyAccountPage myAccountPage;

        Settings()
        {
            //Read settings from JSON file and parse to class object 
            using (StreamReader r = new StreamReader("appSettings.json"))
            {
                string json = r.ReadToEnd();
                configurations = JsonConvert.DeserializeObject<AppSettings>(json);
            }

            configurations.driver = new ChromeDriver();
            configurations.driver.Manage().Timeouts().ImplicitWait = TimeSpan.FromSeconds(3000);
            configurations.driver.Manage().Window.Maximize();
        }

    }
}

Features

User interactions are defined which should reflect how someone interacts with the webpage.

Order_TShirt.feature

Feature: Order_TShirt
	Order T Shirt from website

Background: common login steps
	Given mark has a valid account with "xxx"
	And performs login with user-name as "xxx" and password as "xxx"

@sanity
Scenario: Order T-Shirt and verify in order history
	Given mark wants order a T-Shirt for his wife
	When mark selects & completes the purchase
	Then mark should see purchased T-Shirt in order history

Update_Profile.feature

Feature: Update_Profile
	Update personal information such as first name in My Account section

#Credentials will be picked from appSettings when "xxx"
Background: common login steps
	Given mark has a valid account with "xxx"
	And performs login with user-name as "xxx" and password as "xxx"

#A random First Name will be used from Faker() API call when xxx
@sanity
Scenario: Update users first name in the My Account section with any random name
	Given mark wants to update his first name
	When clicks on My personal information 
	And updates First name as "xxx"
	Then mark should see updated first name in the profile section

@sanity
Scenario: Update users first name in the My Account section with a specific name
	Given mark wants to update his first name
	When clicks on My personal information 
	And updates First name as "TestFirstName"
	Then mark should see updated first name in the profile section

Helpers

Any reusable functions, libraries, or code which will be used at multiple places, and requires customisation should be part of Helpers.

GenericHelper.cs

using System;

namespace Com.Test.AbhinavSharma.Helpers
{
    class GenericHelper
    {
        public static void OutputFormattedAs(string format
            , string title
            , string content
            )
        {
            if (format.Equals(""))
            {
                Console.WriteLine("_____________________________________________________________");
                Console.WriteLine(title + " : ");
                Console.WriteLine(content);
                Console.WriteLine("=============================================================");
            }
        }
    }
}

Hooks

Useful to repeat steps which are to executed before and after all tests, here inside BeforeScenario, page is initialised with settings, so that it can be used while test execution is in place, Similarly once the execution is completed, AfterScenario is used to quit the browser.

TestInitialize.cs

using System;
using System.Configuration;
using TechTalk.SpecFlow;
using Com.Test.AbhinavSharma.Base;
using System.IO;
using System.Collections.Generic;
using Newtonsoft.Json;
using OpenQA.Selenium;
using System.Text;
using OpenQA.Selenium.Chrome;
using Com.Test.AbhinavSharma.PageObject;
using Com.Test.AbhinavSharma.Steps;

namespace Com.Test.AbhinavSharma.Hooks
{
    [Binding]
    class TestInitialize
    {

        public Settings _settings;
        TestInitialize(Settings settings) => _settings = settings;

        [BeforeScenario]
        public void TestSetup()
        {
            _settings.indexPage = new IndexPage(_settings);
            _settings.myAccountPage = new MyAccountPage(_settings);

        }

        [AfterScenario]
        public void TestTearDown()
        {
            _settings.configurations.driver.Quit();

        }
    }
}

PageObject

For easy maintenance, all pages of any website should be added as a separate class with their locators to identity elements on the page

CategoryPage.cs

using Com.Test.AbhinavSharma.Base;
using NUnit.Framework;
using OpenQA.Selenium;
using OpenQA.Selenium.Interactions;
using SeleniumExtras.PageObjects;
using System;

namespace Com.Test.AbhinavSharma.PageObject
{
    public class CategoryPage
    {

        private IWebDriver driver;

        [FindsBy(How = How.XPath, Using = "//*[@id=\"block_top_menu\"]/ul/li[3]/a")]
        private IWebElement linkTshirt;

        [FindsBy(How = How.XPath, Using = "//*[@id=\"center_column\"]/ul/li/div/div[1]/div/a[1]/img")]
        private IWebElement hoverShirt;

        [FindsBy(How = How.XPath, Using = "//span[contains(text(),'Add to cart')]")]
        private IWebElement btnAddToCart;

        [FindsBy(How = How.XPath, Using = "//*[@id=\"layer_cart\"]/div[1]/div[2]/div[4]/a/span")]
        private IWebElement btnProceedToCheckout;


        [FindsBy(How = How.XPath, Using = "//*[@id=\"center_column\"]/p[2]/a[1]/span")]
        private IWebElement btnProceedToCheckoutAfterSummary;


        [FindsBy(How = How.XPath, Using = "//*[@id=\"center_column\"]/form/p/button/span")]
        private IWebElement btnProceedToCheckoutAfterAddress;


        [FindsBy(How = How.XPath, Using = "//input[@id='cgv']")]
        private IWebElement checkBoxTermsOfService;


        [FindsBy(How = How.XPath, Using = "//*[@id=\"form\"]/p/button/span")]
        private IWebElement btnProceedToCheckoutAfterShipping;


        [FindsBy(How = How.XPath, Using = "//*[@id=\"HOOK_PAYMENT\"]/div[1]/div/p/a")]
        private IWebElement btnPayByWire;

        //*[@id="cart_navigation"]/button/span        
        [FindsBy(How = How.XPath, Using = "//*[@id=\"cart_navigation\"]/button/span")]
        private IWebElement btnConfirmMyOrder;

        public Settings _settings;
        public CategoryPage(Settings settings) => _settings = settings;

        public CategoryPage(IWebDriver driver)
        {
            this.driver = driver;
            PageFactory.InitElements(driver, this);
        }

        public CategoryPage navigateToTShirtCategory()
        {


            linkTshirt.Click();
            Actions action = new Actions(driver);
            action.MoveToElement(hoverShirt).Perform();
            btnAddToCart.Click();
            //Assert  //TODO
            return new CategoryPage(driver);

        }

        public CategoryPage proceedToCheckout()
        {
            btnProceedToCheckout.Click();
            btnProceedToCheckoutAfterSummary.Click();
            btnProceedToCheckoutAfterAddress.Click();
            checkBoxTermsOfService.Click();
            btnProceedToCheckoutAfterShipping.Click();
            btnPayByWire.Click();
            btnConfirmMyOrder.Click();
            return new CategoryPage(driver);

        }
    }
}

IndexPage.cs

using Com.Test.AbhinavSharma.Base;
using Com.Test.AbhinavSharma.Helpers;
using NUnit.Framework;
using OpenQA.Selenium;
using SeleniumExtras.PageObjects;
using System;

namespace Com.Test.AbhinavSharma.PageObject
{
    public class IndexPage
    {
        private IWebDriver driver;

        [FindsBy(How = How.XPath, Using = "//a[contains(text(),'Sign in')]")]
        private IWebElement linkSignIn;

        [FindsBy(How = How.XPath, Using = "//input[@id='email']")]
        private IWebElement txtBoxEmail;

        [FindsBy(How = How.XPath, Using = "//input[@id='passwd']")]
        private IWebElement txtBoxPassword;

        [FindsBy(How = How.XPath, Using = "//span[contains(text(),'My personal information')]")]
        private IWebElement linkMyPersonalInformation;

        [FindsBy(How = How.XPath, Using = "//span[contains(text(),'Order history and details')]")]
        private IWebElement linkOrderHistoryAndDetails;

        [FindsBy(How = How.XPath, Using = "//*[@id=\"header\"]/div[2]/div/div/nav/div[1]/a")]
        private IWebElement linkMyAccountSection;

        //Display Order Reference
        
        [FindsBy(How = How.XPath, Using = "//tbody/tr[1]/td[1]")]
        private IWebElement txtOrderReferenceNumber;

        [FindsBy(How = How.XPath, Using = "//tbody/tr[1]/td[2]")]
        private IWebElement txtOrderDate;
          
        [FindsBy(How = How.XPath, Using = "//tbody/tr[1]/td[3]")]
        private IWebElement txtOrderTotalPrice;
          
        [FindsBy(How = How.XPath, Using = "//tbody/tr[1]/td[4]")]
        private IWebElement txtOrderPaymentType;
           
        [FindsBy(How = How.XPath, Using = "//tbody/tr[1]/td[5]")]
        private IWebElement txtOrderStatus;


        public Settings _settings;
        public IndexPage(Settings settings) => _settings = settings;

        public IndexPage(IWebDriver driver)
        {
            this.driver = driver;
            PageFactory.InitElements(driver, this);
        }

        public IndexPage verifyPlacedOrderDetails()
        {

            GenericHelper.OutputFormattedAs("", "Expected Reference Number"
                , txtOrderReferenceNumber.Text);
            GenericHelper.OutputFormattedAs("", "Expected Order Date"
                , txtOrderDate.Text);
            GenericHelper.OutputFormattedAs("", "Expected Total Price"
                , txtOrderTotalPrice.Text);
            GenericHelper.OutputFormattedAs("", "Expected Payment Type"
                , txtOrderPaymentType.Text);
            GenericHelper.OutputFormattedAs("", "Expected Order Status"
                , txtOrderStatus.Text);

            //Assert
            Assert.That(txtOrderReferenceNumber.Text.Length, Is.EqualTo(9));
            Assert.That(txtOrderDate.Text, Is.EqualTo(DateTime.Today.ToString("MM/dd/yyyy")));
            Assert.That(txtOrderTotalPrice.Text, Is.EqualTo("$18.51"));
            //Assert.That(txtOrderPaymentType.Text, Is.EqualTo("Bank wire"));
            Assert.That(txtOrderStatus.Text, Is.EqualTo("On backorder"));



            return new IndexPage(driver);
        }

        public IndexPage goToPage(string url) {
            driver.Navigate().GoToUrl(url);
            //Assert  //TODO
            return new IndexPage(driver);

        }

        public IndexPage navigateToMyAccountSection()
        {
            linkMyAccountSection.Click();
            return new IndexPage(driver);
        }

        public IndexPage performLogin(
              string userName
            , string password)
        {
            linkSignIn.Click();
            txtBoxEmail.SendKeys(userName);
            txtBoxPassword.SendKeys(password);
            txtBoxPassword.SendKeys(Keys.Enter);
            //Assert  //TODO
            return new IndexPage(driver);
        }
        public IndexPage navigateToPersonalInformationInMyAccountSection()
        {
            linkMyPersonalInformation.Click();
            //Assert  //TODO
            return new IndexPage(driver);
        }
        public IndexPage navigateToOrderHistoryAndDetailsInMyAccountSection()
        {
            linkOrderHistoryAndDetails.Click();
            //Assert  //TODO
            return new IndexPage(driver);
        }



    }
}

MyAccountPage.cs

using NUnit.Framework;
using OpenQA.Selenium;
using SeleniumExtras.PageObjects;
using System;
using Bogus;
using Com.Test.AbhinavSharma.Base;
using Com.Test.AbhinavSharma.Helpers;

namespace Com.Test.AbhinavSharma.PageObject
{
    public class MyAccountPage
    {
        public Settings _settings;
        private IWebDriver driver;

        [FindsBy(How = How.XPath, Using = "//input[@id='firstname']")]
        private IWebElement txtBoxFirstName;
        
        [FindsBy(How = How.XPath, Using = "//input[@id='old_passwd']")]
        private IWebElement txtBoxCurrentPassword;

        [FindsBy(How = How.XPath, Using = "//p[contains(text(),'Your personal information has been successfully up')]")]
        private IWebElement txtUpdateConfirmation;


        public MyAccountPage(Settings settings) => _settings = settings;
        public MyAccountPage(IWebDriver driver)
        {
            this.driver = driver;
            PageFactory.InitElements(driver, this);
        }

        public MyAccountPage UpdateFirstName(
              string nameToBeUpdated
            , string password)
        {
            if (nameToBeUpdated.Equals("xxx"))
            {
                var f = new Faker();
                nameToBeUpdated = f.Person.FirstName;
                GenericHelper.OutputFormattedAs("", "New First Name selected for updating"
                    , nameToBeUpdated);
            }            

            txtBoxFirstName.Clear();
            txtBoxFirstName.SendKeys(nameToBeUpdated);
            txtBoxCurrentPassword.SendKeys(password);
            txtBoxCurrentPassword.SendKeys(Keys.Enter);


            return new MyAccountPage(driver);
        }

        public MyAccountPage VerifyProfileInformationIsUpdated()
        {
            Assert.That(txtUpdateConfirmation.Text,
                Is.EqualTo("Your personal information has been successfully updated."));

            return new MyAccountPage(driver);
        }


    }
}

Steps

This section contains, code behind files for the .feature files which are created in plain English (Gherkin), they elaborate the action taking place.

CategorySteps.cs

using OpenQA.Selenium;
using Com.Test.AbhinavSharma.Base;
using Com.Test.AbhinavSharma.PageObject;
using SeleniumExtras.PageObjects;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using TechTalk.SpecFlow;
using Com.Test.AbhinavSharma.Helpers;

namespace Com.Test.AbhinavSharma.Steps
{
    [Binding]
    public sealed class CategorySteps
    {

        public Settings _settings;
        public CategorySteps(Settings settings) => _settings = settings;

        CategoryPage categoryPage;

        [Given(@"mark wants order a T-Shirt for his wife")]
        public void GivenMarkWantsOrderAT_ShirtForHisWife()
        { 
        }
        [When(@"mark selects & completes the purchase")]
        public void WhenMarkSelectsCompletesThePurchase()
        {
            categoryPage = new CategoryPage(_settings.configurations.driver);
            categoryPage
                .navigateToTShirtCategory()
                .proceedToCheckout();
        }
    }
}

CommonSteps.cs

using OpenQA.Selenium;
using Com.Test.AbhinavSharma.Base;
using Com.Test.AbhinavSharma.PageObject;
using SeleniumExtras.PageObjects;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using TechTalk.SpecFlow;
using Com.Test.AbhinavSharma.Helpers;

namespace Com.Test.AbhinavSharma.Steps
{
    [Binding]
    public sealed class CommonSteps
    {

        public Settings _settings;
        public CommonSteps(Settings settings) => _settings = settings;

        public string inputWebsiteName = "";
        IndexPage indexPage;
        //CareersPage careersPage;


        [Given(@"mark has a valid account with ""(.*)""")]
        public void GivenMarkHasAValidAccountWith(
            string websiteName)
        {
            if (websiteName.Equals("xxx")) websiteName = _settings.configurations.baseUrl;
            GenericHelper.OutputFormattedAs("", "Website Name", websiteName);
            inputWebsiteName = websiteName;
        }


        [Given(@"performs login with user-name as ""(.*)"" and password as ""(.*)""")]
        public void GivenPerformsLoginWithUser_NameAsAndPasswordAs(
              string userName
            , string password)
        {
            if (userName.Equals("xxx")) userName = _settings.configurations.userName;
            if (password.Equals("xxx")) password = _settings.configurations.password;
            GenericHelper.OutputFormattedAs("", "user-name used for login", userName);
            GenericHelper.OutputFormattedAs("", "password used for login", password);
            
            indexPage = new IndexPage(_settings.configurations.driver);
            indexPage
                .goToPage(inputWebsiteName)
                .performLogin(userName, password);
        }
    }
}

IndexSteps.cs

using OpenQA.Selenium;
using Com.Test.AbhinavSharma.Base;
using Com.Test.AbhinavSharma.PageObject;
using SeleniumExtras.PageObjects;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using TechTalk.SpecFlow;
using Com.Test.AbhinavSharma.Helpers;

namespace Com.Test.AbhinavSharma.Steps
{
    [Binding]
    public sealed class IndexSteps
    {

        public Settings _settings;
        IndexSteps(Settings settings) => _settings = settings;

        IndexPage indexPage;

        [Given(@"mark wants to update his first name")]
        public void GivenMarkWantsToUpdateHisFirstName()
        { 
        }
        [When(@"clicks on My personal information")]
        public void WhenClicksOnMyPersonalInformation()
        {
            indexPage = new IndexPage(_settings.configurations.driver);
            indexPage.navigateToPersonalInformationInMyAccountSection();
        }
        [Then(@"mark should see purchased T-Shirt in order history")]
        public void ThenMarkShouldSeePurchasedT_ShirtInOrderHistory()
        {
            indexPage = new IndexPage(_settings.configurations.driver);
            indexPage.navigateToMyAccountSection()
                .navigateToOrderHistoryAndDetailsInMyAccountSection()
                .verifyPlacedOrderDetails();
        }
    }
}

MyAccountSteps.cs

using OpenQA.Selenium;
using Com.Test.AbhinavSharma.Base;
using Com.Test.AbhinavSharma.PageObject;
using SeleniumExtras.PageObjects;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using TechTalk.SpecFlow;
using Com.Test.AbhinavSharma.Helpers;

namespace Com.Test.AbhinavSharma.Steps
{
    [Binding]
    public sealed class MyAccountSteps
    {

        public Settings _settings;
        public MyAccountSteps(Settings settings) => _settings = settings;

        MyAccountPage myAccountPage;

        [When(@"updates First name as ""(.*)""")]
        public void WhenUpdatesFirstNameAs(
            string nameToBeUpdated)
        {
            myAccountPage = new MyAccountPage(_settings.configurations.driver);
            myAccountPage.UpdateFirstName(nameToBeUpdated, _settings.configurations.password);
        }

        [Then(@"mark should see updated first name in the profile section")]
        public void ThenMarkShouldSeeUpdatedFirstNameInTheProfileSection()
        {
            myAccountPage = new MyAccountPage(_settings.configurations.driver);
            myAccountPage.VerifyProfileInformationIsUpdated();
        }
    }
}

appSettings.json

All static content is stored in the json file and read by the AppSettings class and further fed into all pages as part of Hooks execution

{
  "baseUrl": "http://automationpractice.com/",
  "username": "777abhi@gmail.com",
  "password": "Test1234",
  "driver": null
}

GitHub Actions

This defines the workflow for automatic execution of test automation framework in the pipeline on every commit/ change made to the code.

dotnet.yml

name: .NET

on:
  push:
    branches: [ main ]
  pull_request:
    branches: [ main ]

jobs:
  build:

    runs-on: windows-latest

    steps:
    - uses: actions/checkout@v2
    - name: Setup .NET
      uses: actions/setup-dotnet@v1
      with:
        dotnet-version: 3.1.x
    - name: Restore dependencies
      run: dotnet restore
    - name: Build
      run: dotnet build --no-restore
    - name: Test
      run: dotnet test --no-build --verbosity normal

Azure DevOps Pipeline

azure-pipelines.yml

# ASP.NET Core
# Build and test ASP.NET Core projects targeting .NET Core.
# Add steps that run tests, create a NuGet package, deploy, and more:
# https://docs.microsoft.com/azure/devops/pipelines/languages/dotnet-core

trigger:
- main

pool:
  vmImage: windows-latest

variables:
  buildConfiguration: 'Release'

steps:
- task: DotNetCoreCLI@2
  inputs:
    command: 'build'
    projects: '**/Com.Test.AbhinavSharma.csproj'

- task: DotNetCoreCLI@2
  inputs:
    command: 'test'
    projects: '**/Com.Test.AbhinavSharma.csproj'
    testRunTitle: 'Com.Test.AbhinavSharma Test Execution Report'

Comments