GuyStore — Testing Guide

This guide covers how to run, understand, and extend the three-layer test suite for the GuyStore ASP.NET Core 9 e-commerce platform.

Overview

93
Unit tests Unit
139
Integration tests Integration
35+
E2E tests E2E
ProjectTypeFrameworkScope
GuyStore.Tests.Unit Unit xUnit + Moq Models, services, controller logic in isolation
GuyStore.Tests.Integration Integration xUnit + WebApplicationFactory Full HTTP pipeline, Identity auth, in-memory EF Core
GuyStore.Tests.E2E E2E xUnit + Playwright (Chromium) Real browser automation against a live Kestrel server

Prerequisites

No external services are needed. All tests use an in-memory EF Core database, no-op SMTP, and simulation-mode translation.

Running All Tests

From the solution root (the folder containing GuyStore.sln):

dotnet test

Unit Tests Only

dotnet test GuyStore.Tests.Unit/GuyStore.Tests.Unit.csproj

Integration Tests Only

dotnet test GuyStore.Tests.Integration/GuyStore.Tests.Integration.csproj

E2E Tests

Step 1 — Install Playwright browsers (one-time)

cd GuyStore.Tests.E2E
dotnet build
pwsh bin/Debug/net9.0/playwright.ps1 install chromium

Step 2 — Run

dotnet test GuyStore.Tests.E2E/GuyStore.Tests.E2E.csproj

Useful filters

# Run tests matching a class name
dotnet test --filter "FullyQualifiedName~ReviewControllerTests"

# Run tests matching a method name
dotnet test --filter "FullyQualifiedName~Submit_AuthenticatedUser"

# Skip E2E (unit + integration only)
dotnet test --filter "FullyQualifiedName!~GuyStore.Tests.E2E"

# Verbose output
dotnet test --logger "console;verbosity=detailed"

Unit Test Structure

Pure in-process tests — no network, no database, no file system. Dependencies are replaced with Moq mocks or lightweight fakes.

FileWhat is tested
Models/ApplicationUserTests.csFullName computed property, IsApproved default
Models/CartItemTests.csLineTotal = UnitPrice × Quantity
Models/DiscountCodeTests.csIsValid() — expiry, max uses, active flag
Models/OrderTests.csOrderStatus enum values, default status, financial fields
Models/PaginatedListTests.csPage slicing, HasNextPage / HasPreviousPage
Models/ProductReviewTests.csRating range 1–5, required Body
Models/ProductTests.csIsPublished default, collection initialization, timestamps
Models/ProductVariantTests.csPrice adjustment math (positive, negative, zero delta)
Models/ShippingAddressTests.csIsDefault, CreatedAt, DataAnnotations validation
Controllers/NotificationsControllerTests.csSignalR broadcast, message validation, type guard
Services/CartServiceTests.csAdd/update/remove/clear items, ItemCount, Total
Services/EmailServiceTests.csSMTP no-op when server is unconfigured
Services/InvoiceServiceTests.csPDF generation returns valid PDF bytes
Services/TranslationServiceTests.csLanguageOption record, ITranslationService mock contract

Integration Test Structure

Tests send real HTTP requests through the ASP.NET Core pipeline. Each test class gets its own WebApplicationFactory instance with a fresh in-memory database, automatically seeded by DataSeeder.

Helpers

HelperPurpose
AuthHelper.LoginAsync(client, email, pw) Logs in via POST and stores the auth cookie in the client's cookie jar.
AuthHelper.PostFormAsync(client, url, fields) GETs url to extract the CSRF token, then POSTs with all fields injected.
AuthHelper.GetAntiForgeryTokenAsync(client, url) Extracts __RequestVerificationToken from any form page.

Controller Coverage

