Skip to main content

End-to-End Testing

End-to-end testing validates complete user workflows by testing the application from the user interface through all layers to the database. It simulates real user interactions to ensure the entire system works together correctly.

Official Definition/Standards

End-to-end testing is a methodology used to test whether the flow of an application is performing as designed from start to finish. It tests the complete user journey and validates that all integrated components work together in production-like environments.

Setup and Usage (Tools, Packages, Test Runners)

Primary E2E Testing Tools:

  • Playwright: Modern, fast, cross-browser automation (Microsoft)
  • Selenium WebDriver: Mature, widely-supported browser automation
  • Cypress: JavaScript-based, developer-friendly testing framework
  • SpecFlow: BDD framework for .NET with Gherkin syntax

Essential Packages:

# Playwright
dotnet add package Microsoft.Playwright
dotnet add package Microsoft.Playwright.NUnit

# Selenium
dotnet add package Selenium.WebDriver
dotnet add package Selenium.WebDriver.ChromeDriver
dotnet add package DotNetSeleniumExtras.WaitHelpers

# SpecFlow for BDD
dotnet add package SpecFlow
dotnet add package SpecFlow.NUnit

Test Infrastructure:

  • Real browsers (Chrome, Firefox, Edge, Safari)
  • Browser drivers and automation APIs
  • Test data management and cleanup
  • Screenshot and video capture
  • Cross-platform and mobile testing

Typical Test Architecture and Patterns

Common Patterns:

  • Page Object Model: Encapsulate page elements and actions
  • Page Factory: Initialize page elements automatically
  • Test Data Builders: Create test data consistently
  • Base Test Classes: Shared setup and teardown
  • Fluent APIs: Chain actions for readable tests
  • BDD Scenarios: Given-When-Then structure

Project Structure:

MyProject.E2ETests/
├── PageObjects/
│ ├── LoginPage.cs
│ ├── BookingPage.cs
│ └── DashboardPage.cs
├── TestData/
├── Helpers/
├── Features/ # For BDD/SpecFlow
└── Tests/

Example E2E Test Code

// Package references:
// <PackageReference Include="Microsoft.Playwright" Version="1.40.0" />
// <PackageReference Include="Microsoft.Playwright.NUnit" Version="1.40.0" />

using Microsoft.Playwright;
using Microsoft.Playwright.NUnit;
using NUnit.Framework;

