Create An Interactive Selenium Automation Report With ExtentReports And C#

Enes Kuhn

If you use Selenium Web Driver and C# as your automation tools for creating and maintaining automation scripts for on-demand/automated web application testing, but dread detailed testing reports — you’re in the right place fellow engineer.

By the time you’re done reading this blog, you’ll learn how to create an interactive automation reporting that can be stored on a shared location and even sent to an email distribution group.

Let’s begin. 

I decided to create interactive Selenium Automation reports with Extent Reports and C#. 

Why Extent Reporting framework?

I didn’t need any persuasion after reading the information on their website: 

With Extent Framework, you can create beautiful, interactive and detailed reports for your tests. Add events, screenshots, tags, devices, authors or any other relevant information you decide is important to create an informative and a stunning report. 

Tip: You can use the Community and Pro version of Extent Reports. Read more about the two versions here

What else do we need to create the first automation report? 

Create a simple Selenium Web Driver C# Framework

Note: If you already have a custom framework, you can skip this part.

For the demo purposes, I am going to use an existing dummy nopCommerce Demo Store framework (learn more in this post).

Framework skeleton

Nuget packages you need to have installed:

  •  DotNetSeleniumExtras.PageObjects
  • NUnit
  • NUnit3TestAdapter
  • Selenium.Chrome.WebDriver
  • Selenium.Firefox.WebDriver
  • Selenium.Support
  • Selenium.WebDriver
  • DotNetSeleniumExtras.WaitHelpers

Class definitions:

//Browsers.cs
using OpenQA.Selenium;
using OpenQA.Selenium.Chrome;
using OpenQA.Selenium.Firefox;
using System.Configuration;
namespace Test
{
    public class Browsers
    {
        public Browsers()
        {
            baseURL = ConfigurationManager.AppSettings["url"];
            browser = ConfigurationManager.AppSettings["browser"];
        }
private IWebDriver webDriver;
        private string baseURL;
        private string browser;
public void Init()
        {
            switch (browser)
            {
                case "Chrome":
                    webDriver = new ChromeDriver();
                    break;
                case "Firefox":
                    webDriver = new FirefoxDriver();
                    break;
                default:
                webDriver = new ChromeDriver();
                break;
            }
            webDriver.Manage().Window.Maximize();
            Goto(baseURL);
        }
        public string Title
        {
            get { return webDriver.Title; }
        }
        public IWebDriver getDriver
        {
            get { return webDriver; }
        }
        public void Goto(string url)
        {
            webDriver.Url = url;
        }
        public void Close()
        {
            webDriver.Quit();
        }
    }
}
//Pages.cs
using SeleniumExtras.PageObjects;
using System;
namespace Test
{
    public class Pages
    {
        public Pages(Browsers browser)
        {
            _browser = browser;
        }
Browsers _browser { get; }
private T GetPages<T>() where T : new()
        {
            var page = (T)Activator.CreateInstance(typeof(T), _browser.getDriver);
            PageFactory.InitElements(_browser.getDriver, page);
            return page;
        }
        public Home Home => GetPages<Home>();
        public Computers Computers => GetPages<Computers>();
    }
}
//Computers.cs
using Microsoft.VisualStudio.TestTools.UnitTesting;
using OpenQA.Selenium;
using SeleniumExtras.PageObjects;
namespace Test
{
    public class Computers
    {
        public Computers()
        {
            driver = null;
        }
        public Computers(IWebDriver webDriver)
        {
            driver = webDriver;
        }
//Driver
        IWebDriver driver;
//Locators
        [FindsBy(How = How.Id, Using = "small-searchterms")]
        private IWebElement SearchInput;
[FindsBy(How = How.XPath, Using = "//input[@value='Search']")]
        private IWebElement SearchButton;
        //Actions
        public Computers isAt()
        {
            Assert.IsTrue(driver.Title.Equals("nopCommerce demo store. Computers"));
            return this;
        }
        public Computers EnterSearchText(string searchText)
        {
            SearchInput.SendKeys(searchText);
            return this;
        }
        public Computers ClickSearch()
        {
            SearchButton.Click();
            return this;
        }
    }
}
//Home.cs
using Microsoft.VisualStudio.TestTools.UnitTesting;
using OpenQA.Selenium;
using SeleniumExtras.PageObjects;
namespace Test
{
    public class Home
    {
        public Home()
        {
            driver = null;
        }
        public Home(IWebDriver webDriver)
        {
            driver = webDriver;
        }
//Driver
        IWebDriver driver;
//Locators
        [FindsBy(How = How.XPath, Using = "//ul[@class='top-menu notmobile']//a[@href='/computers']")]
        private IWebElement ComputersLink;
//Actions
        public Home isAt()
        {
            Assert.IsTrue(driver.Title.Equals("nopCommerce demo store"));
            return this;
        }
        public Home GoToComputers()
        {
            ComputersLink.Click();
            return this;
        }
    }
}
//MyFirstTest.cs
using NUnit.Framework;
namespace Test
{
    [TestFixture]
    public class MyFirstTest : TestBase
    {
        [Test]
        public void NopCommerceDummyTest()
        {
            Pages.Home.isAt();
            Pages.Home.GoToComputers();
            Pages.Computers.isAt();
            Pages.Computers.EnterSearchText("Search for me");
            Pages.Computers.ClickSearch();
        }
    }
}
//TestBase
using NUnit.Framework;
namespace Test
{
    [TestFixture]
    public abstract class TestBase
    {
        protected Browsers browser;
        protected Pages Pages;
[SetUp]
        public void StartUpTest()
        {
            browser = new Browsers();
            browser.Init();
            Pages = new Pages(browser);
        }
[TearDown]
        public void EndTest()
        {
            browser.Close();
        }
    }
}

