ScreenshotNeo

BlogHow-to

How to Run Selenium Tests in Parallel with SpecFlow and NUnit

Enable safe parallel execution for Selenium and SpecFlow tests with NUnit. Configure workers, isolate scenarios and browsers, and diagnose common failures.

By the ScreenshotNeo team4 October 20267 min read

Direct answer: Enable NUnit parallel execution at the feature or fixture level, set a worker limit that matches available browser and application capacity, and give every scenario its own WebDriver, state, and test data. For many SpecFlow/NUnit projects, assembly attributes are the starting point:

using NUnit.Framework;

[assembly: LevelOfParallelism(4)]
[assembly: Parallelizable(ParallelScope.Fixtures)]

This is illustrative, not universal: first confirm your NUnit and SpecFlow versions, how generated tests map features to NUnit fixtures, and what the runner discovers. The available SpecFlow documentation copy recommends parallelizing features rather than scenarios within one feature; verify that guidance against your installed version. NUnit attributes make tests eligible for parallel execution; a worker limit by itself does not.

This guide covers in-assembly parallelism. Selenium Grid is an optional remote execution layer when you need browser sessions on other machines or browser/platform combinations. Grid does not replace test isolation.

1. Check versions and the generated NUnit structure

Before changing configuration, record the target framework and versions of NUnit, the NUnit adapter or runner, SpecFlow, and Selenium WebDriver. Inspect generated tests or runner output to establish whether a feature becomes a fixture and whether the runner discovers those fixtures from one assembly. Parallel attributes apply to NUnit’s discovered test tree, so the generated structure determines their effect.

Do not assume that a method-level attribute on generated code is supported by your SpecFlow version or has the scope you intend. The SpecFlow-specific guidance in this article comes from an indexed third-party copy of historical documentation. Check the documentation for your installed version, especially for its NUnit provider, scenario scheduling limits, and context injection API.

2. Enable NUnit parallel execution at a safe scope

NUnit framework-level parallel execution is off by default. Parallelizable marks tests or fixtures as eligible, and its scope controls which nodes can be scheduled concurrently. LevelOfParallelism sets the maximum worker count, but the test hierarchy, attributes, runner options, and available work can produce lower concurrency. Runner command-line options can also override the worker cap.

For a suite whose generated NUnit fixtures correspond to features, an assembly-level starting point is:

using NUnit.Framework;

[assembly: LevelOfParallelism(4)]
[assembly: Parallelizable(ParallelScope.Fixtures)]

Choose the cap based on browser slots and the capacity of the application, database, and test data. Four is only an example, not a measured recommendation. NUnit documents its default worker count as Environment.ProcessorCount or 2, whichever is greater; that default is not a Selenium capacity recommendation. See the NUnit framework execution guide, Parallelizable, and LevelOfParallelism.

Keep same-feature scenarios in mind

The available SpecFlow documentation copy recommends parallel execution across features, not scenarios within a feature, for NUnit-based tests. Treat that as version-dependent: verify it against the installed SpecFlow documentation and generated test structure before enabling broader scopes. Start with fixture/feature scheduling and widen it only when the installed version supports the arrangement and representative tests remain stable.

Mark unsafe work as non-parallel

If a fixture depends on a shared resource that cannot be isolated, NUnit’s [NonParallelizable] can keep it from overlapping with parallel-eligible work. Use it narrowly, and explain the resource constraint in the test code or team documentation. Lowering the worker cap may be a better fix when the constraint affects many tests. See NUnit’s NonParallelizable documentation.

3. Isolate scenario context, browser sessions, and data

Parallel tests overlap in time. Static scenario state, shared fixture fields, reused browser sessions, and records selected or overwritten by multiple tests can create intermittent failures. NUnit also warns that parallel tests can interfere when they mutate shared fixture state without synchronization.

Use injected scenario-scoped context

Avoid static ScenarioContext or FeatureContext access for data belonging to a running scenario. The available SpecFlow documentation copy recommends constructor context injection into binding classes. Keep injected context and scenario-specific services scoped to the scenario, and give any shared service an intentional lifetime and thread-safety design. Confirm the injection API for your SpecFlow version.

Create and dispose a WebDriver per scenario

Selenium recommends a new WebDriver instance per test and cleanup after each test. In SpecFlow, a BeforeScenario/AfterScenario hook pair is a natural lifecycle boundary, as long as the driver is stored in scenario-scoped state rather than a static field. Always quit the driver on failure as well as success so sessions do not accumulate.

// Illustrative lifecycle pseudocode: adapt constructor injection and hooks
// to the SpecFlow version and dependency-injection setup in your project.
[Binding]
public sealed class BrowserHooks
{
    private readonly ScenarioDriver _scenarioDriver;

    public BrowserHooks(ScenarioDriver scenarioDriver)
    {
        _scenarioDriver = scenarioDriver;
    }

    [BeforeScenario]
    public void StartBrowser()
    {
        _scenarioDriver.Driver = new ChromeDriver();
    }

    [AfterScenario]
    public void StopBrowser()
    {
        _scenarioDriver.Driver?.Quit();
        _scenarioDriver.Driver?.Dispose();
        _scenarioDriver.Driver = null;
    }
}

// ScenarioDriver must be registered with scenario scope in the
// project's supported SpecFlow DI mechanism. It must not be static.

This snippet illustrates ownership and cleanup, not a complete SpecFlow registration recipe; hook and DI details depend on the installed version. If driver startup can partially fail, ensure cleanup logic also handles a missing or partially created driver. Selenium’s guidance: avoid sharing state.

