Back to "End-to-End Testing met PuppeteerSharp - Een goed alternatief voor Selenium"

This is a viewer only at the moment see the article on how this works.

To update the preview hit Ctrl-Alt-R (or ⌘-Alt-R on Mac) or Enter to refresh. The Save icon lets you save the markdown file to disk

This is a preview from the server running through my markdig pipeline

E2E Testing PuppeteerSharp Testing xUnit

End-to-End Testing met PuppeteerSharp - Een goed alternatief voor Selenium

Thursday, 27 November 2025

Moderne E2E (End-To-End, met behulp van uw site zoals gebruikers zou) testen hoeft niet pijnlijk te zijn. Deze uitgebreide gids laat zien hoe u PuppeteerSharp te gebruiken voor snelle, betrouwbare browserautomatisering in .NET-covering alles van fundamentele testen tot PDF-generatie en web scraping. Terwijl Microsoft's Playwright is de meer moderne multi-browser oplossing, Ik koos PuppeteerSharp voor deze blog omdat het is wat ik wist en Chrome-only testen was genoeg voor mijn behoeften. Als u Firefox en Safari ondersteuning nodig hebt, check out mijn Playwright-gids In plaats daarvan.

Inleiding

Als je ooit hebt gewerkt met Selenium voor end-to-end testen, zult u weten dat het kan een juiste pijn in de achterkant. Tussen worstelen met bestuurdersversies, omgaan met schilferige tests die werken op uw machine maar nergens anders, en de algemene traagheid van het WebDriver protocol, het is genoeg om te maken dat je wilt gooien het allemaal in en handmatig testen in plaats daarvan.

Enter PuppeterSharp - de .NET-poort van Google's Puppeter Het is net als Selenium's jongere, snellere neef die de moeite neemt om op tijd te komen en je niet nodig hebt om zeventien verschillende browserdrivers te downloaden.

In dit artikel, Ik loop u door hoe ik heb geïmplementeerd PuppeteerSharp voor E2E testen op deze zeer blog, compleet met echte code voorbeelden van de repo. We zullen het testen, PDF generatie, web scraping, en vergelijk het met de alternatieven.

Wat is PuppeterSharp dan?

PuppeterSharp is een .NET-bibliotheek die een hoog niveau API om Chrome of Chromium browsers met behulp van de Chrome DevTools-protocol. In tegenstelling tot Selenium, waarbij gebruik wordt gemaakt van de WebDriver-protocol (een nogal clunky HTTP-gebaseerde JSON wire protocol), PuppeteerSharp praat rechtstreeks met de browser via DevTools.

Bekijk het zo:

  • Selenium: Zoals het verzenden van brieven via de post om te communiceren met uw browser
  • PuppeterSharp: Zoals het hebben van een directe telefoonlijn naar de hersenen van de browser
graph LR
    A[Test Code] -->|WebDriver Protocol| B[Selenium]
    B -->|JSON Wire Protocol| C[Browser Driver]
    C -->|Commands| D[Browser]

    E[Test Code] -->|DevTools Protocol| F[PuppeteerSharp]
    F -->|Direct Connection| G[Chrome/Chromium]

    style A stroke:#333,stroke-width:2px
    style E stroke:#333,stroke-width:2px
    style F stroke:#0066cc,stroke-width:3px
    style G stroke:#0066cc,stroke-width:3px

Waar E2E test past in uw teststrategie

Voordat we dieper duiken, laten we praten over waar E2E testen past in het grote schema van dingen. Je hebt waarschijnlijk gehoord van de testpiramide - hier is hoe het eigenlijk werkt in de praktijk:

graph TB
    subgraph "Testing Pyramid"
        E2E[E2E Tests<br/>Few, Slow, High Confidence<br/>Test full user journeys]
        INT[Integration Tests<br/>Medium number, Medium speed<br/>Test component interactions]
        UNIT[Unit Tests<br/>Many, Fast, Low Cost<br/>Test individual functions]
    end

    subgraph "Trade-offs"
        SPEED[Speed]
        CONF[Confidence]
        COST[Cost]
    end

    subgraph "When to Use E2E"
        W1[Critical user journeys<br/>e.g. checkout, login]
        W2[Cross-browser compatibility]
        W3[JavaScript-heavy UIs]
        W4[Complex user interactions]
    end

    E2E -.->|Slow but high confidence| CONF
    INT -.->|Balanced| SPEED
    UNIT -.->|Fast and cheap| SPEED

    E2E -.->|Expensive to run| COST
    UNIT -.->|Cheap to run| COST

    style E2E stroke:#cc0000,stroke-width:3px
    style INT stroke:#ff9900,stroke-width:2px
    style UNIT stroke:#00aa00,stroke-width:2px
    style CONF stroke:#0066cc,stroke-width:2px
    style SPEED stroke:#00aa00,stroke-width:2px
    style COST stroke:#cc0000,stroke-width:2px

The Reality Check:

  • Eenheidstests (80% van uw testen): Snel, goedkoop, test individuele functies, maar ze vertellen u niet of het systeem als geheel werkt.
  • Integratietests (15% van uw testen): Test hoe verschillende onderdelen samenwerken. Sneller dan E2E maar test niet de volledige UI.
  • E2E-tests (5% van uw testen): Langzaam, duur, maar test het systeem precies zoals gebruikers het ervaren. Dit is waar PuppeteerSharp glanst.

Wanneer u E2E-tests nodig heeft:

  1. Kritische ritten van de gebruiker - Inloggen, uitchecken, betaling verwerken. Als deze breken, uw bedrijf stopt.
  2. JavaScript-heavy UIs - Moderne SPA's (Reageren, Vue, Hoek) waar de UI wordt weergegeven client-side.
  3. Problemen met cross-browser - Verschillende browsers maken dingen anders (hoewel met PuppeteerSharp ben je Chrome-only).
  4. Complexe interacties - Multi-step wizards, drag-and-drop, file uploads.

Wanneer u geen E2E-tests nodig heeft:

  1. Eenvoudige CRUD-bewerkingen - Integratietests zijn genoeg.
  2. Pure logica - Daar zijn de testen voor.
  3. Elke rand case - E2E-tests zijn te traag en te duur voor exhaustieve tests.

Waarom PuppeteerSharp Over Selenium?

Laat me de manieren tellen:

  1. Geen Driver Management Faff: PuppeteerSharp downloadt en beheert de Chrome browser voor u. Geen gerommel meer over met ChromeDriver versies die niet overeenkomen met uw geïnstalleerde Chrome versie.

  2. Snellere uitvoering: Het DevTools Protocol is aanzienlijk sneller dan WebDriver. Uw tests zullen sneller lopen, en u zult minder tijd besteden aan het wachten op dingen die gebeuren.

  3. Betere API: De API is moderner en intuïtiever. Het is async/wait helemaal naar beneden, die mooi past bij de moderne .NET ontwikkeling.

  4. Ingebouwde schermafdruk & PDF-generatie: Wil je een screenshot als een test mislukt? Het is dood eenvoudig met PuppeteerSharp.

  5. Netwerkverzoeken intercepteren: U kunt eenvoudig netwerkverzoeken onderscheppen, wijzigen of blokkeren - briljant voor het testen van offline scenario's of het bespotten van API-antwoorden.

  6. Juiste JavaScript-uitvoering: Voer JavaScript uit in de context van de pagina en krijg resultaten terug op een manier die je niet laat huilen.

PuppeteerSharp instellen

Ten eerste, voeg de PuppeterSharp NuGet pakket:

dotnet add package PuppeteerSharp