App.config file:

<?xml version="1.0" encoding="utf-8"?>
<configuration>
  <appSettings>
    <add key="url" value="https://demo.nopcommerce.com" />
    <add key="browser" value="Firefox" />
  </appSettings>
</configuration>

Create a Reporting Library project

Add a new Class Library project to your framework, name it “ReportingLibrary”, and install Extent Reports NuGet.

Create a new Class Library project ReportingLibrary
Install ExtentReports to ReportingLibrary project

Add a new class “ExtentReportsHelper.cs” to the newly created library. 

Your solution should now look like this:

We are going to use three classes:

  • ExtentReports class — creates an HTML report generator and saves it to a user-defined path
  • ExtentTest class —test steps collector
  • ExtentV3HtmlReporter class — API that takes the results data from a Selenium test run and then processes it into a concise HTML report

Note: You can find the official documentation here.

Definition of ExtentReportsHelper.cs class:

using AventStack.ExtentReports;
using AventStack.ExtentReports.Reporter;
using System;
using System.IO;
using System.Reflection;
namespace ReportingLibrary
{
    public class ExtentReportsHelper
    {
        public ExtentReports extent { get; set; }
        public ExtentV3HtmlReporter reporter { get; set; }
        public ExtentTest test { get; set; }
public ExtentReportsHelper()
        {
            extent = new ExtentReports();
            reporter = new ExtentV3HtmlReporter(Path.Combine(Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location), "ExtentReports.html"));
            reporter.Config.DocumentTitle = "Automation Testing Report";
            reporter.Config.ReportName = "Regression Testing";
            reporter.Config.Theme = AventStack.ExtentReports.Reporter.Configuration.Theme.Standard;
            extent.AttachReporter(reporter);
            extent.AddSystemInfo("Application Under Test", "nop Commerce Demo");
            extent.AddSystemInfo("Environment", "QA");
            extent.AddSystemInfo("Machine", Environment.MachineName);
            extent.AddSystemInfo("OS", Environment.OSVersion.VersionString);
        }
public void CreateTest(string testName)
        {
            test = extent.CreateTest(testName);
        }
public void SetStepStatusPass(string stepDescription)
        {
            test.Log(Status.Pass, stepDescription);
        }
public void SetStepStatusWarning(string stepDescription)
        {
            test.Log(Status.Warning, stepDescription);
        }
public void SetTestStatusPass()
        {
            test.Pass("Test Executed Sucessfully!");
        }
public void SetTestStatusFail(string message = null)
        {
            var printMessage = "<p><b>Test FAILED!</b></p>";
if (!string.IsNullOrEmpty(message))
            {
                printMessage += $"Message: <br>{message}<br>"; 
            }
            test.Fail(printMessage);
        }
public void AddTestFailureScreenshot(string base64ScreenCapture)
        {
            test.AddScreenCaptureFromBase64String(base64ScreenCapture, "Screenshot on Error:");
        }
public void SetTestStatusSkipped()
        {
            test.Skip("Test skipped!");
        }
public void Close()
        {
            extent.Flush();
        }
}
}

You’ll notice that I am hard-coding most of the values in the class constructor.
We can build in a way to be configurable with values stored in some sort of configuration file. The generated HTML file is saved at the project output location.