// Playwright E2E Tests
[Parallelizable(ParallelScope.Self)]
[TestFixture]
public class HotelBookingE2ETests : PageTest
{
private string _baseUrl = "https://localhost:7001"; // Your app URL

[SetUp]
public async Task Setup()
{
// Configure browser options
await Context.Tracing.StartAsync(new()
{
Title = TestContext.CurrentContext.Test.Name,
Screenshots = true,
Snapshots = true,
Sources = true
});
}

[TearDown]
public async Task TearDown()
{
// Save trace for debugging failures
await Context.Tracing.StopAsync(new()
{
Path = Path.Combine(TestContext.CurrentContext.WorkDirectory,
$"{TestContext.CurrentContext.Test.Name}.zip")
});
}

[Test]
public async Task CompleteBookingFlow_ValidUser_BookingSuccessful()
{
// Arrange - Navigate to home page
await Page.GotoAsync(_baseUrl);
await Page.WaitForLoadStateAsync(LoadState.NetworkIdle);

// Act & Assert - Check rooms availability
await Page.ClickAsync("[data-testid='check-availability']");

await Page.FillAsync("[data-testid='checkin-date']",
DateTime.Today.AddDays(7).ToString("yyyy-MM-dd"));
await Page.FillAsync("[data-testid='checkout-date']",
DateTime.Today.AddDays(10).ToString("yyyy-MM-dd"));
await Page.SelectOptionAsync("[data-testid='guests']", "2");

await Page.ClickAsync("[data-testid='search-rooms']");
await Page.WaitForSelectorAsync("[data-testid='room-list']");

// Verify rooms are displayed
var roomCards = await Page.Locator("[data-testid='room-card']").CountAsync();
Assert.That(roomCards, Is.GreaterThan(0), "No rooms found for the selected dates");

// Select first available room
await Page.ClickAsync("[data-testid='room-card']:first-child [data-testid='book-room']");
await Page.WaitForURLAsync("**/booking/**");

// Fill guest information
await Page.FillAsync("[data-testid='first-name']", "John");
await Page.FillAsync("[data-testid='last-name']", "Doe");
await Page.FillAsync("[data-testid='email']", "john.doe@example.com");
await Page.FillAsync("[data-testid='phone']", "555-123-4567");

// Fill payment information
await Page.FillAsync("[data-testid='card-number']", "4111111111111111");
await Page.FillAsync("[data-testid='expiry']", "12/25");
await Page.FillAsync("[data-testid='cvv']", "123");
await Page.FillAsync("[data-testid='cardholder-name']", "John Doe");

// Submit booking
await Page.ClickAsync("[data-testid='submit-booking']");

// Wait for confirmation page
await Page.WaitForURLAsync("**/booking/confirmation/**");
await Page.WaitForSelectorAsync("[data-testid='booking-confirmation']");

// Assert booking confirmation
var confirmationMessage = await Page.TextContentAsync("[data-testid='confirmation-message']");
Assert.That(confirmationMessage, Does.Contain("Your booking has been confirmed"));

var bookingNumber = await Page.TextContentAsync("[data-testid='booking-number']");
Assert.That(bookingNumber, Is.Not.Empty, "Booking number should be displayed");

// Verify booking details
var guestName = await Page.TextContentAsync("[data-testid='guest-name']");
Assert.That(guestName, Does.Contain("John Doe"));

var dates = await Page.TextContentAsync("[data-testid='booking-dates']");
Assert.That(dates, Is.Not.Empty, "Booking dates should be displayed");
}

[Test]
public async Task LoginFlow_ValidCredentials_RedirectsToDashboard()
{
// Navigate to login page
await Page.GotoAsync($"{_baseUrl}/login");

// Fill login form
await Page.FillAsync("[data-testid='email']", "admin@hotel.com");
await Page.FillAsync("[data-testid='password']", "AdminPassword123!");

// Submit form
await Page.ClickAsync("[data-testid='login-submit']");

// Wait for redirect to dashboard
await Page.WaitForURLAsync("**/dashboard");

// Verify dashboard elements
await Page.WaitForSelectorAsync("[data-testid='dashboard-header']");
var welcomeMessage = await Page.TextContentAsync("[data-testid='welcome-message']");
Assert.That(welcomeMessage, Does.Contain("Welcome"));
}

[Test]
public async Task SearchRooms_NoAvailableRooms_ShowsNoResultsMessage()
{
await Page.GotoAsync(_baseUrl);

// Search for rooms in the past (should have no results)
await Page.FillAsync("[data-testid='checkin-date']",
DateTime.Today.AddDays(-10).ToString("yyyy-MM-dd"));
await Page.FillAsync("[data-testid='checkout-date']",
DateTime.Today.AddDays(-5).ToString("yyyy-MM-dd"));

await Page.ClickAsync("[data-testid='search-rooms']");

// Wait for no results message
await Page.WaitForSelectorAsync("[data-testid='no-rooms-message']");
var message = await Page.TextContentAsync("[data-testid='no-rooms-message']");
Assert.That(message, Does.Contain("No rooms available"));
}
}

// Page Object Model example
public class BookingPage
{
private readonly IPage _page;

public BookingPage(IPage page)
{
_page = page;
}

// Locators
private ILocator FirstNameInput => _page.Locator("[data-testid='first-name']");
private ILocator LastNameInput => _page.Locator("[data-testid='last-name']");
private ILocator EmailInput => _page.Locator("[data-testid='email']");
private ILocator SubmitButton => _page.Locator("[data-testid='submit-booking']");
private ILocator ConfirmationMessage => _page.Locator("[data-testid='confirmation-message']");

// Actions
public async Task FillGuestInformation(string firstName, string lastName, string email)
{
await FirstNameInput.FillAsync(firstName);
await LastNameInput.FillAsync(lastName);
await EmailInput.FillAsync(email);
}

public async Task SubmitBooking()
{
await SubmitButton.ClickAsync();
await _page.WaitForURLAsync("**/booking/confirmation/**");
}

public async Task<string> GetConfirmationMessage()
{
await ConfirmationMessage.WaitForAsync();
return await ConfirmationMessage.TextContentAsync() ?? string.Empty;
}
}