Hier is mijn test project configuratie (Mostlylucid.Test/Mostlylucid.Test.csproj:23):

<PackageReference Include="PuppeteerSharp" Version="20.2.4" />
<PackageReference Include="xunit" Version="2.9.3" />
<PackageReference Include="xunit.runner.visualstudio" Version="3.1.4">
  <PrivateAssets>all</PrivateAssets>
  <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>

Ik gebruik xEenheid (ASP.NET Core's default), maar PuppeteerSharp werkt net zo goed met NEenheid of MSTest.

Een basistestklasse aanmaken

In plaats van het herhalen van setup/teardown code in elke test, heb ik een basisklasse gemaakt (Mostlylucid.Test/E2E/E2ETestBase.cs:12) dat browser lifecycle management behandelt:

De klassestructuur

using PuppeteerSharp;
using Xunit.Abstractions;

namespace Mostlylucid.Test.E2E;

public abstract class E2ETestBase : IAsyncLifetime
{
    protected readonly ITestOutputHelper Output;
    protected IBrowser Browser = null!;
    protected IPage Page = null!;

    protected const string BaseUrl = "http://localhost:8080";
    protected const int DefaultTimeout = 30000;

    protected E2ETestBase(ITestOutputHelper output)
    {
        Output = output;
    }

Wij implementeren IAsyncLifetime van xUnit, die biedt async setup / teardown. In tegenstelling tot traditionele constructors, dit laat ons goed wachten browser initialisatie.

Browser-initialisatie

    public async Task InitializeAsync()
    {
        // Download Chromium on first run
        var browserFetcher = new BrowserFetcher();
        await browserFetcher.DownloadAsync();

        // Launch browser with sensible defaults
        Browser = await Puppeteer.LaunchAsync(new LaunchOptions
        {
            Headless = true, // Set false for debugging
            DefaultViewport = new ViewPortOptions
            {
                Width = 1400,
                Height = 900
            },
            Args = new[]
            {
                "--no-sandbox",
                "--disable-setuid-sandbox"
            }
        });

        Page = await Browser.NewPageAsync();
        Page.DefaultTimeout = DefaultTimeout;
    }

De BrowserFetcher automatisch downloadt een compatibele Chromium versie op de eerste run - geen handmatig driver management nodig. --no-sandbox Vlaggen zijn vereist voor Docker/CI omgevingen.

Opruimen

    public async Task DisposeAsync()
    {
        if (Page != null) await Page.CloseAsync();
        if (Browser != null) await Browser.CloseAsync();
    }
}

De juiste verwijdering is cruciaal om geheugenlekken te voorkomen. Elke browser instantie gebruikt 100-200MB RAM.

Hulpmethoden

De basisklasse omvat helper methoden om ketelplaat te verminderen (Mostlylucid.Test/E2E/E2ETestBase.cs:72-172):

// Navigation with automatic network idle waiting
protected async Task NavigateAsync(string path)
{
    var url = path.StartsWith("http") ? path : $"{BaseUrl}{path}";
    await Page.GoToAsync(url, new NavigationOptions
    {
        WaitUntil = new[] { WaitUntilNavigation.Networkidle2 }
    });
}

// Safe element waiting with timeout handling
protected async Task<IElementHandle?> WaitForSelectorAsync(string selector, int timeout = 5000)
{
    try
    {
        return await Page.WaitForSelectorAsync(selector, new WaitForSelectorOptions
        {
            Timeout = timeout,
            Visible = true
        });
    }
    catch (WaitTaskTimeoutException)
    {
        return null; // Graceful degradation
    }
}

// Common element operations
protected async Task<bool> ElementExistsAsync(string selector) =>
    await Page.QuerySelectorAsync(selector) != null;

protected async Task<string?> GetTextContentAsync(string selector)
{
    var element = await Page.QuerySelectorAsync(selector);
    return element == null ? null :
        await Page.EvaluateFunctionAsync<string>("el => el.textContent", element);
}

protected async Task TypeAsync(string selector, string text, int delay = 50)
{
    await Page.WaitForSelectorAsync(selector);
    await Page.TypeAsync(selector, text, new TypeOptions { Delay = delay });
}

protected async Task ClickAsync(string selector)
{
    await Page.WaitForSelectorAsync(selector);
    await Page.ClickAsync(selector);
}

Deze hanteren de vervelende bits - wachtend op elementen om te bestaan, sierlijke timeout handling, en automatische logging voor wanneer tests falen in CI.

Schrijven van feitelijke tests

Goed, laten we naar de goede dingen - het schrijven van echte testen. Hier is een echte test van mijn blog filterbalk functionaliteit (Mostlylucid.Test/E2E/FilterBarTests.cs:20-50):

[Fact(Skip = "Local E2E test - requires site to be running on localhost:8080")]
public async Task FilterBar_LanguageDropdown_ShowsLanguages()
{
    // Arrange
    await NavigateAsync("/blog");

    // Act - Click the language dropdown button
    var dropdownButton = await WaitForSelectorAsync("#LanguageDropDown button");
    Assert.NotNull(dropdownButton);

    await ClickAsync("#LanguageDropDown button");
    await WaitAsync(300);

    // Assert - Dropdown menu should be visible with language options
    var dropdownOpen = await EvaluateFunctionAsync<bool>(@"() => {
        const dropdown = document.querySelector('#LanguageDropDown div[x-show]');
        if (!dropdown) return false;
        const style = window.getComputedStyle(dropdown);
        return style.display !== 'none';
    }");

    Assert.True(dropdownOpen, "Language dropdown should be open");

    // Check that English option exists
    var hasEnglish = await EvaluateFunctionAsync<bool>(@"() => {
        const options = document.querySelectorAll('#LanguageDropDown li a');
        return Array.from(options).some(opt => opt.textContent.toLowerCase().includes('english'));
    }");

    Assert.True(hasEnglish, "Language dropdown should contain English option");
    Output.WriteLine("✅ Language dropdown shows languages correctly");
}

Deze test controleert of mijn taal dropdown goed werkt. Laten we eens kijken wat ervoor zorgt dat het tikt:

Het Skip-attribuut

[Fact(Skip = "Local E2E test - requires site to be running on localhost:8080")]

Ik heb deze test standaard overgeslagen omdat het vereist dat de site lokaal wordt uitgevoerd. Voor E2E-tests, u wilt ze meestal uitvoeren op aanvraag in plaats van met elke bouw. U kunt ze unskip wanneer u klaar bent om ze uit te voeren, of voer ze in een aparte CI-taak waar je de site hebt opgezet.

Uitvoeren van JavaScript

var dropdownOpen = await EvaluateFunctionAsync<bool>(@"() => {
    const dropdown = document.querySelector('#LanguageDropDown div[x-show]');
    if (!dropdown) return false;
    const style = window.getComputedStyle(dropdown);
    return style.display !== 'none';
}");

Dit is een van de gebieden waar PuppeteerSharp absoluut schijnt. EvaluateFunctionAsync methode kunt u JavaScript uitvoeren in de browser context en het resultaat terug te krijgen als een juiste .NET type. In dit geval, Ik controleer of een dropdown daadwerkelijk zichtbaar is (niet alleen aanwezig in de DOM) door te kijken naar de berekende stijlen.

Vergelijk dit met Selenium waar u moet:

  1. Het element zoeken
  2. De weergave-eigenschap ophalen
  3. Het tekenreeksresultaat ontleden
  4. Hopelijk is het niet oud tegen de tijd dat je het controleert.

Testen van HTMX interacties

Mijn blog gebruikt HTMX uitgebreid (server-side rendering zonder het schrijven van JavaScript). Hier is een test die sorteerfunctionaliteit controleert (Mostlylucid.Test/E2E/FilterBarTests.cs:98-126):

[Fact(Skip = "Local E2E test - requires site to be running on localhost:8080")]
public async Task FilterBar_SortOrder_ChangesPostOrder()
{
    // Arrange
    await NavigateAsync("/blog");

    // Get the first post title before sorting
    var firstPostBefore = await EvaluateFunctionAsync<string>(@"() => {
        const postLink = document.querySelector('.post-title, article h2 a, #contentcontainer article a');
        return postLink?.textContent?.trim() || '';
    }");
    Output.WriteLine($"First post before sort: {firstPostBefore}");

    // Act - Change sort order to "Oldest first"
    await Page.SelectAsync("#orderSelect", "date_asc");
    await WaitAsync(1000); // Wait for HTMX to update

    // Assert - Post order should have changed
    var firstPostAfter = await EvaluateFunctionAsync<string>(@"() => {
        const postLink = document.querySelector('.post-title, article h2 a, #contentcontainer article a');
        return postLink?.textContent?.trim() || '';
    }");
    Output.WriteLine($"First post after sort: {firstPostAfter}");

    var selectValue = await EvaluateFunctionAsync<string>("() => document.querySelector('#orderSelect')?.value");
    Assert.Equal("date_asc", selectValue);
    Output.WriteLine("✅ Sort order selection works correctly");
}

De sleutel hier is de await WaitAsync(1000) HTMX heeft een moment nodig om zijn verzoek in te dienen en de DOM bij te werken. In een perfecte wereld wachten we op een specifieke netwerkaanvraag, maar voor eenvoudige gevallen is een korte vertraging prima.

Responsief ontwerp testen

Hier is een brutale test die controleert mijn filterbalk is goed verborgen op mobiele apparaten (Mostlylucid.Test/E2E/FilterBarTests.cs:216-245):

[Fact(Skip = "Local E2E test - requires site to be running on localhost:8080")]
public async Task FilterBar_ResponsiveDesign_HiddenOnMobile()
{
    // Arrange - Set mobile viewport
    await Page.SetViewportAsync(new ViewPortOptions
    {
        Width = 375,
        Height = 667
    });

    await NavigateAsync("/blog");
    await WaitAsync(500);

    // Assert - Filter bar should be hidden on mobile
    var filterBarVisible = await EvaluateFunctionAsync<bool>(@"() => {
        const filterBar = document.querySelector('.hidden.lg\\:flex');
        if (!filterBar) return true;
        const rect = filterBar.getBoundingClientRect();
        return rect.width > 0 && rect.height > 0;
    }");

    Assert.False(filterBarVisible, "Filter bar should be hidden on mobile viewport");
    Output.WriteLine("✅ Filter bar correctly hidden on mobile");

    // Reset viewport
    await Page.SetViewportAsync(new ViewPortOptions
    {
        Width = 1400,
        Height = 900
    });
}

U kunt de viewport op elk gewenst moment, dat is briljant voor het testen van responsieve lay-outs. Veel makkelijker dan het wijzigen van uw browservenster handmatig!

Geavanceerde PuppeteerSharp functies

Netwerkinterceptie

Een van mijn favoriete functies is de mogelijkheid om netwerkverzoeken te onderscheppen en aan te passen. Dit is van onschatbare waarde voor het testen van fouttoestanden of offline scenario's:

await Page.SetRequestInterceptionAsync(true);

Page.Request += async (sender, e) =>
{
    // Block all image requests to speed up tests
    if (e.Request.ResourceType == ResourceType.Image)
    {
        await e.Request.AbortAsync();
    }
    // Mock API responses
    else if (e.Request.Url.Contains("/api/posts"))
    {
        await e.Request.RespondAsync(new ResponseData
        {
            Status = HttpStatusCode.OK,
            ContentType = "application/json",
            Body = "{\"posts\": []}"
        });
    }
    else
    {
        await e.Request.ContinueAsync();
    }
};

Screenshots maken

Als een test mislukt, is een screenshot duizend logberichten waard:

try
{
    // Your test code here
    await Page.ClickAsync("#someButton");
}
catch (Exception)
{
    // Take a screenshot on failure
    await Page.ScreenshotAsync("test-failure.png");
    throw; // Re-throw to fail the test
}

PDF-generatie

U kunt zelfs PDF's van pagina's genereren, wat nuttig is voor het testen van server-side rendering of print stylesheets:

await Page.PdfAsync("page.pdf", new PdfOptions
{
    Format = PaperFormat.A4,
    PrintBackground = true
});

Codedekking

PuppeteerSharp kan zelfs JavaScript code dekkingsgegevens verzamelen:

await Page.Coverage.StartJSCoverageAsync();
await Page.GoToAsync("http://localhost:8080");

var coverage = await Page.Coverage.StopJSCoverageAsync();
var totalBytes = coverage.Sum(c => c.Text.Length);
var usedBytes = coverage.Sum(c => c.Ranges.Sum(r => r.End - r.Start));
var percentUsed = usedBytes / (double)totalBytes * 100;

Output.WriteLine($"JavaScript coverage: {percentUsed:F2}%");

PuppeteerSharp vs. The Competition

Laten we eens goed kijken hoe PuppeteerSharp zich tegen andere E2E testtools keert:

graph TD
    A[E2E Testing Tools] --> B[Selenium WebDriver]
    A --> C[PuppeteerSharp]
    A --> D[Playwright]
    A --> E[Cypress]

    B --> B1[❌ Slow WebDriver protocol]
    B --> B2[❌ Driver management hassle]
    B --> B3[✅ Multi-browser support]
    B --> B4[✅ Mature ecosystem]

    C --> C1[✅ Fast DevTools protocol]
    C --> C2[✅ Auto browser management]
    C --> C3[❌ Chrome/Chromium only]
    C --> C4[✅ Great .NET integration]

    D --> D1[✅ Fast DevTools protocol]
    D --> D2[✅ Auto browser management]
    D --> D3[✅ Multi-browser support]
    D --> D4[⚠️ Newer to .NET ecosystem]

    E --> E1[✅ Great developer experience]
    E --> E2[❌ JavaScript only]
    E --> E3[❌ Not for .NET]
    E --> E4[✅ Excellent documentation]

    style C stroke:#0066cc,stroke-width:3px
    style C1 stroke:#00aa00,stroke-width:2px
    style C2 stroke:#00aa00,stroke-width:2px
    style C4 stroke:#00aa00,stroke-width:2px

Selenium WebDriver

De Oude Garde

Selenium bestaat al sinds 2004 en het toont. Het is volwassen, goed gedocumenteerd, en ondersteunt elke browser onder de zon. Maar het toont ook zijn leeftijd:

Voordelen:

  • Ondersteunt alle browsers (Chrome, Firefox, Safari, Rand, IE als je een masochist)
  • Massief ecosysteem van hulpmiddelen en uitbreidingen
  • Bekend en algemeen aangenomen
  • Goed voor cross-browser testen

Nadelen:

  • WebDriver protocol is traag
  • Bestuurder management is een pijn (hoewel WebDriverManager helpt)
  • API voelt zich gedateerd in vergelijking met moderne alternatieven
  • Flaky tests zijn vaak als gevolg van timing problemen
  • Geen ingebouwde netwerkonderschepping

Wanneer te gebruiken: Wanneer je absoluut moet testen in meerdere browsers, of wanneer je al geïnvesteerd bent in het Selenium ecosysteem.

Playwright

The New Kid on the Block

Playwright is Microsoft's antwoord op Puppeteer, met .NET ondersteuning Gebakken vanaf het begin. Het is in wezen PuppeteerSharp maar met multi-browser ondersteuning:

Voordelen:

  • Ondersteunt Chrome, Firefox, Safari (WebKit)
  • Moderne API vergelijkbaar met Puppeteer
  • Browsers automatisch downloaden
  • Ingebouwde netwerkonderschepping, screenshots, enz.
  • Uitstekende .NET ondersteuning

Nadelen:

  • Nieuwer, zo kleiner ecosysteem
  • Kan overkill zijn als je alleen Chrome nodig hebt
  • Iets complexere setup dankzij ondersteuning voor multi-browser

Wanneer te gebruiken: Als je multi-browser ondersteuning nodig hebt, maar een moderne API wilt. Als je een nieuw project start en cross-browser testen nodig hebt, is Playwright waarschijnlijk je beste weddenschap.

Cypress

De Darling van de JavaScript Ontwikkelaar

Cypress is briljant als je werkt in JavaScript/TypeScript, maar het is een non-starter voor .NET ontwikkelaars:

Voordelen:

  • Fantastische ontwikkelaar ervaring
  • Time-travel debuggen
  • Automatisch wachten
  • Geweldige documentatie

Nadelen:

  • JavaScript/TypeScript
  • Geen .NET-ondersteuning
  • Kan niet meerdere tabbladen of vensters testen
  • Beperkt tot het testen van uw eigen toepassing (geen testen in verschillende domeinen)

Wanneer te gebruiken: Niet doen, je schrijft .NET code. Blijf bij iets dat integreert met je tech stack.

Dus wat zou je moeten gebruiken?

Hier is mijn mening:

graph TD
    A[What E2E tool?] --> B{Need multi-browser testing?}
    B -->|Yes| C{Starting new project?}
    B -->|No| D[PuppeteerSharp]

    C -->|Yes| E[Playwright]
    C -->|No| F{Invested in Selenium?}

    F -->|Yes| G[Stick with Selenium]
    F -->|No| E

    D --> H[✅ Fast, simple, reliable]
    E --> I[✅ Modern, flexible]
    G --> J[⚠️ Consider migrating]

    style D stroke:#0066cc,stroke-width:3px
    style H stroke:#00aa00,stroke-width:2px

Voor de meeste .NET ontwikkelaars die moderne webapplicaties bouwen:

  • Chrome-alleen testen? → PuppeterSharp
  • Multi-browser testen? → Playwright
  • Gebruik je Selenium al? → Overweeg migreren naar Playwright, maar niet te haasten

Uitvoeren van tests in CI/CD

E2E testen zijn allemaal goed en goed op uw lokale machine, maar ze moeten draaien in CI / CD pijpleidingen ook. Hier is hoe ik heb ingesteld voor GitHub-acties:

name: E2E Tests

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

jobs:
  e2e-tests:
    runs-on: ubuntu-latest

    steps:
    - uses: actions/checkout@v3

    - name: Setup .NET
      uses: actions/setup-dotnet@v3
      with:
        dotnet-version: '9.0.x'

    - name: Install dependencies
      run: dotnet restore

    - name: Build
      run: dotnet build --no-restore

    - name: Start application
      run: |
        dotnet run --project Mostlylucid/Mostlylucid.csproj &
        echo $! > app.pid

    - name: Wait for application to start
      run: |
        timeout 60 bash -c 'until curl -f http://localhost:8080/health; do sleep 2; done'

    - name: Run E2E tests
      run: |
        dotnet test Mostlylucid.Test/Mostlylucid.Test.csproj \
          --filter "Category=E2E" \
          --logger "console;verbosity=detailed"

    - name: Upload screenshots on failure
      if: failure()
      uses: actions/upload-artifact@v3
      with:
        name: test-screenshots
        path: '**/test-failure-*.png'

    - name: Stop application
      if: always()
      run: |
        kill $(cat app.pid) || true

De kernpunten:

  1. De toepassing op de achtergrond starten
  2. Wacht tot het gezond is (met behulp van een gezondheidscheck eindpunt)
  3. Voer de E2E-tests uit
  4. Schermafbeeldingen uploaden als geen enkele test mislukt
  5. Stop altijd de toepassing, zelfs als de tests falen

Veel voorkomende valkuilen en hoe ze te vermijden

Flaky-tests

E2E testen kunnen schilferig zijn - ze passeren soms en falen anderen. Dit is meestal te maken met timing problemen. Hier is hoe ze te vermijden:

Slecht:

await Page.ClickAsync("#button");
var text = await GetTextContentAsync("#result");
Assert.Equal("Success", text);

Goed:

await Page.ClickAsync("#button");
await Page.WaitForSelectorAsync("#result");
var text = await GetTextContentAsync("#result");
Assert.Equal("Success", text);

Wacht altijd op het element waarmee je op het punt staat te communiceren om te bestaan en zichtbaar te zijn.

Testisolatie

Elke test moet volledig onafhankelijk zijn. Vertrouw niet op toestand van eerdere tests:

Slecht:

[Fact]
public async Task Test1_Login()
{
    await LoginAsync("user", "password");
    // User is now logged in for subsequent tests
}

[Fact]
public async Task Test2_ViewDashboard()
{
    // Assumes user is still logged in from Test1
    await NavigateAsync("/dashboard");
}

Goed:

[Fact]
public async Task Test1_Login()
{
    await LoginAsync("user", "password");
    await LogoutAsync(); // Clean up
}

[Fact]
public async Task Test2_ViewDashboard()
{
    await LoginAsync("user", "password"); // Set up needed state
    await NavigateAsync("/dashboard");
    await LogoutAsync(); // Clean up
}

Page Object Pattern

Voor complexe pagina's, gebruik het Page Object patroon om uw testen onderhoudbaar te houden:

public class BlogPageObject
{
    private readonly IPage _page;

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

    public async Task SelectLanguageAsync(string language)
    {
        await _page.ClickAsync("#LanguageDropDown button");
        await _page.WaitAsync(300);
        await _page.ClickAsync($"#LanguageDropDown a:has-text('{language}')");
    }

    public async Task<string[]> GetPostTitlesAsync()
    {
        return await _page.EvaluateFunctionAsync<string[]>(@"() => {
            return Array.from(document.querySelectorAll('.post-title'))
                        .map(el => el.textContent.trim());
        }");
    }
}

// Usage in tests
[Fact]
public async Task Can_Filter_By_Language()
{
    var blogPage = new BlogPageObject(Page);
    await NavigateAsync("/blog");

    await blogPage.SelectLanguageAsync("Spanish");
    var titles = await blogPage.GetPostTitlesAsync();

    Assert.All(titles, title => Assert.NotEmpty(title));
}

Prestatieoverwegingen

E2E testen zijn langzamer dan unit tests, er is geen omweg. Maar je kunt ze sneller maken:

Testen uitvoeren in parallel

xUnit voert standaard tests parallel uit, maar je moet voorzichtig zijn met gedeelde toestand:

[Collection("E2E Tests")] // Tests in same collection run sequentially
public class FilterBarTests : E2ETestBase
{
    // Tests here share resources
}

[Collection("Blog Tests")] // Different collection runs in parallel
public class BlogTests : E2ETestBase
{
    // Tests here run in parallel with FilterBarTests
}

Onnodige functies uitschakelen

Versnel tests door het uitschakelen van functies die u niet nodig hebt:

Browser = await Puppeteer.LaunchAsync(new LaunchOptions
{
    Headless = true,
    Args = new[]
    {
        "--no-sandbox",
        "--disable-setuid-sandbox",
        "--disable-dev-shm-usage", // Overcome limited resource problems
        "--disable-accelerated-2d-canvas",
        "--disable-gpu", // Not needed for headless
        "--disable-images", // Don't load images if you don't need them
        "--disable-javascript", // Only if testing static content
    }
});

Netwerkinterceptie verstandig gebruiken

Blokkeer onnodige middelen om dingen te versnellen:

await Page.SetRequestInterceptionAsync(true);
Page.Request += async (sender, e) =>
{
    var blockedResourceTypes = new[]
    {
        ResourceType.Image,
        ResourceType.Media,
        ResourceType.Font,
        ResourceType.StyleSheet // If you don't need to test styling
    };

    if (blockedResourceTypes.Contains(e.Request.ResourceType))
    {
        await e.Request.AbortAsync();
    }
    else
    {
        await e.Request.ContinueAsync();
    }
};

Debuggen van E2E-tests

Als tests falen (en dat zullen ze), moet je ze debuggen. Hier zijn een aantal technieken:

Uitvoeren in niet-hoofdloze modus

Instellen Headless = false om de browser in actie te bekijken:

Browser = await Puppeteer.LaunchAsync(new LaunchOptions
{
    Headless = false,
    SlowMo = 100, // Slow down by 100ms to see what's happening
});

DevTools gebruiken

Je kunt DevTools programmatisch openen:

Browser = await Puppeteer.LaunchAsync(new LaunchOptions
{
    Headless = false,
    Devtools = true, // Auto-open DevTools
});

Console-loggen

Console-berichten van de browser vastleggen:

Page.Console += (sender, e) =>
{
    Output.WriteLine($"Browser console: {e.Message.Text}");
};

Aanmelden aanvragen

Log alle netwerkverzoeken:

Page.Request += (sender, e) =>
{
    Output.WriteLine($"Request: {e.Request.Method} {e.Request.Url}");
};

Page.Response += (sender, e) =>
{
    Output.WriteLine($"Response: {e.Response.Status} {e.Response.Url}");
};

Real-World Test Patronen

Hier zijn enkele patronen die ik regelmatig gebruik in mijn E2E testen:

Inzendingen van het testformulier

[Fact]
public async Task Can_Submit_Comment()
{
    await NavigateAsync("/blog/some-post");

    // Fill in the comment form
    await TypeAsync("#comment-name", "Test User");
    await TypeAsync("#comment-email", "[email protected]");
    await TypeAsync("#comment-content", "This is a test comment");

    // Submit the form
    await ClickAsync("#comment-submit");

    // Wait for success message
    await WaitForSelectorAsync(".comment-success");

    // Verify the comment appears
    var commentText = await GetTextContentAsync(".comment-list .comment:last-child .comment-content");
    Assert.Contains("test comment", commentText.ToLower());
}

Toetsenbordinteracties testen

[Fact]
public async Task Can_Navigate_With_Keyboard()
{
    await NavigateAsync("/blog");

    // Focus the search box
    await Page.FocusAsync("#search");

    // Type a search query
    await Page.Keyboard.TypeAsync("testing");

    // Press arrow down to select first result
    await Page.Keyboard.PressAsync("ArrowDown");

    // Press enter to navigate
    await Page.Keyboard.PressAsync("Enter");

    // Verify we navigated to the right page
    await WaitAsync(1000);
    Assert.Contains("/blog/", Page.Url);
}

Bestandsuploads testen

[Fact]
public async Task Can_Upload_Image()
{
    await NavigateAsync("/admin/upload");

    // Create a test file
    var testFilePath = Path.Combine(Path.GetTempPath(), "test-image.jpg");
    File.WriteAllBytes(testFilePath, new byte[] { 0xFF, 0xD8, 0xFF }); // JPEG header

    // Upload the file
    var fileInput = await Page.QuerySelectorAsync("input[type=file]");
    await fileInput.UploadFileAsync(testFilePath);

    await ClickAsync("#upload-submit");

    // Verify upload succeeded
    await WaitForSelectorAsync(".upload-success");

    // Clean up
    File.Delete(testFilePath);
}

Sleep en val testen

[Fact]
public async Task Can_teAsync("/admin/posts");

    var dragSource = await Page.QuerySelectorAsync(".post-item[data-id='1']");
    var dropTarget = await Page.QuerySelectorAsync(".post-item[data-id='3']");

    var sourceBox = await dragSource.BoundingBoxAsync();
    var targetBox = await dropTarget.BoundingBoxAsync();

    // Perform drag and drop
    await Page.Mouse.MoveAsync(sourceBox.X + sourceBox.Width / 2, sourceBox.Y + sourceBox.Height / 2);
    await Page.Mouse.DownAsync();
    await Page.Mouse.MoveAsync(targetBox.X + targetBox.Width / 2, targetBox.Y + targetBox.Height / 2);
    await Page.Mouse.UpAsync();

    await WaitAsync(500);

    // Verify new order
    var firstItemId = await Page.EvaluateFunctionAsync<string>(
        "() => document.querySelector('.post-item').dataset.id"
    );
    Assert.Equal("1", firstItemId);
}