Methods created in Extent Reports helper class:

  • CreateTest method — creates a test
  • SetStepStatusPass —sets test step status to Pass
  • SetStepStatusWarning — sets test step status to Warning
  • SetTestStatusPass — sets overall test status to Pass
  • SetTestStatusFail — sets overall test status to Fail with an error message
  • AddTestFailureScreenshot —adds exception screenshot to Extent Report
  • SetTestStatusSkipped — sets overall test status to Skipped
  • Close — writes or updates all the information to our reporter

Web Element and Web Driver extensions

Let me introduce web element and web driver extensions (helpers) which will be our fellow partners in keeping the code clean, and will also add more power to the code itself. 

They are nothing more than a wrapper around existing Selenium methods, but they do add more value to the existing methods, such as logging, implicit wait and capturing screenshots.

Let’s see how to use these extensions. 

Add a new class library project “SeleniumHelperLibrary” with two classes WebDiverExtensions.cs and WebElementExtension.cs:

The new project skeleton

Extension definitions:

//SeleniumHelperLibrary.cs
using OpenQA.Selenium;
namespace SeleniumHelperLibrary
{
    public static class WebDiverExtensions
    {
        public static string ScreenCaptureAsBase64String(this IWebDriver driver)
        {
            ITakesScreenshot ts = (ITakesScreenshot)driver;
            Screenshot screenshot = ts.GetScreenshot();
return screenshot.AsBase64EncodedString;
        }
    }
}
//SeleniumHelperLibrary.cs
using OpenQA.Selenium;
using OpenQA.Selenium.Support.UI;
using ReportingLibrary;
using System;
using WaitHelpers = SeleniumExtras.WaitHelpers;
namespace SeleniumHelperLibrary
{
    public static class WebElementExtension
    {
        public static bool ControlDisplayed(this IWebElement element, IWebDriver driver, ExtentReportsHelper extentReportsHelper, string elementName, bool displayed = true, uint timeoutInSeconds = 60)
        {
            var wait = new WebDriverWait(driver, TimeSpan.FromSeconds(timeoutInSeconds));
            wait.IgnoreExceptionTypes(typeof(Exception));
            return wait.Until(drv =>
            {
                if (!displayed && !element.Displayed || displayed && element.Displayed)
                {
                    extentReportsHelper.SetStepStatusPass($"[{elementName}] is displayed on the page.");
                    return true;
                    
                }
                extentReportsHelper.SetStepStatusPass($"[{elementName}] is displayed on the page.");
                return false;
            });
        }
public static void ClearWrapper(this IWebElement element, ExtentReportsHelper extentReportsHelper, string elementName)
        {
            element.Clear();
            if(element.Text.Equals(string.Empty))
            {
                extentReportsHelper.SetStepStatusPass($"Cleared element [{elementName}] content.");
            }
            else
            {
                extentReportsHelper.SetStepStatusWarning($"Element [{elementName}] content is not cleared. Element value is [{element.Text}]");
            }
        }
public static bool ElementlIsClickable(this IWebElement element, IWebDriver driver, ExtentReportsHelper extentReportsHelper, string elementName, uint timeoutInSeconds = 60, bool displayed = true)
        {
            try
            {
                WebDriverWait wait = new WebDriverWait(driver, TimeSpan.FromSeconds(timeoutInSeconds));
                return wait.Until(drv =>
                {
                    if (WaitHelpers.ExpectedConditions.ElementToBeClickable(element) != null)
                    {
                        extentReportsHelper.SetStepStatusPass($"Element [{elementName}] is clickable.");
                        return true;
                    }
extentReportsHelper.SetStepStatusWarning($"Element [{elementName}] is not clickable.");
                    return false;
                });
            }
            catch
            {
                return false;
            }
        }
public static void ClickWrapper(this IWebElement element, IWebDriver driver, ExtentReportsHelper extentReportsHelper, string elementName)
        {
            if (element.ElementlIsClickable(driver, extentReportsHelper, elementName))
            {
                element.Click();
                extentReportsHelper.SetStepStatusPass($"Clicked on the element [{elementName}].");
            }
            else
            {
                throw new Exception($"Element [{elementName}] is not displayed");
            }
        }
public static void SendKeysWrapper(this IWebElement element, ExtentReportsHelper extentReportsHelper, string value, string elementName)
        {
            element.SendKeys(value);
            extentReportsHelper.SetStepStatusPass($"SendKeys value [{value}] to  element [{elementName}].");
        }
public static bool ValidatePageTitle(this IWebDriver driver, ExtentReportsHelper extentReportsHelper, string title, uint timeoutInSeconds = 300)
        {
            bool result = false;
            var wait = new WebDriverWait(driver, TimeSpan.FromSeconds(timeoutInSeconds));
            wait.IgnoreExceptionTypes(typeof(Exception));
            return result = wait.Until(drv =>
            {
                if (drv.Title.Contains(title))
                {
                    extentReportsHelper.SetStepStatusPass($"Page title [{drv.Title}] contains [{title}].");
                    return true;
                }
extentReportsHelper.SetStepStatusWarning($"Page title [{drv.Title}] does not contain [{title}].");
                return false;
            });
        }
public static bool ValidateURLContains(this IWebDriver driver, ExtentReportsHelper extentReportsHelper, string urlPart, uint timeoutInSeconds = 120)
        {
            bool result = false;
            var wait = new WebDriverWait(driver, TimeSpan.FromSeconds(timeoutInSeconds));
            wait.IgnoreExceptionTypes(typeof(Exception));
            return result = wait.Until(drv =>
            {
                if (drv.Url.Contains(urlPart))
                {
                    extentReportsHelper.SetStepStatusPass($"Page URL [{drv.Url}] contains [{urlPart}].");
                    return true;
                }
extentReportsHelper.SetStepStatusWarning($"Page URL [{drv.Url}] does not contain [{urlPart}].");
                return false;
            });
        }
    }
}

