Cloud Testing Platforms
Run Selenium on BrowserStack, Sauce Labs, LambdaTest and similar services: the vendor-prefixed capabilities, tunnels to private environments, marking results, cost control, and when a self-hosted Grid is cheaper.
Cloud platforms are Selenium Grids someone else runs, with real devices, every browser version back a decade, video, logs and a dashboard. They exist because Selenium is a W3C protocol: your tests send the same commands to hub.browserstack.com as to localhost:4444. This lesson covers what changes (capabilities, networking, result reporting) and how to decide between cloud and self-hosted.
What You Get, and What It Costs
| Benefit | Detail |
|---|---|
| Browser and OS matrix | Safari on real macOS, old Chrome and Firefox builds, Windows and Linux, real iOS and Android via Appium |
| Zero infrastructure | No Grid to patch, no images to build, no autoscaling to tune |
| Evidence | Video, screenshots, console and network logs per session, in a dashboard |
| Tunnels | Test apps on localhost or behind a firewall through a vendor agent |
Costs: per-parallel-session pricing that grows with suite size, network latency of tens of milliseconds per command (a 500-command test gains 10 to 30 seconds), vendor lock-in through capability formats and SDKs, and data leaving your network.
Vendor-Prefixed Capabilities
Selenium 4 speaks W3C, so vendor settings must sit under a prefixed key. The three main platforms:
| Vendor | Endpoint | Options key |
|---|---|---|
| BrowserStack | https://hub-cloud.browserstack.com/wd/hub | bstack:options |
| Sauce Labs | https://ondemand.<region>.saucelabs.com/wd/hub | sauce:options |
| LambdaTest (TestMu AI) | https://hub.lambdatest.com/wd/hub | LT:Options |
Credentials go in environment variables, never in code.
String user = System.getenv("BROWSERSTACK_USERNAME");String key = System.getenv("BROWSERSTACK_ACCESS_KEY");
ChromeOptions options = new ChromeOptions();options.setBrowserVersion("latest");options.setPlatformName("Windows 11");
Map<String, Object> bstack = new HashMap<>();bstack.put("projectName", "Storefront");bstack.put("buildName", System.getenv().getOrDefault("CI_BUILD", "local"));bstack.put("sessionName", "checkout applies coupon");bstack.put("local", "true"); // route through the tunnelbstack.put("debug", "true"); // screenshots per stepbstack.put("networkLogs", "true");bstack.put("seleniumVersion", "4.48.0");options.setCapability("bstack:options", bstack);
URL url = new URL("https://" + user + ":" + key + "@hub-cloud.browserstack.com/wd/hub");RemoteWebDriver driver = new RemoteWebDriver(url, options);driver.setFileDetector(new LocalFileDetector());import os
options = webdriver.ChromeOptions()options.browser_version = "latest"options.platform_name = "Windows 11"options.set_capability("bstack:options", { "projectName": "Storefront", "buildName": os.getenv("CI_BUILD", "local"), "sessionName": "checkout applies coupon", "local": "true", "debug": "true", "networkLogs": "true", "seleniumVersion": "4.48.0",})
url = f"https://{os.environ['BROWSERSTACK_USERNAME']}:{os.environ['BROWSERSTACK_ACCESS_KEY']}@hub-cloud.browserstack.com/wd/hub"driver = webdriver.Remote(command_executor=url, options=options)driver.file_detector = LocalFileDetector()const options = new chrome.Options().setBrowserVersion('latest').setPlatformName('Windows 11').set('bstack:options', { projectName: 'Storefront', buildName: process.env.CI_BUILD ?? 'local', sessionName: 'checkout applies coupon', local: 'true', debug: 'true', networkLogs: 'true', seleniumVersion: '4.48.0',});
const url = `https://${process.env.BROWSERSTACK_USERNAME}:${process.env.BROWSERSTACK_ACCESS_KEY}@hub-cloud.browserstack.com/wd/hub`;const driver = await new Builder().usingServer(url).forBrowser('chrome').setChromeOptions(options).build();var options = new ChromeOptions { BrowserVersion = "latest", PlatformName = "Windows 11" };options.AddAdditionalOption("bstack:options", new Dictionary<string, object>{ ["projectName"] = "Storefront", ["buildName"] = Environment.GetEnvironmentVariable("CI_BUILD") ?? "local", ["sessionName"] = "checkout applies coupon", ["local"] = "true", ["debug"] = "true", ["networkLogs"] = "true", ["seleniumVersion"] = "4.48.0",});
var user = Environment.GetEnvironmentVariable("BROWSERSTACK_USERNAME");var key = Environment.GetEnvironmentVariable("BROWSERSTACK_ACCESS_KEY");var driver = new RemoteWebDriver(new Uri($"https://{user}:{key}@hub-cloud.browserstack.com/wd/hub"), options);driver.FileDetector = new LocalFileDetector();Sauce Labs uses sauce:options with build, name, tunnelName; LambdaTest uses LT:Options with build, name, tunnel. Each vendor’s capability generator produces the exact map for your browser and OS combination.
Tunnels to Private Environments
Cloud browsers cannot reach localhost or a staging server behind your firewall. Each vendor ships a tunnel binary (BrowserStack Local, Sauce Connect, LambdaTest Tunnel) that opens an outbound connection from your network; browsers route through it when the capability (local, tunnelName, tunnel) is set.
In CI, start the tunnel before tests and stop it after; vendor GitHub Actions and Jenkins plugins do this. Name tunnels per job (localIdentifier) so parallel pipelines do not share one.
Marking Results
The cloud dashboard does not know whether your assertions passed. Tell it in the teardown hook through the vendor’s JavaScript executor command or REST API.
// BrowserStack: executor commandString status = failed ? "failed" : "passed";String reason = failed ? cause.getMessage().replace("\"", "'") : "";((JavascriptExecutor) driver).executeScript( "browserstack_executor: {\"action\": \"setSessionStatus\", \"arguments\": {\"status\":\"" + status + "\", \"reason\": \"" + reason + "\"}}");
// Sauce Labs((JavascriptExecutor) driver).executeScript("sauce:job-result=" + status);
// LambdaTest((JavascriptExecutor) driver).executeScript("lambda-status=" + status);import json
status = "failed" if failed else "passed"
# BrowserStackdriver.execute_script("browserstack_executor: " + json.dumps({ "action": "setSessionStatus", "arguments": {"status": status, "reason": str(exc)[:250] if failed else ""},}))
# Sauce Labsdriver.execute_script(f"sauce:job-result={status}")
# LambdaTestdriver.execute_script(f"lambda-status={status}")const status = failed ? 'failed' : 'passed';
// BrowserStackawait driver.executeScript(`browserstack_executor: ${JSON.stringify({action: 'setSessionStatus', arguments: { status, reason: failed ? err.message.slice(0, 250) : '' },})}`);
// Sauce Labsawait driver.executeScript(`sauce:job-result=${status}`);
// LambdaTestawait driver.executeScript(`lambda-status=${status}`);var status = failed ? "failed" : "passed";var js = (IJavaScriptExecutor)driver;
// BrowserStackjs.ExecuteScript("browserstack_executor: " + JsonSerializer.Serialize(new{ action = "setSessionStatus", arguments = new { status, reason = failed ? message[..Math.Min(250, message.Length)] : "" },}));
// Sauce Labsjs.ExecuteScript($"sauce:job-result={status}");
// LambdaTestjs.ExecuteScript($"lambda-status={status}");Also set sessionName (or name) from the test name and buildName from the CI build id so the dashboard groups sessions the way your pipeline does.
Cloud-Specific Behaviour to Plan For
- Latency: batch where possible. Read several values with one
executeScriptinstead of severalgetTextcalls. UsepageLoadStrategy = eager. - Uploads need
LocalFileDetector; downloads use vendor-specific APIs or are best asserted via the app’s UI. - Timeouts: vendors kill idle sessions (typically 90 seconds without a command) and long sessions (30 minutes to 2 hours). Keep tests short.
- Parallel limits: exceeding your plan queues sessions; align runner parallelism with the plan.
- Mobile: real devices use Appium capabilities (
appium:prefix), same driver classes. - BiDi: supported on major vendors for Chromium and Firefox; check the vendor’s Selenium version support before relying on it.
Cloud vs Self-Hosted Grid
| Factor | Cloud | Self-hosted (Docker or Kubernetes) |
|---|---|---|
| Time to first test | Minutes | Hours to days |
| Browser matrix | Everything, including Safari and real devices | Chrome, Edge, Firefox on Linux; Safari needs a Mac |
| Cost at scale | Linear per parallel session; expensive above roughly 20 to 50 parallels | Mostly fixed after setup; cheap per additional session |
| Latency | Tens of ms per command | Single-digit ms in the same network |
| Data locality | Traffic leaves your network (tunnel encrypts it) | Stays internal |
| Maintenance | Vendor’s | Yours: upgrades, images, autoscaling, video storage |
A common and sensible split: self-hosted Grid for the bulk Chrome runs on every pull request, cloud for the nightly cross-browser and real-device matrix.
Summary
- Cloud platforms are remote Grids; only the URL, credentials and vendor-prefixed options change.
- Use tunnels for private environments,
LocalFileDetectorfor uploads, and mark results from your teardown hook. - Design for latency and session limits: fewer commands, shorter tests, parallelism matched to the plan.
- Choose cloud for breadth and speed to start, self-hosted for volume and locality, or both.