FileCoverage
AccountControllerTests.csLogin, register, pending approval, wrong password
AddressControllerTests.csIndex, Create (valid/invalid/unauthenticated), Delete 404, set default
AdminAccountsControllerTests.csUser list, pending list, approval, role-based access
AdminDiscountsControllerTests.csCreate, duplicate code error, uppercase normalisation, Delete
AdminFeedbackControllerTests.csIndex (filtered), Details 404, Delete, API→admin visibility flow
AdminOrdersControllerTests.csList, status filter, status update, access control
AdminProductsControllerTests.csCRUD, publish toggle, role-based denial
AdminSettingsControllerTests.csIndex, Update existing/non-existent key, access control
AdminPaginationTests.csAccounts/Discounts/Feedback/PaymentMethods page 1 + page 2, no-nav on single page, out-of-range 200
ApiControllerTests.csAuth login/me, paginated products + feedback/mine (envelope shape, page 2, pageSize clamp)
CartControllerTests.csAdd, checkout, My Orders, empty-cart guard
ReviewControllerTests.csSubmit (auth/unauth/duplicate/404), Delete (auth/unauth/404)
ShopControllerTests.csListing, search, category filter, detail, image endpoint
ThemeControllerTests.csCookie switching, defaults, open-redirect guard
Stage2IntegrationTests.csPagination, self-service password, support, roles, invoices
Stage3IntegrationTests.csDiscounts, addresses, reviews, variants, 2FA, chat, OAuth
PasskeyControllerTests.csBeginRegistration returns JSON options, BeginAuthentication returns JSON options, session round-trip. CompleteRegistration and CompleteAuthentication require a real WebAuthn authenticator — verify via E2E with Playwright's fake authenticator API instead.

E2E Test Structure

All E2E test classes share a single PlaywrightFixture via [Collection("E2E")]. The fixture starts a Kestrel server, seeds data, and opens a headless Chromium browser. Each test method opens its own page and closes it when done.

FileScenarios
AdminTests.csDashboard stats, product list, notification broadcast, role-based access
AddressTests.csAddress list, create/validate, set-default, unauthenticated redirect
CheckoutTests.csAdd-to-cart, full checkout flow, My Orders page
DiscountTests.csAdmin creates code, apply coupon at checkout, invalid code error
InvoiceTests.cs404 for non-existent order, Invoice link after checkout, Admin orders page
LoginTests.csLogin success/failure, pending account block, logout
ReviewTests.csSubmit review, duplicate prevention, unauthenticated hides form
PaginationTests.csShop page 1 next-link, page 2 indicator, pages show different products; Admin Products + Discounts page 2
ShopTests.csPublished products, search, category filter, product detail
Stage2Tests.csForest theme, password form, support widget/inbox, Viewer/SalesRep roles
Stage3Tests.csSales reports, discount list, address book, reviews, variants, 2FA, import
ThemeTests.csDark/corporate theme cookie, persistence across navigation

Seeded Test Data

Every test database is pre-populated by DataSeeder.SeedAsync():

ResourceDetails
Admin user[email protected] / Admin@123 — role: Administrator
Account manager[email protected] / Acct@123 — role: AccountManagement
Products30 products; 29 published, 1 draft (15 Electronics, 8 Office, 7 Accessories)
RolesAdministrator, AccountManagement, Registered, Viewer, SalesRep
Payment methodsPurchase Order (PO), Bank Transfer
App settingsEmailOrderConfirmation = false
Tip: 21 discount codes are seeded (17 active, 4 inactive). No addresses or orders are seeded. Create them within the test that needs them and use Guid.NewGuid() to avoid collisions between tests in the same class.

Test Isolation

LayerDatabaseShared between
Unit None (pure) Fully isolated per test
Integration In-memory EF Core (one per test class) Tests within same class share the DB; different classes have separate DBs
E2E In-memory EF Core (one for entire collection) All E2E tests share one DB + browser instance; use fresh pages per test
Warning: E2E tests share a database. Data created by one test (e.g., a review, a discount code, an order) persists for subsequent tests. Design tests to be additive rather than relying on an empty state.

Continuous Integration

GitHub Actions example

- name: Install Playwright browsers
  run: |
    cd GuyStore.Tests.E2E
    dotnet build --configuration Release
    pwsh bin/Release/net9.0/playwright.ps1 install chromium --with-deps