Putting it all together

I am going to create an instance of the ExtentReportHelper class inside TestBase.cs abstract class. It is very important to do it within the OneTimeSetUp method. If we, for any reason, do it within the SetUp method, it will create a report with just one test result.

The same thing applies when writing the results to an HTML report. We do this at the very end (OneTimeTearDown).

ExtentReportHelper instance needs to be sent to WebElelementExtension class in order to provide logging. We are going to do it via Page.cs class all the way to the extension class.

Update the framework classes as follows:

//TestBase.cs
using NUnit.Framework;
using NUnit.Framework.Interfaces;
using ReportingLibrary;
using SeleniumHelperLibrary;
using System;
namespace Test
{
    [TestFixture]
    public abstract class TestBase
    {
        protected Browsers browser;
        protected Pages Pages;
        protected ExtentReportsHelper extent;
[OneTimeSetUp]
        public void SetUpReporter()
        {
            extent = new ExtentReportsHelper();
        }
[SetUp]
        public void StartUpTest()
        {
            extent.CreateTest(TestContext.CurrentContext.Test.Name);
            browser = new Browsers(extent);
            browser.Init();
            Pages = new Pages(browser, extent);
        }
[TearDown]
        public void AfterTest()
        {
            try
            {
                var status = TestContext.CurrentContext.Result.Outcome.Status;
                var stacktrace = TestContext.CurrentContext.Result.StackTrace;
                var errorMessage = "<pre>" + TestContext.CurrentContext.Result.Message + "</pre>";
                switch (status)
                {
                    case TestStatus.Failed:
                        extent.SetTestStatusFail($"<br>{errorMessage}<br>Stack Trace: <br>{stacktrace}<br>");
                        extent.AddTestFailureScreenshot(browser.getDriver.ScreenCaptureAsBase64String());
                        break;
                    case TestStatus.Skipped:
                        extent.SetTestStatusSkipped();
                        break;
                    default:
                        extent.SetTestStatusPass();
                        break;
                }
            }
            catch (Exception e)
            {
                throw (e);
            }
            finally
            {
                browser.Close();
            } 
        }
[OneTimeTearDown]
        public void CloseAll()
        {
            try
            {
                extent.Close();
            }
            catch (Exception e)
            {
                throw (e);
            }
        }
    }
}
//Browsers.cs
using OpenQA.Selenium;
using OpenQA.Selenium.Chrome;
using OpenQA.Selenium.Firefox;
using ReportingLibrary;
using System.Configuration;
namespace Test
{
    public class Browsers
    {
        public Browsers(ExtentReportsHelper reportsHelper)
        {
            baseURL = ConfigurationManager.AppSettings["url"];
            browser = ConfigurationManager.AppSettings["browser"];
            extentReportsHelper = reportsHelper;
        }
private IWebDriver webDriver;
        private string baseURL;
        private string browser;
        private ExtentReportsHelper extentReportsHelper;
public void Init()
        {
            switch (browser)
            {
                case "Chrome":
                    webDriver = new ChromeDriver();
                    break;
                case "Firefox":
                    webDriver = new FirefoxDriver();
                    break;
                default:
                    webDriver = new ChromeDriver();
                break;
            }
            extentReportsHelper.SetStepStatusPass("Browser started.");
            webDriver.Manage().Window.Maximize();
            extentReportsHelper.SetStepStatusPass("Browser maximized.");
            Goto(baseURL);
            
        }
        public string Title
        {
            get { return webDriver.Title; }
        }
        public IWebDriver getDriver
        {
            get { return webDriver; }
        }
        public void Goto(string url)
        {
            webDriver.Url = url;
            extentReportsHelper.SetStepStatusPass($"Browser navigated to the url [{url}].");
        }
        public void Close()
        {
            webDriver.Quit();
            extentReportsHelper.SetStepStatusPass($"Browser closed.");
        }
    }
}
//Pages.cs
using ReportingLibrary;
using SeleniumExtras.PageObjects;
using System;
namespace Test
{
    public class Pages
    {
        public Pages(Browsers browser, ExtentReportsHelper extentReportsHelper)
        {
            _browser = browser;
            _extentReportsHelper = extentReportsHelper;
        }
Browsers _browser { get; }
        ExtentReportsHelper _extentReportsHelper { get; set; }
private T GetPages<T>() where T : new()
        {
            var page = (T)Activator.CreateInstance(typeof(T), _browser.getDriver, _extentReportsHelper);
            PageFactory.InitElements(_browser.getDriver, page);
            return page;
        }
        public Home Home => GetPages<Home>();
        public Computers Computers => GetPages<Computers>();
    }
}
//Home.cs and all other page map classes update in the same way
using NUnit.Framework;
using OpenQA.Selenium;
using ReportingLibrary;
using SeleniumExtras.PageObjects;
using SeleniumHelperLibrary;
namespace Test
{
    public class Home
    {
        public Home()
        {
            driver = null;
            extentReportsHelper = null;
        }
        public Home(IWebDriver webDriver, ExtentReportsHelper reportsHelper)
        {
            driver = webDriver;
            extentReportsHelper = reportsHelper;
        }
private IWebDriver driver;
        private ExtentReportsHelper extentReportsHelper;
[FindsBy(How = How.XPath, Using = "//ul[@class='top-menu notmobile']//a[@href='/computers']")]
        private IWebElement ComputersLink;
public Home isAt()
        {
            Assert.IsTrue(driver.ValidatePageTitle(extentReportsHelper, "nopCommerce demo store"));
            return this;
        }
        public Home GoToComputers()
        {
            ComputersLink.ClickWrapper(driver, extentReportsHelper, "Computers link");
            return this;
        }
    }
}