Integratie met ASP.NET Core Testing

U kunt PuppeteerSharp integreren met ASP.NET Core's WebApplicationFactory voor een meer geïntegreerde testervaring:

public class E2EWebApplicationFactory : WebApplicationFactory<Program>
{
    protected override void ConfigureWebHost(IWebHostBuilder builder)
    {
        builder.UseUrls("http://localhost:5050");

        builder.ConfigureServices(services =>
        {
            // Override services for testing
            // For example, use in-memory database
            services.RemoveAll<DbContextOptions<MostlylucidDbContext>>();
            services.AddDbContext<MostlylucidDbContext>(options =>
            {
                options.UseInMemoryDatabase("TestDb");
            });
        });
    }
}

public abstract class IntegratedE2ETestBase : E2ETestBase, IClassFixture<E2EWebApplicationFactory>
{
    protected E2EWebApplicationFactory Factory { get; }

    protected IntegratedE2ETestBase(E2EWebApplicationFactory factory, ITestOutputHelper output)
        : base(output)
    {
        Factory = factory;
    }

    public override async Task InitializeAsync()
    {
        await base.InitializeAsync();

        // Application is automatically started by WebApplicationFactory
        // Override BaseUrl to use the factory's address
        BaseUrl = "http://localhost:5050";
    }
}