Make test data independent

  • Create unique records per scenario, or derive deterministic unique keys from a run and scenario identifier.
  • Avoid tests selecting “the first” shared record or changing global account settings.
  • Clean up created data, and account for stale records left by interrupted runs.
  • Use a separate database or namespace per worker when the system supports it.
  • Audit filesystem paths, queues, email inboxes, ports, and external accounts for collisions too.

4. Choose the right layer of parallelism

Option What runs concurrently Useful when Trade-off
NUnit framework parallelism Eligible tests or fixtures in one assembly on worker threads Running independent SpecFlow feature fixtures together Shared process state and thread safety need attention; same-feature scenario parallelism is version-sensitive
NUnit engine parallelism Separate test assemblies in different processes The suite is already split across assemblies Processes add startup and resource cost; databases, files, and external services may still be shared
Selenium Grid Remote browser sessions on nodes, machines, or browser/platform combinations Local machines lack the desired browser capacity or coverage Requires Grid infrastructure and available sessions; it does not fix shared test data or state

NUnit framework parallelism across tests in an assembly and engine parallelism across assemblies are distinct. The NUnit engine documentation describes assembly-level execution. Selenium describes Grid’s role in its overview; remote execution requires Selenium Server, with setup depending on the current release and deployment. Size NUnit workers to usable Grid slots rather than configuring more workers than the remote capacity can serve.

5. Increase concurrency gradually

  1. Run the suite sequentially and record existing failures and duration.
  2. Enable feature/fixture parallelism with a small worker cap.
  3. Repeat representative tests and look for data collisions, shared-state races, and leaked browser sessions.
  4. Increase the cap only while results remain stable and browser, application, database, and Grid capacity are available.
  5. Keep a short list of tests excluded from parallel execution and the reason each one still needs shared resources.

There is no sourced benchmark for a particular speedup with this stack. More workers can increase contention and failure rates; throughput depends on how much time tests spend waiting and on the capacity of every dependency.

6. Troubleshoot common failures

Symptom Likely cause Fix
Tests still run one at a time No tests are marked parallelizable, the attribute scope does not match the discovered structure, or the runner configuration limits execution Inspect NUnit discovery and generated fixtures; confirm assembly attributes are compiled into the test assembly and check runner options
Parallel attribute fails to compile NUnit references or versions do not expose the attribute/enum used Check the installed NUnit framework package and use documentation matching that version
Intermittent failures only under parallel load Shared static/fixture state, shared test records, or non-thread-safe service Move state to scenario scope, create unique data, or narrowly mark the affected fixture non-parallel
Browser actions appear in another scenario Two scenarios share a WebDriver or a static driver holder Create one driver per scenario and keep its reference in scenario-scoped state
Browser processes or sessions remain after failures Cleanup runs only on the success path or teardown is skipped Quit and dispose in an after-scenario cleanup path that handles failure and partial initialization
Grid returns session creation errors or queues work Worker count exceeds available slots, or the remote endpoint/capabilities are incorrect Check endpoint and capabilities, inspect available Grid capacity, and cap workers to sessions the Grid can serve
Failures also occur sequentially Application behavior, test assumptions, or environment issues unrelated to parallel scheduling Reproduce sequentially and diagnose as an ordinary test failure before attributing it to parallelism

Or skip the browser setup

If your task is to capture a web page rather than test browser interactions, ScreenshotNeo is a website screenshot API and MCP server. One GET request returns a PNG, JPEG, WebP, or PDF. For documentation and all parameters, see the ScreenshotNeo API docs.

curl -G "https://api.screenshotneo.com/v1/shot" \
  -d access_key=YOUR_API_KEY \
  --data-urlencode url=https://stripe.com \
  -o shot.webp
import requests

r = requests.get(
    "https://api.screenshotneo.com/v1/shot",
    params={"access_key": "YOUR_API_KEY", "url": "https://stripe.com"},
    timeout=90,
)
open("shot.webp", "wb").write(r.content)
const q = new URLSearchParams({ access_key: 'YOUR_API_KEY', url: 'https://stripe.com' });
const res = await fetch(`https://api.screenshotneo.com/v1/shot?${q}`);
if (!res.ok) throw new Error(`ScreenshotNeo returned HTTP ${res.status}`);
await Bun.write('shot.webp', res);

ScreenshotNeo accepts cookie and consent banners and removes more than 60 known consent platforms, newsletter popups, and chat widgets before capture; each step can be turned off. Bot checks, blank pages, failed loads, timeouts, and cache hits are not billed, and responses identify page verdict and billing in headers. Its MCP server provides take_screenshot, get_page_info, and capture_pdf for AI agents. The free plan includes 1,000 screenshots a month with no card; paid plans start at $5 for 3,000. Start free at ScreenshotNeo sign-up.

FAQ

Does setting LevelOfParallelism enable concurrency by itself?

No. It caps worker count; NUnit also needs parallel-eligible tests.

Will Selenium Grid make my tests safe to parallelize?

No. Grid supplies remote browser execution capacity. Each test still needs isolated state, browser sessions, and data.

Should I parallelize every scenario?

Only when the installed SpecFlow version and generated NUnit structure support it and each scenario is independent. Feature/fixture-level scheduling is the safer starting point described here.

How many workers should I configure?

Start with a small cap supported by browser slots and application/test-data capacity. Increase it based on repeatable stability and observed resource limits, not NUnit’s default alone.