Test Run

For the demo purposes, I created two very same tests and ran those from the Visual Studio’s test explorer:

Two tests with the same steps
Tests executed successfully

Generated Extent Report named “ExtentReports.html” is located at the solution output folder. Double-click on it and it will bring up the interactive report:

Testing Report
Dashboard

We are almost done, but there’s still one important puzzle piece missing — we want to run this via CMD command.

Navigate to NUnit download site to download and install the NUnit Console app.

Download and install the NUnit Console application

Then, find the locations of “nunit3-console.exe” and “SeleniumFramework.dll” files on your computer.

This is what it looks like on my machine:

  • C:\Program Files (x86)\NUnit.org\nunit-console\nunit3-console.exe
  • C:\FW\ReportingWithExtentReports\Output\SeleniumFramework.dll

Open Command Prompt and execute the command:

"C:\Program Files (x86)\NUnit.org\nunit-console\nunit3-console.exe" "C:\FW\ReportingWithExtentReports\Output\SeleniumFramework.dll"
CMD command before pressing Enter
Testing completed

And there it is — we did it! High five 🙂 

For more nunit-console.exe features, just execute this CMD command:

"C:\Program Files (x86)\NUnit.org\nunit-console\nunit3-console.exe" /help

Later, you can create a simple console app that will pick up the report and send it via email. But that’s a story for another blog. 

I hope reading this blog was a good investment of your time, and hope it will make your day at least a little bit easier. Until the next time — Happy testing.

Leave a Reply

Your email address will not be published.

After you leave a comment, it will be held for moderation, and published afterwards.

This site is protected by reCAPTCHA and the Google Privacy Policy and Terms of Service apply.