Beyond Testing - PuppeteerSharp voor PDF Generatie en Automatisering

Terwijl E2E testen is briljant, PuppeteerSharp is een Zwitserse leger mes dat veel meer kan doen. Een van de meest populaire toepassingen is het genereren van PDF's van web-inhoud - het is ongelooflijk nuttig voor deze, maar niet zonder de gotchas. Als u facturen, rapporten, of een document generatie systeem, deze sectie bespaart u uren van debuggen.

PDF's genereren - De belofte en de pijn

Het idee is eenvoudig: maak een webpagina in Chrome en sla het op als een PDF. Perfect voor het genereren van facturen, rapporten, certificaten, of elke dynamische inhoud die moet worden verspreid in PDF-formaat.

Hier is de basis aanpak:

public class PdfGeneratorService
{
    public async Task<byte[]> GeneratePdfFromUrlAsync(string url)
    {
        await using var browser = await Puppeteer.LaunchAsync(new LaunchOptions
        {
            Headless = true,
            Args = new[] { "--no-sandbox", "--disable-setuid-sandbox" }
        });

        await using var page = await browser.NewPageAsync();
        await page.GoToAsync(url, new NavigationOptions
        {
            WaitUntil = new[] { WaitUntilNavigation.Networkidle0 }
        });

        var pdfData = await page.PdfDataAsync(new PdfOptions
        {
            Format = PaperFormat.A4,
            PrintBackground = true,
            MarginOptions = new MarginOptions
            {
                Top = "20mm",
                Right = "20mm",
                Bottom = "20mm",
                Left = "20mm"
            }
        });

        return pdfData;
    }
}