// Selenium WebDriver alternative example
public class SeleniumE2ETests
{
private IWebDriver _driver = null!;
private readonly string _baseUrl = "https://localhost:7001";

[SetUp]
public void Setup()
{
var options = new ChromeOptions();
options.AddArguments("--headless"); // Run in headless mode for CI
_driver = new ChromeDriver(options);
_driver.Manage().Timeouts().ImplicitWait = TimeSpan.FromSeconds(10);
}

[TearDown]
public void TearDown()
{
_driver?.Quit();
_driver?.Dispose();
}

[Test]
public void BookRoom_ValidData_ShowsConfirmation()
{
// Navigate to home page
_driver.Navigate().GoToUrl(_baseUrl);

// Fill search form
var checkinDate = _driver.FindElement(By.CssSelector("[data-testid='checkin-date']"));
checkinDate.SendKeys(DateTime.Today.AddDays(7).ToString("MM/dd/yyyy"));

var checkoutDate = _driver.FindElement(By.CssSelector("[data-testid='checkout-date']"));
checkoutDate.SendKeys(DateTime.Today.AddDays(10).ToString("MM/dd/yyyy"));

// Search rooms
var searchButton = _driver.FindElement(By.CssSelector("[data-testid='search-rooms']"));
searchButton.Click();

// Wait for rooms to load
var wait = new WebDriverWait(_driver, TimeSpan.FromSeconds(10));
wait.Until(ExpectedConditions.ElementIsVisible(By.CssSelector("[data-testid='room-list']")));

// Book first room
var bookButton = _driver.FindElement(By.CssSelector("[data-testid='room-card']:first-child [data-testid='book-room']"));
bookButton.Click();

// Fill booking form
_driver.FindElement(By.CssSelector("[data-testid='first-name']")).SendKeys("Jane");
_driver.FindElement(By.CssSelector("[data-testid='last-name']")).SendKeys("Smith");
_driver.FindElement(By.CssSelector("[data-testid='email']")).SendKeys("jane@example.com");

// Submit booking
_driver.FindElement(By.CssSelector("[data-testid='submit-booking']")).Click();

// Verify confirmation
wait.Until(ExpectedConditions.ElementIsVisible(By.CssSelector("[data-testid='booking-confirmation']")));
var confirmationText = _driver.FindElement(By.CssSelector("[data-testid='confirmation-message']")).Text;

Assert.That(confirmationText, Does.Contain("confirmed"));
}
}

// BDD with SpecFlow example
[Binding]
public class BookingSteps
{
private readonly IPage _page;
private readonly BookingPage _bookingPage;

public BookingSteps(IPage page)
{
_page = page;
_bookingPage = new BookingPage(page);
}

[Given(@"I am on the hotel booking website")]
public async Task GivenIAmOnTheHotelBookingWebsite()
{
await _page.GotoAsync("https://localhost:7001");
}

[When(@"I search for rooms from ""(.*)"" to ""(.*)""")]
public async Task WhenISearchForRoomsFromTo(string checkin, string checkout)
{
await _page.FillAsync("[data-testid='checkin-date']", checkin);
await _page.FillAsync("[data-testid='checkout-date']", checkout);
await _page.ClickAsync("[data-testid='search-rooms']");
}

[Then(@"I should see available rooms")]
public async Task ThenIShouldSeeAvailableRooms()
{
await _page.WaitForSelectorAsync("[data-testid='room-list']");
var roomCount = await _page.Locator("[data-testid='room-card']").CountAsync();
Assert.That(roomCount, Is.GreaterThan(0));
}
}

// Feature file (BookingFlow.feature)
/*
Feature: Hotel Room Booking
As a hotel guest
I want to book a room online
So that I can secure accommodation for my stay

Scenario: Successful room booking
Given I am on the hotel booking website
When I search for rooms from "2024-12-01" to "2024-12-03"
Then I should see available rooms
When I select the first available room
And I fill in my guest details
And I provide payment information
And I submit the booking
Then I should see a booking confirmation
*/

When to Use and When Not to Use

Use E2E Testing when:

  • Testing critical user journeys and workflows
  • Validating cross-browser compatibility
  • Testing complete feature functionality
  • Verifying production-like scenarios
  • Testing user interface interactions
  • Validating third-party integrations

Don't use E2E Testing when:

  • Testing individual business logic (use unit tests)
  • Fast feedback is needed during development
  • Testing internal APIs without UI
  • Limited CI/CD execution time
  • Testing detailed edge cases
  • Validating specific component behavior

Pros and Cons and Alternatives

Pros:

  • Tests real user scenarios
  • Validates entire application stack
  • Catches integration and UI issues
  • Provides high confidence in releases
  • Tests cross-browser compatibility
  • Validates user experience

Cons:

  • Slow execution and feedback
  • Brittle and hard to maintain
  • Expensive to run and maintain
  • Complex debugging and troubleshooting
  • Flaky due to timing and environment issues
  • Requires more infrastructure

Alternatives:

  • Visual regression testing (Percy, Chromatic)
  • Manual exploratory testing
  • Smoke testing for critical paths
  • User acceptance testing
  • Component testing with Storybook
  • API contract testing