- name: Run unit + integration tests
  run: dotnet test --filter "FullyQualifiedName!~GuyStore.Tests.E2E" --configuration Release

- name: Run E2E tests
  run: dotnet test GuyStore.Tests.E2E --configuration Release

Adding New Tests

New unit test

1
Create *.cs in GuyStore.Tests.Unit/Models/, Services/, or Controllers/.
2
Use Moq to mock any interface dependencies.
3
No HTTP, no database — keep it pure. Use FakeSession for session-dependent code.
// Example
public class MyModelTests
{
    [Fact]
    public void Property_CanBeSet() { ... }

    [Theory]
    [InlineData(1, true)]
    [InlineData(0, false)]
    public void Condition_ReturnsExpected(int value, bool expected) { ... }
}

New integration test

1
Add a class in GuyStore.Tests.Integration/Controllers/ that implements IClassFixture<GuyStoreWebApplicationFactory>.
2
Use client.LoginAsync(email, password) to authenticate.
3
Use client.PostFormAsync(url, fields) for form submissions — CSRF tokens are injected automatically.
public class MyControllerTests : IClassFixture<GuyStoreWebApplicationFactory>
{
    private readonly GuyStoreWebApplicationFactory _factory;
    public MyControllerTests(GuyStoreWebApplicationFactory factory) => _factory = factory;

    private HttpClient FreshClient() =>
        _factory.CreateClient(new WebApplicationFactoryClientOptions { AllowAutoRedirect = true });

    [Fact]
    public async Task SomeAction_Returns200()
    {
        var client = FreshClient();
        await client.LoginAsync("[email protected]", "Admin@123");
        var response = await client.GetAsync("/SomeController/SomeAction");
        response.EnsureSuccessStatusCode();
    }
}

New E2E test

1
Add a class in GuyStore.Tests.E2E/Tests/ decorated with [Collection("E2E")].
2
Inject PlaywrightFixture in the constructor.
3
Call _fixture.NewPageAsync() at the start and page.CloseAsync() at the end of each test.
[Collection("E2E")]
public class MyFeatureTests
{
    private readonly PlaywrightFixture _fixture;
    public MyFeatureTests(PlaywrightFixture fixture) => _fixture = fixture;

    [Fact]
    public async Task FeaturePage_IsAccessible()
    {
        var page = await _fixture.NewPageAsync();
        await LoginAsAdminAsync(page);
        await page.GotoAsync($"{_fixture.BaseUrl}/MyFeature");
        await page.WaitForLoadStateAsync(LoadState.NetworkIdle);

        var html = await page.ContentAsync();
        Assert.Contains("My Feature", html);
        await page.CloseAsync();
    }

    private async Task LoginAsAdminAsync(IPage page)
    {
        await page.GotoAsync($"{_fixture.BaseUrl}/Account/Login");
        await page.WaitForLoadStateAsync(LoadState.NetworkIdle);
        await page.FillAsync("input[name='Email']", "[email protected]");
        await page.FillAsync("input[name='Password']", "Admin@123");
        await page.RunAndWaitForNavigationAsync(
            () => page.ClickAsync("button[type='submit']"),
            new PageRunAndWaitForNavigationOptions { WaitUntil = WaitUntilState.NetworkIdle });
    }
}

Testing Passkey / WebAuthn endpoints

The BeginRegistration and BeginAuthentication endpoints are testable with a standard HttpClient — they just return JSON and write to session. Use an integration test that calls GET /Passkey/BeginRegistration (authenticated) and asserts the response contains challenge and rp fields.

CompleteRegistration and CompleteAuthentication require a real WebAuthn authenticator response, which can only be produced by a browser. Use Playwright's virtual authenticator API (page.AddVirtualAuthenticatorAsync()) in an E2E test to create and assert a full passkey registration → sign-in flow without real biometrics.

ASPNETCORE_ENVIRONMENT=Testing always forces InMemory. The "UseInMemoryDatabase" appsettings toggle is ignored in the Testing environment — GuyStoreWebApplicationFactory always gets an InMemory database with a unique name, keeping tests isolated from each other and from the development SQL Server.