Nou, dat is het, totdat het niet zo is, laat me je door de landmijnen leiden.

PDF Generation Gotchas - Wat niemand je vertelt

1. Lettertype inbedden nachtmerries

Het probleem: Uw mooie aangepaste lettertypen verschijnen niet in de PDF, of erger nog, ze zijn er maar zien er absoluut onzin.

Waarom het gebeurt: Chrome heeft toegang nodig tot de fontbestanden tijdens de PDF-generatie. Als uw lettertypen worden geladen via externe CDN en Chrome kan ze niet bereiken (firewall, netwerkproblemen, timing), bent u gevuld.

De oplossing:

await page.GoToAsync(url, new NavigationOptions
{
    WaitUntil = new[]
    {
        WaitUntilNavigation.Networkidle0,  // Wait for network to be idle
        WaitUntilNavigation.Load           // Wait for fonts to load
    },
    Timeout = 60000  // Give it time to load fonts
});

// Extra insurance - wait for fonts to actually load
await page.EvaluateFunctionAsync(@"async () => {
    await document.fonts.ready;
}");

var pdfData = await page.PdfDataAsync(new PdfOptions
{
    Format = PaperFormat.A4,
    PrintBackground = true  // CRUCIAL for @font-face fonts
});

Nog beter, host uw fonts lokaal of embed hen als base64 in uw CSS. Ja, het is een faff, maar het is betrouwbaar.

2. CSS Print Media Queries

Het probleem: Uw PDF lijkt niet op uw webpagina omdat Chrome toepassingen print media queries.

Dit is eigenlijk correct gedrag PDF's zijn printmedia, maar het haalt iedereen er de eerste keer uit.

De oplossing:

Gebruik @media print CSS-regels op passende wijze:

/* Show on screen, hide in PDF */
.no-print {
    display: block;
}

@media print {
    .no-print {
        display: none !important;
    }

    /* Prevent page breaks inside elements */
    .keep-together {
        page-break-inside: avoid;
        break-inside: avoid;
    }

    /* Force page breaks */
    .page-break {
        page-break-before: always;
    }
}

Of, als u de schermversie in uw PDF wilt (handig voor het genereren van "screenshots" als PDF's):

await page.EmulateMediaTypeAsync(MediaType.Screen);  // Force screen media
var pdfData = await page.PdfDataAsync();

3. Page Breaks - De Bane van Uw Bestaan

Het probleem: Uw inhoud wordt ongemakkelijk verdeeld over pagina's, met rubrieken wees aan de onderkant of tafels gesneden in de helft.

De realiteit: Je vecht tegen Chrome's interne paginatie algoritme, en het gaat meestal winnen.

Wat je kunt doen:

@media print {
    h1, h2, h3, h4, h5, h6 {
        page-break-after: avoid;
        break-after: avoid;
    }

    table, figure, img {
        page-break-inside: avoid;
        break-inside: avoid;
    }

    /* Force specific breaks */
    .new-page {
        page-break-before: always;
    }
}

En in je PuppeterSharp code:

var pdfData = await page.PdfDataAsync(new PdfOptions
{
    Format = PaperFormat.A4,
    PrintBackground = true,
    PreferCSSPageSize = true,  // Respect CSS @page rules
    DisplayHeaderFooter = false
});

Pro Tip: Voor complexe lay-outs is het soms makkelijker om uw HTML te structureren met expliciete paginapauzes in plaats van tegen de browser te vechten:

<div class="page">
    <!-- First page content -->
</div>
<div class="page-break"></div>
<div class="page">
    <!-- Second page content -->
</div>

4. Kopteksten en voetteksten - Complexer dan je zou denken

Je kunt headers en voetteksten toevoegen, maar de API is een beetje link:

var pdfData = await page.PdfDataAsync(new PdfOptions
{
    Format = PaperFormat.A4,
    DisplayHeaderFooter = true,
    HeaderTemplate = @"
        <div style='font-size: 10px; text-align: center; width: 100%;'>
            <span class='title'></span>
        </div>
    ",
    FooterTemplate = @"
        <div style='font-size: 10px; text-align: center; width: 100%;'>
            Page <span class='pageNumber'></span> of <span class='totalPages'></span>
        </div>
    ",
    MarginOptions = new MarginOptions
    {
        Top = "30mm",     // Must be larger to accommodate header
        Bottom = "25mm"   // Must be larger to accommodate footer
    }
});

Hebbes:

  • Header/voeter sjablonen moeten geldig zijn HTML maar zijn uiterst beperkt - geen externe CSS, geen JavaScript
  • Je krijgt alleen specifieke variabelen: date, title, url, pageNumber, totalPages
  • Styling is alleen inline
  • De marges moeten groot genoeg zijn voor headers/voeters of ze overlappen uw inhoud

5. Achtergrondgrafieken

Standaard print Chrome geen achtergrondafbeeldingen of kleuren (dit is een standaard browser voor het opslaan van inkt). mustunit synonyms for matching user input het mogelijk maken:

var pdfData = await page.PdfDataAsync(new PdfOptions
{
    PrintBackground = true  // Without this, your beautiful backgrounds vanish
});

6. Geheugenlekken met grote documenten

Het probleem: Het genereren van veel PDF's veroorzaakt het geheugen van uw toepassing ballonnen en uiteindelijk crashen.

Waarom: Elke browser instantie maakt gebruik van significant geheugen (100-200MB), en als je niet goed verwijdert, ze stapelen op.

De oplossing:

Altijd gebruiken await using of de juiste verwijdering:

// Good - automatic disposal
await using var browser = await Puppeteer.LaunchAsync(options);
await using var page = await browser.NewPageAsync();

// Or manually
IBrowser? browser = null;
try
{
    browser = await Puppeteer.LaunchAsync(options);
    // ... use browser
}
finally
{
    if (browser != null)
    {
        await browser.CloseAsync();
        await browser.DisposeAsync();
    }
}

Voor PDF-generatie met een hoog volume, overweeg het hergebruiken van browser-instances:

public class PdfGeneratorService : IDisposable
{
    private IBrowser? _browser;
    private readonly SemaphoreSlim _semaphore = new(1, 1);

    public async Task<byte[]> GeneratePdfAsync(string url)
    {
        await _semaphore.WaitAsync();
        try
        {
            // Reuse browser instance
            _browser ??= await Puppeteer.LaunchAsync(new LaunchOptions
            {
                Headless = true
            });

            await using var page = await _browser.NewPageAsync();
            await page.GoToAsync(url);
            return await page.PdfDataAsync();
        }
        finally
        {
            _semaphore.Release();
        }
    }

    public async ValueTask DisposeAsync()
    {
        if (_browser != null)
        {
            await _browser.CloseAsync();
            await _browser.DisposeAsync();
        }
        _semaphore.Dispose();
    }

    public void Dispose()
    {
        DisposeAsync().AsTask().Wait();
    }
}

7. De Schaaloptie - Kleinere tekst, meer inhoud

Soms moet je meer inhoud op een pagina passen:

var pdfData = await page.PdfDataAsync(new PdfOptions
{
    Format = PaperFormat.A4,
    Scale = 0.8m,  // 80% scale - fits more content
    PrintBackground = true
});

Maar wees voorzichtig - te klein en onleesbaar.

Real-World PDF-generatiepatroon

Hier is hoe ik PDF-generatie in productie doe:

public class InvoicePdfGenerator
{
    private readonly ILogger<InvoicePdfGenerator> _logger;

    public InvoicePdfGenerator(ILogger<InvoicePdfGenerator> logger)
    {
        _logger = logger;
    }

    public async Task<byte[]> GenerateInvoicePdfAsync(Invoice invoice)
    {
        await using var browser = await Puppeteer.LaunchAsync(new LaunchOptions
        {
            Headless = true,
            Args = new[]
            {
                "--no-sandbox",
                "--disable-setuid-sandbox",
                "--disable-dev-shm-usage"  // Overcome limited resource problems
            }
        });

        await using var page = await browser.NewPageAsync();

        // Set up console logging to debug issues
        page.Console += (_, e) =>
        {
            _logger.LogInformation("Browser console: {Message}", e.Message.Text);
        };

        try
        {
            // Generate HTML content (using Razor, or however you do it)
            var htmlContent = await GenerateInvoiceHtmlAsync(invoice);

            // Set content directly rather than navigating to URL
            await page.SetContentAsync(htmlContent, new NavigationOptions
            {
                WaitUntil = new[] { WaitUntilNavigation.Networkidle0 }
            });

            // Wait for fonts to load
            await page.EvaluateFunctionAsync("() => document.fonts.ready");

            // Force screen media type to avoid print media queries changing layout
            await page.EmulateMediaTypeAsync(MediaType.Screen);

            // Generate PDF
            var pdfData = await page.PdfDataAsync(new PdfOptions
            {
                Format = PaperFormat.A4,
                PrintBackground = true,
                MarginOptions = new MarginOptions
                {
                    Top = "10mm",
                    Right = "10mm",
                    Bottom = "10mm",
                    Left = "10mm"
                },
                PreferCSSPageSize = false
            });

            _logger.LogInformation("Generated PDF for invoice {InvoiceId}, size: {Size} bytes",
                invoice.Id, pdfData.Length);

            return pdfData;
        }
        catch (Exception ex)
        {
            _logger.LogError(ex, "Failed to generate PDF for invoice {InvoiceId}", invoice.Id);

            // Take a screenshot for debugging
            try
            {
                var screenshot = await page.ScreenshotDataAsync();
                _logger.LogWarning("Captured screenshot of failed PDF generation: {Size} bytes",
                    screenshot.Length);
                // Could save this to blob storage for debugging
            }
            catch
            {
                // Swallow screenshot errors
            }

            throw;
        }
    }

    private async Task<string> GenerateInvoiceHtmlAsync(Invoice invoice)
    {
        // Your HTML generation logic here
        // Could use Razor views, or any templating engine
        return $@"
<!DOCTYPE html>
<html>
<head>
    <meta charset='utf-8'>
    <style>
        @import url('https://fonts.googleapis.com/css2?family=Inter:wght@400;600;700&display=swap');

        body {{
            font-family: 'Inter', sans-serif;
            margin: 0;
            padding: 20px;
            color: #333;
        }}

        @media print {{
            .page-break {{
                page-break-before: always;
            }}

            .no-break {{
                page-break-inside: avoid;
            }}
        }}
    </style>
</head>
<body>
    <div class='no-break'>
        <h1>Invoice #{invoice.Number}</h1>
        <p>Date: {invoice.Date:yyyy-MM-dd}</p>
    </div>

    <!-- Invoice content -->
</body>
</html>";
    }
}

Landschap vs Portret

Eenvoudig maar vaak nodig:

var pdfData = await page.PdfDataAsync(new PdfOptions
{
    Format = PaperFormat.A4,
    Landscape = true,  // Horizontal orientation
    PrintBackground = true
});

Aangepaste paginagroottes

Niet beperkt tot standaardformaten:

var pdfData = await page.PdfDataAsync(new PdfOptions
{
    Width = "210mm",   // Custom width
    Height = "297mm",  // Custom height (this is A4, but you can use any size)
    PrintBackground = true
});

Andere praktische toepassingen voor PuppeteerSharp

Naast testen en PDF-generatie blinkt PuppeteerSharp uit op verschillende andere automatiseringstaken. Laten we de meest voorkomende real-world toepassingen verkennen.

Web Scraping voor Data Extractie

PuppeteerSharp is briljant voor het schrapen van JavaScript-zware sites waar traditionele HTML-parsers tekort komen:

public class ProductScraper
{
    public async Task<List<Product>> ScrapeProductsAsync(string url)
    {
        await using var browser = await Puppeteer.LaunchAsync(new LaunchOptions
        {
            Headless = true
        });

        await using var page = await browser.NewPageAsync();
        await page.GoToAsync(url, new NavigationOptions
        {
            WaitUntil = new[] { WaitUntilNavigation.Networkidle2 }
        });

        // Wait for products to render (adjust selector as needed)
        await page.WaitForSelectorAsync(".product-item");

        // Extract product data using JavaScript
        var products = await page.EvaluateFunctionAsync<List<Product>>(@"() => {
            return Array.from(document.querySelectorAll('.product-item')).map(item => ({
                name: item.querySelector('.product-name')?.textContent?.trim(),
                price: parseFloat(item.querySelector('.product-price')?.textContent?.replace('£', '')),
                imageUrl: item.querySelector('img')?.src,
                inStock: !item.querySelector('.out-of-stock')
            }));
        }");

        return products;
    }
}

Wanneer moet u het gebruiken:

  • Scraping single-page toepassingen (React, Vue, Angular)
  • Sites met oneindige scroll of luie belasting
  • Wanneer u moet interageren met de pagina (klik op knoppen, vul formulieren) voordat schrapen
  • Inhoud achter aanmeldmuren

Wanneer NIET te gebruiken:

  • Eenvoudige statische HTML schrapen (gebruik HtmlAgilityPack of HoekSharp in plaats daarvan - veel sneller en lichter)
  • Hoogvolume schrapen (browser overhead is significant)
  • Wanneer er een API beschikbaar is (altijd liever officiële API's dan schrapen!)

Geautomatiseerde schermafdrukgeneratie

Naast het testen, screenshots zijn nuttig voor thumbnails, previews, of archivering:

public class ScreenshotService
{
    public async Task<byte[]> CaptureWebsiteAsync(string url, int width = 1920, int height = 1080)
    {
        await using var browser = await Puppeteer.LaunchAsync(new LaunchOptions
        {
            Headless = true
        });

        await using var page = await browser.NewPageAsync();
        await page.SetViewportAsync(new ViewPortOptions
        {
            Width = width,
            Height = height
        });

        await page.GoToAsync(url, new NavigationOptions
        {
            WaitUntil = new[] { WaitUntilNavigation.Networkidle2 }
        });

        // Full page screenshot
        return await page.ScreenshotDataAsync(new ScreenshotOptions
        {
            FullPage = true,
            Type = ScreenshotType.Png
        });
    }

    public async Task<byte[]> CaptureElementAsync(string url, string selector)
    {
        await using var browser = await Puppeteer.LaunchAsync(new LaunchOptions
        {
            Headless = true
        });

        await using var page = await browser.NewPageAsync();
        await page.GoToAsync(url);

        var element = await page.WaitForSelectorAsync(selector);
        if (element == null)
        {
            throw new InvalidOperationException($"Element {selector} not found");
        }

        // Screenshot of specific element
        return await element.ScreenshotDataAsync();
    }
}

Praktische toepassingen:

  • Genereren van og:image tags voor blog posts
  • Thumbnails aanmaken voor website galeries
  • Webpagina's archiveren voor naleving
  • Het genereren van voorbeeldafbeeldingen voor het delen van links

Prestatiebewaking

Meet de laadprestaties van de pagina:

public class PerformanceMonitor
{
    public async Task<PerformanceMetrics> MeasurePagePerformanceAsync(string url)
    {
        await using var browser = await Puppeteer.LaunchAsync(new LaunchOptions
        {
            Headless = true
        });

        await using var page = await browser.NewPageAsync();

        var stopwatch = Stopwatch.StartNew();
        await page.GoToAsync(url, new NavigationOptions
        {
            WaitUntil = new[] { WaitUntilNavigation.Networkidle2 }
        });
        stopwatch.Stop();

        // Get performance metrics from the browser
        var metrics = await page.MetricsAsync();

        // Get performance timing data
        var performanceTiming = await page.EvaluateExpressionAsync<PerformanceTiming>(@"
            JSON.parse(JSON.stringify(performance.timing))
        ");

        return new PerformanceMetrics
        {
            TotalLoadTime = stopwatch.ElapsedMilliseconds,
            DomContentLoaded = performanceTiming.DomContentLoadedEventEnd - performanceTiming.NavigationStart,
            FirstPaint = metrics["FirstPaint"],
            LayoutCount = (int)metrics["LayoutCount"],
            ScriptDuration = metrics["ScriptDuration"]
        };
    }
}

Geautomatiseerd genereren van rapporten

Combineer HTML-templating met PDF-generatie voor geautomatiseerde rapportage:

public class MonthlyReportGenerator
{
    public async Task<byte[]> GenerateMonthlyReportAsync(ReportData data)
    {
        await using var browser = await Puppeteer.LaunchAsync(new LaunchOptions
        {
            Headless = true
        });

        await using var page = await browser.NewPageAsync();

        // Generate HTML report using your preferred templating engine
        var html = GenerateReportHtml(data);
        await page.SetContentAsync(html);

        // Wait for any charts to render (if using Chart.js, D3.js, etc.)
        await Task.Delay(2000);

        return await page.PdfDataAsync(new PdfOptions
        {
            Format = PaperFormat.A4,
            PrintBackground = true,
            DisplayHeaderFooter = true,
            HeaderTemplate = $@"
                <div style='font-size: 9px; margin: 0 auto; text-align: center;'>
                    Monthly Report - {data.Month:MMMM yyyy}
                </div>
            ",
            FooterTemplate = @"
                <div style='font-size: 9px; margin: 0 auto; text-align: center;'>
                    Page <span class='pageNumber'></span> of <span class='totalPages'></span>
                </div>
            ",
            MarginOptions = new MarginOptions
            {
                Top = "25mm",
                Bottom = "20mm",
                Left = "15mm",
                Right = "15mm"
            }
        });
    }
}

De kosten van "Gratis" PDF-generatie

Nu, hier is het ding over het gebruik van PuppeteerSharp voor PDF-generatie - het is "gratis" in de zin dat je niet betaalt voor een PDF-bibliotheek licentie, maar het is niet vrij in termen van middelen.

Elke browser instantie:

  • Gebruikt 100-200MB RAM
  • Vereist significante CPU voor rendering
  • Het duurt 2-5 seconden om een PDF te genereren (afhankelijk van complexiteit)

Vergelijk dit met speciale PDF-bibliotheken zoals:

  • iText (voorheen iTextSharp) - Commerciële licentie vereist (~ £50-300/jaar), maar genereert PDF's in milliseconden met kleine geheugen voetafdruk
  • QuestPDF - Vrije en open source onder MIT licentie, genereert PDF's van vloeiend C# code (geen HTML), razendsnel
  • PdfSharpCore - Gratis MIT licentie, maar beperkter in mogelijkheden

Wanneer PuppeteerSharp gebruiken voor PDF's:

  • U heeft al HTML sjablonen en wilt niet herschrijven in PDF layout code
  • U heeft een pixel-perfecte weergave van complexe weblay-outs nodig
  • Volume is laag (< 100 PDF's per uur)
  • U moet PDF's genereren van externe websites waar u geen controle over heeft

Wanneer kunt u speciale PDF-bibliotheken gebruiken:

  • Hoge volumeproductie (> 100 PDF's per uur)
  • Eenvoudige indeling (facturen, ontvangsten, verslagen)
  • Door hulpbronnen ingeperkte omgevingen
  • U heeft geavanceerde PDF-functies nodig (formulieren, handtekeningen, encryptie)

Een hybride aanpak

Soms is de beste oplossing is het gebruik van beide:

public class PdfService
{
    private readonly ILogger<PdfService> _logger;

    public async Task<byte[]> GeneratePdfAsync(PdfRequest request)
    {
        // Simple documents - use QuestPDF (fast, low resources)
        if (request.IsSimpleLayout)
        {
            return GenerateWithQuestPdf(request);
        }

        // Complex documents with web content - use PuppeteerSharp
        return await GenerateWithPuppeteerAsync(request);
    }

    private byte[] GenerateWithQuestPdf(PdfRequest request)
    {
        // QuestPDF code here - much faster for simple layouts
        return Document.Create(container =>
        {
            container.Page(page =>
            {
                page.Size(PageSizes.A4);
                page.Margin(2, Unit.Centimetre);
                page.Content().Text(request.Content);
            });
        }).GeneratePdf();
    }

    private async Task<byte[]> GenerateWithPuppeteerAsync(PdfRequest request)
    {
        // PuppeteerSharp code for complex layouts
        await using var browser = await Puppeteer.LaunchAsync(new LaunchOptions
        {
            Headless = true
        });

        await using var page = await browser.NewPageAsync();
        await page.SetContentAsync(request.HtmlContent);
        return await page.PdfDataAsync();
    }
}

Conclusie

PuppeteerSharp is een absolute game-changer voor E2E testen in mijn .NET projecten. Het is sneller dan Selenium, heeft een meer moderne API, en maakt gewoon over het algemeen testen minder van een karwei.

Dit is wat ik zou aanraden:

  1. Begin met PuppeteerSharp als je alleen Chrome/Chromium test. Het is eenvoudiger en sneller dan de alternatieven.

  2. Playwright gebruiken Als u multi-browser ondersteuning nodig hebt. Het heeft alle voordelen van PuppeteerSharp plus Firefox en Safari.

  3. Vermijd Selenium voor nieuwe projecten tenzij je een specifieke reden hebt om het te gebruiken (zoals IE11 ondersteuning, wat je hopelijk niet doet).

  4. Schrijf de tests verstandig. E2E tests zijn traag en kunnen bros zijn. Gebruik ze voor kritische gebruikers reizen, niet voor het testen van elk klein detail. Dat is waar eenheid en integratie testen voor zijn.

  5. Hou de tests geïsoleerd.. Elke test moet zijn eigen gegevens opzetten en na zichzelf opruimen.

  6. Hulpmethoden gebruiken Het basisklassepatroon dat ik liet zien houdt je testcode schoon en focust op wat je test, niet op hoe je het test.

E2E testen hoeft niet pijnlijk te zijn. Met de juiste gereedschappen en patronen, kan het eigenlijk heel aangenaam zijn. Geef PuppeteerSharp een go op uw volgende project - ik denk dat u zult aangenaam verrast.

Oké, ik ga nog meer testen schrijven.

Meer lezen

logo

© 2026 Scott Galloway — Unlicense — All content and source code on this site is free to use, copy, modify, and sell.