<?xml version="1.0" encoding="UTF-8"?><rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom" version="2.0" xmlns:cc="http://cyber.law.harvard.edu/rss/creativeCommonsRssModule.html">
    <channel>
        <title><![CDATA[Stories by Boni García on Medium]]></title>
        <description><![CDATA[Stories by Boni García on Medium]]></description>
        <link>https://medium.com/@boni.gg?source=rss-7118f7714c53------2</link>
        <image>
            <url>https://cdn-images-1.medium.com/fit/c/150/150/1*_TbY9dIa4wSFbGd9K6Cn2A.jpeg</url>
            <title>Stories by Boni García on Medium</title>
            <link>https://medium.com/@boni.gg?source=rss-7118f7714c53------2</link>
        </image>
        <generator>Medium</generator>
        <lastBuildDate>Sat, 06 Jun 2026 19:10:11 GMT</lastBuildDate>
        <atom:link href="https://medium.com/@boni.gg/feed" rel="self" type="application/rss+xml"/>
        <webMaster><![CDATA[yourfriends@medium.com]]></webMaster>
        <atom:link href="http://medium.superfeedr.com" rel="hub"/>
        <item>
            <title><![CDATA[Implementing an MCP Server in Java]]></title>
            <link>https://medium.com/@boni.gg/implementing-an-mcp-server-in-java-4a08c509ee7f?source=rss-7118f7714c53------2</link>
            <guid isPermaLink="false">https://medium.com/p/4a08c509ee7f</guid>
            <dc:creator><![CDATA[Boni García]]></dc:creator>
            <pubDate>Sat, 18 Apr 2026 10:01:23 GMT</pubDate>
            <atom:updated>2026-04-18T10:01:23.205Z</atom:updated>
            <content:encoded><![CDATA[<p>This story summarizes a talk by <a href="https://bonigarcia.dev/">Boni García</a> at <a href="https://www.javaday.istanbul/">Java Day Istanbul</a>, Turkey, on April 18, 2026. You can find the original slides of this talk <a href="https://bonigarcia.dev/slides/2026-JavaDayIstanbul-Implementing_an_MCP_Server_in_Java-v1.pdf">here</a>.</p><h3>What is MCP?</h3><p><a href="https://modelcontextprotocol.io/">Model Context Protocol (MCP)</a> is an open standard for connecting AI agents to external services. It was created by Anthropic in 2024 and is now part of the Linux Foundation’s <a href="https://aaif.io/">Agentic AI Foundation</a>. In practice, MCP defines a common contract between an MCP client and one or more MCP servers, enabling models to interact with tools and external data sources consistently rather than relying on one-off integrations.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*9yZ9tHd9OPhU9pu_-4o9HA.png" /></figure><p>A useful way to think about MCP is as a boundary layer between the agent and the outside world. The agent handles prompts, memory, reasoning, and responses. The MCP client sits on the agent side. MCP servers sit on the system side and expose capabilities such as tools, resources, and prompts over a transport. That separation is valuable because it lets us evolve the implementation behind a server without changing the contract the agent sees.</p><h3>MCP servers</h3><p>An MCP server is simply an adapter that exposes selected capabilities from an external system. That system might be a filesystem, a source control platform, a container platform, a documentation engine, or an internal enterprise API. The server translates between MCP concepts and the underlying implementation details.</p><p>There is already a healthy ecosystem of MCP servers. Some examples are the <a href="https://github.com/modelcontextprotocol/servers/tree/main/src/filesystem">Filesystem MCP Server</a>, the <a href="https://github.com/github/github-mcp-server">GitHub MCP Server</a>, the <a href="https://github.com/docker/hub-mcp/">Docker MCP Server</a>, or <a href="https://github.com/upstash/context7">Context7</a>. Those examples matter because they show the breadth of the model: MCP is not limited to one category of automation. It can encompass local tools, SaaS APIs, developer platforms, and knowledge systems under a single protocol umbrella.</p><h3>Implementing an MCP server in Java</h3><p>For Java developers, that makes MCP especially interesting. Java already has mature ecosystems for HTTP services, security, observability, validation, browser automation, enterprise integration, and cloud deployment. So the real question is not whether Java can implement MCP servers. The interesting question is which Java style gives us the best trade-offs for a given team and deployment model.</p><p>There are four main alternatives to build an MCP server with Java:</p><ol><li><a href="https://java.sdk.modelcontextprotocol.io/latest/">The official MCP Java SDK</a></li><li><a href="https://docs.spring.io/spring-ai/reference/api/mcp/mcp-overview.html">Spring AI’s MCP support</a></li><li><a href="https://docs.quarkiverse.io/quarkus-mcp-server/dev/index.html">Quarkus MCP Server</a></li><li><a href="https://micronaut-projects.github.io/micronaut-mcp/latest/guide/">Micronaut MCP</a></li></ol><p>They all target the same protocol, but they optimize for very different priorities.</p><p>The official SDK provides the most direct access to the protocol model and transport layer. Spring AI provides the most familiar enterprise developer experience for teams that already live in Spring Boot. Quarkus offers the strongest documented story for performance, native deployment, observability, and build-time validation. Micronaut sits in an interesting middle ground, building on top of the official SDK while adding Micronaut-style dependency injection and compile-time schema generation.</p><p>One important disclaimer before we start: this is an architecture and documentation comparison, not a benchmark.</p><h3>1. The official MCP Java SDK</h3><p>The official MCP Java SDK is the reference option. It exposes the protocol model directly, including tools, resources, prompts, completions, sampling, elicitation, progress, and logging. It supports synchronous and asynchronous programming models, and the core module ships with built-in STDIO, SSE, and Streamable HTTP transports without requiring an external web framework.</p><p>From an API design perspective, this option is explicit. You build <em>McpSyncServer</em> or <em>McpAsyncServer</em>, declare capabilities, define tools with <em>Tool.builder()</em>, provide JSON Schema, and wire handlers manually. Resources, prompts, subscriptions, and notifications follow the same pattern.</p><h3>Strengths</h3><p>The SDK is ideal when you want protocol-level control. It is also the cleanest choice if you want to embed MCP into an existing Java application without adopting a heavier framework to expose a few tools. Its concrete strengths are:</p><ul><li>Direct access to sync and async server APIs</li><li>Built-in support for stateful and stateless server patterns</li><li>Lightweight STDIO transport with non-blocking message processing</li><li>Pluggable JSON serialization</li><li>Pluggable authorization hooks</li><li>DNS rebinding protection via Host and Origin validation</li></ul><p>The SDK docs give you the primitives, but they do not offer the same batteries-included experience you get with a framework integration. You are the one deciding how to package the server, how to integrate authentication, how to structure dependency injection, how to wire validation, how to expose metrics, and how to test beyond the protocol layer.</p><h3>Security</h3><p>It has low-level security hooks. The documentation explicitly mentions pluggable authorization hooks and DNS rebinding protection. Streamable HTTP transports also support security validation. If you need OAuth2 resource server integration, role-based policies, token validation, or method-level authorization, you will either integrate these concerns yourself or use the surrounding platform to handle them.</p><h3>Performance</h3><p>The SDK docs describe the STDIO transport as lightweight and non-blocking. That is a useful signal, but the official documentation does not position the SDK as a performance-tuned application platform in the same way Quarkus does.</p><h3>Best fit</h3><p>Choose the official MCP Java SDK when:</p><ul><li>You want maximum control over MCP internals.</li><li>You want the leanest abstraction layer.</li><li>You do not want a large framework dependency.</li></ul><h3>2. Spring AI</h3><p>Spring AI wraps the official MCP Java SDK in the Spring Boot way: starters, auto-configuration, annotation scanning, and a declarative programming model.</p><p>Spring AI offers dedicated MCP Boot starters for STDIO, WebMVC, and WebFlux, plus support for SSE, Streamable HTTP, and stateless Streamable HTTP depending on the selected starter and configuration. On top of that, the MCP annotations module adds <em>@McpTool</em>, <em>@McpResource</em>, <em>@McpPrompt</em>, and <em>@McpComplete</em>, with automatic JSON schema generation and request-context injection.</p><h3>Strengths</h3><p>Spring AI’s biggest strength is developer productivity inside the Spring ecosystem:</p><ul><li>Boot starters for transport selection and auto-configuration</li><li>Annotation-based registration of tools, resources, prompts, and completions</li><li>Automatic bean scanning and registration</li><li>Support for sync and async modes</li><li>Support for stateful and stateless server modes</li><li>Access to Spring’s existing ecosystem for configuration, packaging, deployment, security, and operations</li></ul><h3>Security</h3><p>There is a dedicated MCP Security module that adds OAuth2 resource server support, API key authentication, an MCP-oriented authorization server, and fine-grained access control for tools and resources. The docs also show the direct use of Spring Security’s <em>@PreAuthorize</em> method, and tool methods can access the current authentication through <em>SecurityContextHolder</em>. However, the current documentation also contains important caveats:</p><ul><li>The security module is marked work in progress.</li><li>It is community-driven rather than officially endorsed by Spring AI or the MCP project.</li><li>It is compatible only with Spring WebMVC-based servers.</li><li>This security module does not support deprecated SSE.</li><li>This security module does not support webFlux-based servers.</li><li>Opaque tokens are not supported, and the docs recommend JWT.</li></ul><h3>Performance</h3><p>The Spring MCP docs emphasize flexibility, starter-based integration, multiple transports, and declarative annotations. They do not position MCP support around low-memory or instant-startup claims.</p><p>That means performance in Spring AI MCP is less a property of the MCP layer and more a property of how you run Spring Boot. In practice, that usually means a heavier runtime footprint than the raw SDK and a less aggressively optimized story than Quarkus native.</p><h3>Best fit</h3><p>Choose the Spring AI to implement an MCP server when:</p><ul><li>Your team already builds Java backends with Spring Boot; this is the shortest path from “we want an MCP server” to working code.</li><li>You want the shortest path from annotated Java methods to an MCP server.</li><li>You want strong alignment with Spring Security.</li></ul><h3>3. Quarkus</h3><p>Quarkus MCP Server is the most opinionated and operationally ambitious option of the four. The documentation is strong. It does not just explain how to expose tools. It also covers performance, build-time validation, multiple isolated servers, security, observability, testing, and guardrails.</p><h3>Strengths</h3><p>Quarkus is designed around build-time analysis. The MCP extension follows that philosophy. The docs state that tools, resources, and prompts are discovered and validated at build time, with zero reflection, and that native executables can start in milliseconds and run on around 30 MB of RAM. Concrete strengths appointed by its documentation are:</p><ul><li>Multiple transports including STDIO, HTTP, SSE, and WebSocket</li><li>Full CDI integration</li><li>Reactive execution on Vert.x</li><li>Support for virtual threads</li><li>Multiple isolated MCP servers in a single application</li><li>Different endpoints, feature sets, and security policies per server</li><li>Micrometer-based metrics integration</li><li>A dedicated testing library called McpAssured</li><li>Validation integration through Hibernate Validator</li><li>Tool guardrails that run before invocation</li><li>OIDC and OAuth2 protected resource metadata support for MCP authorization workflows</li></ul><p>Observability is documented through Micrometer integration with metrics for active connections, request durations, and success and failure rates. Testing is documented through McpAssured, which supports SSE, Streamable HTTP, WebSocket, and STDIO. Input validation can be pushed into Jakarta Bean Validation and reflected into the tool schema generation. Guardrails can reject or transform tool inputs before they are invoked.</p><h3>Security</h3><p>Instead of bolting on a separate MCP security library, Quarkus provides a platform-level approach by integrating MCP security concerns into the same security infrastructure you already use for other Quarkus services.</p><p>HTTP transports can be secured through the Quarkus web security layer. Annotated methods can also use <em>@Authenticated</em> and <em>@RolesAllowed</em>. The docs strongly recommend restricting CORS to trusted origins for Streamable HTTP endpoints. For OIDC integration, the docs show support for audience validation and protected resource metadata discovery to align with the MCP authorization specification.</p><p>The one caveat documented explicitly is that, when authentication fails, method-level annotation security returns an MCP protocol error code rather than an HTTP status code.</p><h3>Performance</h3><p>Quarkus is the only one of the four whose MCP documentation makes strong, explicit runtime claims:</p><ul><li>Native executables start in milliseconds</li><li>Production servers run on about 30 MB of RAM</li><li>The build-time model eliminates reflection</li><li>Vert.x provides non-blocking I/O</li><li>Virtual threads are also supported</li></ul><h3>Best fit</h3><p>Choose Quarkus MCP Server when:</p><ul><li>Performance, startup time, container density, and cold-start behavior are first-class requirements.</li><li>You want native-image-friendly deployment.</li><li>You need strong observability and testing out of the box.</li></ul><h3>4. Micronaut</h3><p>Micronaut MCP is rooted in the official MCP Java SDK, as the guide explicitly states. It adds Micronaut-style configuration, dependency injection, transport-aware server creation, annotation-driven tool definitions, and compile-time JSON Schema generation using Micronaut JSON Schema.</p><p>At the time of this writing, the current doc is labeled version 0.0.20, suggesting this integration is earlier in its lifecycle than the other three.</p><h3>Strengths</h3><p>Micronaut MCP provides a higher-level programming model than the raw SDK, without moving as far into framework abstraction as Spring AI:</p><ul><li>Configuration-driven transport selection for STDIO and HTTP</li><li>Sync and reactive modes</li><li>SDK server instance selection based on transport and reactive mode</li><li><em>@Tool</em>, <em>@Prompt</em>, <em>@Resource</em>, and template annotations</li><li>Micronaut-specific transport context with access to authenticated user, locale, and host</li><li>Compile-time generation of tool input and output JSON Schema</li><li>Factory-based definitions as an alternative to annotations</li><li>Helper support for <em>SearchTool </em>and <em>FetchTool </em>implementations.</li></ul><h3>Security</h3><p>Micronaut MCP is the least explicit of the four on security in its current guide. The guide exposes <em>MicronautMcpTransportContext</em>, which provides access to the authenticated user and related request information. But unlike Spring and Quarkus, the MCP guide currently lacks a dedicated security section with concrete authentication and authorization patterns for MCP endpoints.</p><h3>Performance</h3><p>Micronaut, as a framework, is generally associated with low startup overhead and strong compile-time processing. Still, the Micronaut MCP guide itself does not make the same explicit runtime claims as Quarkus does.</p><p>What the guide does document is compile-time schema generation and a design closely tied to the MCP Java SDK. That suggests a reasonably efficient stack, but not enough to claim superiority without measurement.</p><h3>Best fit</h3><p>Choose the Micronaut for MCP when:</p><ul><li>Your team already prefers Micronaut.</li><li>You want a lighter framework style than Spring.</li><li>You like compile-time schema generation and explicit SDK alignment.</li><li>You are comfortable accepting a younger MCP integration with some current feature gaps.</li></ul><h3>Head-to-head comparison</h3><p>Reducing the comparison to one sentence per option, the picture looks like this: the official Java SDK is the protocol-first choice, Spring AI is the enterprise-default choice, Quarkus is the production-optimization choice, and Micronaut is the lightweight and promising choice.</p><p>From an abstraction perspective, the official SDK sits closest to the protocol. You define capabilities explicitly, wire handlers yourself, and stay very close to the transport and schema models. Spring AI and Quarkus move higher up the stack with annotation-driven programming models and framework integration. Micronaut also raises the abstraction level, but in a slightly more restrained way, staying closely aligned with the official SDK while adding dependency injection and compile-time schema generation.</p><p>From an operational perspective, the differences become even clearer. Spring AI benefits from the broader Spring Boot ecosystem and is probably the safest recommendation for an average enterprise Java team that already runs Spring services in production. Quarkus stands out for its documented focus on startup time, native-image friendliness, observability, validation, and testing. Micronaut looks attractive to teams that want a lighter framework and strong compile-time processing, but its MCP integration still seems newer than the other options. The raw SDK remains the most direct and portable implementation, but it also asks more from you in terms of packaging, security integration, and platform concerns.</p><h3>Case study: basic Selenium MCP server</h3><p>For the case study, I wanted something concrete, easy to understand, and representative of real tool integration. Selenium was a natural fit, as it is a browser automation library that provides a clear mapping between ordinary Java logic and MCP-exposed tools.</p><p>This example was implemented as a part of my book <em>Context engineering: the art and science of shaping context-aware AI systems</em>, to be published by <a href="https://www.manning.com/">Manning</a> in 2026. Find the complete source code in the open-source companion <a href="https://github.com/bonigarcia/context-engineering/tree/main/ch04/java">GitHub repository</a> for this book.</p><p>The Selenium server in this talk exposes four simple tools: <em>open_browser</em>, <em>navigate_url</em>, <em>get_browser_text</em>, and <em>close_browser</em>. That is intentionally small, but it is enough to demonstrate the whole pattern. An AI agent can request a browser session, navigate to a page, extract the visible text, and close the session. In other words, we take a familiar browser automation workflow and wrap it in an MCP contract that any compatible client can call.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*LAEnJ1xzo3JHhIpKTeLqnA.png" /></figure><p>The design lesson is more important than the four tools themselves: keep the business logic separate from the MCP adapter. That separation appears as a simple layered model. At the top, there is the AI agent. Then comes the tool contract. Then the MCP adapter layer, implemented with the Java SDK, Spring, Quarkus, or Micronaut. Finally, the browser manager contains the Selenium code. Once you structure the application that way, the core automation logic stays stable while the framework-facing adapter becomes replaceable.</p><p>Across the four implementations, the user-facing tool surface and the Selenium logic are essentially the same. What changes are the amount of boilerplate, the annotation model, the lifecycle integration, and the surrounding platform capabilities? The Java SDK version is explicit and low-level. The Spring version is concise and familiar for Spring Boot teams. The Quarkus version feels strongly integrated into the platform model. The Micronaut version is similarly compact, with a reactive flavor and compile-time style.</p><p>For inspection and manual testing, I used the MCP Inspector to launch the packaged server and interactively exercise the tools.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*pxxW9b1KSKfkOOvqolPz_A.png" /></figure><h3>Conclusions</h3><p>There is no single best way to implement an MCP server in Java. There are four good options, and each one is best when its design assumptions match your context.</p><p>Choose the official MCP Java SDK for the most direct, portable, and protocol-centric implementation. Choose Spring AI when your team already lives in Spring Boot and wants the safest path from annotated Java methods to an MCP server. Choose Quarkus when production features such as performance, security integration, observability, validation, testing, and native deployment are first-class concerns. Choose Micronaut when you prefer a lighter framework style, like compile-time processing, and are comfortable adopting a younger but promising MCP integration.</p><p>The Selenium case study reinforces the main architectural takeaway: the smartest long-term design is to keep the business capability independent from the MCP framework layer. Once you do that, the protocol adapter becomes a replaceable detail, and the decision between SDK, Spring, Quarkus, and Micronaut becomes a question of ergonomics and operations rather than a rewrite of your core logic.</p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=4a08c509ee7f" width="1" height="1" alt="">]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[WebDriver BiDi: The Future of Browser Automation is Now]]></title>
            <link>https://medium.com/@boni.gg/webdriver-bidi-the-future-of-browser-automation-is-now-1ca0d5ee74dd?source=rss-7118f7714c53------2</link>
            <guid isPermaLink="false">https://medium.com/p/1ca0d5ee74dd</guid>
            <dc:creator><![CDATA[Boni García]]></dc:creator>
            <pubDate>Tue, 21 Oct 2025 06:20:01 GMT</pubDate>
            <atom:updated>2025-10-23T20:25:56.684Z</atom:updated>
            <content:encoded><![CDATA[<p>This story summarizes a talk given by <a href="https://bonigarcia.dev/">Boni García</a> at the <a href="https://www.dstb.dk/konferencer/2025">Quality Beacon conference</a> in Copenhagen, Denmark, on October 21, 2025. You can find the original slides of this talk <a href="https://bonigarcia.dev/slides/2025-Quality_Beacon-WebDriver_BiDi_The_Future_of_Browser_Automation_is_Now-v1.pdf">here</a>.</p><h3>What Is Browser Automation?</h3><p>Browser automation means controlling a web browser using code — for testing, scraping, or performing repetitive tasks. Traditionally, tools like <strong>Selenium WebDriver</strong>, <strong>Puppeteer</strong>, <strong>Cypress</strong>, and <strong>Playwright</strong> have powered this automation revolution. But each came with different architectures, protocols, and quirks. WebDriver BiDi is changing that.</p><h3>Enter WebDriver BiDi</h3><p><strong>WebDriver BiDi</strong> (short for <em>bidirectional</em>) is a <strong>W3C standard-in-progress</strong> that enables <strong>two-way, real-time communication</strong> between your automation scripts and the browser. Unlike the classic WebDriver (which used HTTP in a request-response model), BiDi uses <strong>WebSockets</strong>, allowing the browser to <em>push</em> events like network requests, console logs, or JavaScript exceptions directly to your code. In short:</p><ul><li>🦾 More reliable automation</li><li>🌐 Better browser control</li><li>📃 One unified standard</li><li>⚒ Simpler tooling and fewer hacks</li></ul><p>Specification: <a href="https://www.w3.org/TR/webdriver-bidi/">https://www.w3.org/TR/webdriver-bidi/</a></p><h4>Why WebDriver BiDi Matters</h4><p>Traditional <strong>W3C WebDriver</strong> works like a walkie-talkie: your code sends a command, waits, and gets a response. WebDriver BiDi, on the other hand, is like a <strong>live conversation</strong> — both sides talk freely. That means we can now:</p><ul><li>Capture console logs in real time.</li><li>Intercept and modify network traffic.</li><li>React instantly to browser events.</li><li>Simulate complex user input with more precision.</li></ul><p>BiDi merges two worlds:</p><ul><li>The <strong>reliability</strong> of the W3C WebDriver protocol.</li><li>The <strong>real-time capabilities</strong> of the Chrome DevTools Protocol (CDP).</li></ul><h3>Selenium</h3><p>Selenium WebDriver (often known as simply <strong>Selenium</strong>) is a multilanguage <strong>browser automation library</strong>. Selenium’s architecture is based on the <a href="https://www.w3.org/TR/webdriver2/">W3C WebDriver</a> standard, which defines a protocol for browser communication using JSON over HTTP.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/684/1*nctuDTfhL1FsBnnLEQ2KCw.png" /></figure><p>Selenium has supported BiDi since version 4, with high-level APIs coming in Selenium 5. This way, currently both WebDriver and BiDi-based automation are possible with Selenium:</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*YbSUSs-KPexSj91JhVsZdA.png" /></figure><p>For enabling BiDi in Selenium you need:</p><pre>@BeforeEach<br>void setup() {<br>    ChromeOptions options = new ChromeOptions();<br>    options.enableBiDi();<br>    driver = new ChromeDriver(options);<br>}</pre><p>Once enabled, you can interact with BiDi modules like BrowsingContext, Input, Network, and Log.</p><p><em>Example: Browsing Context</em></p><pre>@Test<br>void testBrowsing() {<br>    BrowsingContext context = new BrowsingContext(driver, driver.getWindowHandle());<br>    context.navigate(&quot;https://bonigarcia.dev/selenium-webdriver-java/&quot;);<br>    String screenshot = context.captureScreenshot(); // Base64 image<br>    assertThat(screenshot).isNotBlank();<br>}</pre><p><em>Example: Listening to Logs</em></p><pre>@Test<br>void testLog() {<br>    List&lt;GenericLogEntry&gt; logs = new ArrayList&lt;&gt;();<br>    try (LogInspector inspector = new LogInspector(driver)) {<br>        inspector.onConsoleEntry(logs::add);<br>    }<br>    driver.get(&quot;https://bonigarcia.dev/selenium-webdriver-java/console-logs.html&quot;);<br>    new WebDriverWait(driver, Duration.ofSeconds(5)).until(_ -&gt; logs.size() &gt; 3);<br>    logs.forEach(log -&gt; System.out.println(log.getText()));<br>}</pre><h3>Puppeteer</h3><p><strong>Puppeteer</strong> is a Node.js <strong>browser automation library</strong> created and maintained by the Chrome DevTools team at Google since 2017. <strong>Puppeteer </strong>was initially tied to CDP, now supports BiDi for Firefox as of v23 — and uses it as the <strong>default protocol</strong> from Puppeteer 24 onward.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*o4sOWK32HTYR0zYznz-cZA.png" /></figure><p><em>Example: Hello World with Puppeteer and BiDi</em></p><pre>const puppeteer = require(&#39;puppeteer&#39;);<br><br>describe(&#39;Hello World with Puppeteer and BiDi&#39;, () =&gt; {<br>  let browser, page;<br><br>  beforeAll(async () =&gt; {<br>    browser = await puppeteer.launch({<br>      browser: &#39;firefox&#39;,<br>      protocol: &#39;webDriverBiDi&#39;,<br>    });<br>    page = await browser.newPage();<br>  });<br><br>  it(&#39;checks page title&#39;, async () =&gt; {<br>    await page.goto(&#39;https://bonigarcia.dev/selenium-webdriver-java/&#39;);<br>    const title = await page.title();<br>    expect(title).toContain(&#39;Selenium WebDriver&#39;);<br>  });<br><br>  afterAll(async () =&gt; await browser.close());<br>});java</pre><h3>Cypress</h3><p><strong>Cypress</strong> is a JavaScript <strong>end-to-end automated testing framework</strong> created as a company in 2014 to provide a seamless experience for automated web testing.</p><p>As of <strong>Cypress 14.1.0 (February 2025)</strong>, Firefox automation runs over WebDriver BiDi by default. By <strong>Cypress 15 (August 2025)</strong>, CDP support in Firefox was completely dropped.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*cghH-IsWXFfXuwgONvWEiQ.png" /></figure><p>Users don’t need to change a thing — BiDi runs transparently beneath the familiar Cypress API:</p><pre>describe(&#39;Hello World Cypress&#39;, () =&gt; {<br>  it(&#39;checks title&#39;, () =&gt; {<br>    cy.visit(&#39;https://bonigarcia.dev/selenium-webdriver-java/&#39;);<br>    cy.title().should(&#39;include&#39;, &#39;Selenium WebDriver&#39;);<br>  });<br>});</pre><h3>Playwright</h3><p><strong>Playwright</strong> is a multilanguage <strong>end-to-end automated testing framework </strong>maintained by Microsoft since 2020. Playwright maintains patched versions of Chromium, Firefox, and WebKit (to enable automation and cross-browser consistency). Playwright uses an extended version of CDP to implement to control uniformly across these browsers.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*-_SFEWfr6xE32zsaMVcVyg.png" /></figure><p><strong>Playwright</strong> is working toward BiDi integration (see <a href="https://github.com/microsoft/playwright/issues/32577">issue</a>). Although support is still <strong>experimental</strong>, the transition to WebDriver BiDi will likely happen automatically in a future version of Playwright once the protocol is mature enough to support all of Playwright’s features.</p><h3>Conclusions</h3><ul><li>WebDriver BiDi is an in-progress W3C standard for the next generation of browser automation</li><li>It combines the stability of WebDriver with the power of CDP, offering a single, standard way to automate browsers</li><li>Major tools like Selenium, Puppeteer, Cypress, and Playwright are actively integrating WebDriver BiDi</li><li>Selenium: BiDi low-level features available in Selenium 4, high-level API is in development (planned for Selenium 5)</li><li>Puppeteer: BiDi support for Firefox since v23</li><li>Cypress: BiDi support for Firefox since v14.1.0</li><li>Playwright: BiDi support is still experimental</li></ul><h4>How to track the evolution of WebDriver BiDi?</h4><ul><li><a href="https://github.com/w3c/webdriver-bidi/issues">W3C WebDriver issues</a></li><li><a href="https://github.com/w3c/webdriver-bidi/blob/main/roadmap.md">W3C WebDriver BiDi roadmap</a></li><li><a href="https://docs.google.com/spreadsheets/d/1Cg3rifrBZClIitU3aFW_WDv64gY3ge8xPtN-HE1qzrY/">W3C WebDriver BiDi planning</a></li><li><a href="https://wpt.fyi/results/webdriver/tests/bidi?label=experimental&amp;label=master&amp;aligned">WPT dashboard</a></li><li><a href="https://github.com/GoogleChromeLabs/chromium-bidi">Implementation of WebDriver BiDi for Chromium</a></li><li><a href="https://developer.chrome.com/blog/webdriver-bidi">Chrome for developers blog about BiDi</a></li><li><a href="https://wiki.mozilla.org/WebDriver/RemoteProtocol">Communication with the Firefox team about WebDriver BiDi</a></li></ul><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=1ca0d5ee74dd" width="1" height="1" alt="">]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[JUnit vs. TestNG: Which Framework Fits Your Testing Strategy?]]></title>
            <link>https://medium.com/@boni.gg/junit-vs-testng-which-framework-fits-your-testing-strategy-9883debedf7e?source=rss-7118f7714c53------2</link>
            <guid isPermaLink="false">https://medium.com/p/9883debedf7e</guid>
            <dc:creator><![CDATA[Boni García]]></dc:creator>
            <pubDate>Tue, 14 Oct 2025 11:07:11 GMT</pubDate>
            <atom:updated>2025-10-14T11:07:11.904Z</atom:updated>
            <content:encoded><![CDATA[<p>This story summarizes a talk given by <a href="https://bonigarcia.dev/">Boni García</a> at the <a href="https://2025.javacro.hr/eng/">JavaCro’25</a> conference in Rovinj, Croatia, on October 14, 2025. You can find the original slides of this talk <a href="https://bonigarcia.dev/slides/2025-JavaCro25-JUnit_vs_TestNG_Which_Framework_Fits_Your_Testing_Strategy-v1.pdf">here</a>.</p><h3>Introduction</h3><p>A <strong>unit testing framework</strong> is a tool that provides structure and reusable components to write, organize, and run automated tests for given pieces of code, ensuring they behave as expected. This story reviews two of Java’s most popular unit testing frameworks: <a href="https://docs.junit.org/current/user-guide/">JUnit</a> and <a href="https://testng.org/">TestNG</a>.</p><h4>JUnit</h4><p>Created by Kent Beck and Erich Gamma in 1999, <strong>JUnit </strong>quickly became the de facto testing library for Java. Its evolution is as follows:</p><ul><li>JUnit 4: widespread adoption; annotation-based model (@Test, @Before, etc.)</li><li>JUnit 5 (2017): major redesign including the JUnit Platform, the foundation of the JUnit 5 testing framework, providing the launching infrastructure for running tests and defining the API for test engines (like JUnit Jupiter or Vintage) to discover and execute tests.</li><li>JUnit 6 (2025): the latest release (September 30, 2025), supporting Java 17 and cleaning up deprecated APIs</li></ul><p><strong>TestNG</strong></p><p>Created by Cédric Beust in 2004, <strong>TestNG </strong>was designed to fix some of JUnit’s early limitations. Its key features include:</p><ul><li>Grouping and filtering via annotations</li><li>Native parallel execution</li><li>Built-in data providers for parameterized tests</li></ul><p><strong>Comparison</strong></p><p>In this story, we’ll look at seven areas side-by-side:</p><ol><li>Test lifecycle (basics)</li><li>Parameterized tests</li><li>Categorizing &amp; filtering tests</li><li>Conditional test execution</li><li>Ordering tests</li><li>Parallel execution</li><li>Advanced test lifecycle</li></ol><p>As a real use case, this comparison will be done using <a href="https://selenium.dev/">Selenium</a> to illustrate the key differences between JUnit and TestNG. <strong>Selenium is a browser automation library</strong>, not a testing framework. However, the main use case of Selenium is end-to-end automated testing. For this reason, it is typically used in conjunction with a unit testing framework like JUnit or TestNG.</p><p>This story results from the work on developing examples of two books: <a href="https://www.amazon.com/Mastering-Software-Testing-JUnit-Comprehensive-ebook/dp/B076ZQCK5Q/">Mastering Software Testing with JUnit 5</a> (Packt Publishing, 2017) and <a href="https://oreil.ly/1E7CX">Hands-On Selenium WebDriver with Java</a> (O’Reilly Media, 2022). The test examples are presented here in the following open-source repositories: <a href="https://github.com/bonigarcia/mastering-junit5">mastering-junit5</a> and <a href="https://github.com/bonigarcia/selenium-webdriver-java">selenium-webdriver-java</a>.</p><h3>1. Test lifecycle (basics)</h3><p>The <strong>test lifecycle</strong> is the sequence of steps a testing framework follows to set up the test fixture (initial state), execute the test(s), and clean up afterward. The following picture shows that the basic test lifecycle is similar in JUnit and TestNG, but with changes in the annotations’ names.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/954/1*QcSfqG9oGFNMT9_yDWH_fQ.png" /></figure><p><em>Example #1.1: basic test with Selenium — JUnit</em></p><pre>import org.openqa.selenium.WebDriver;<br>import org.openqa.selenium.chrome.ChromeDriver;<br>import org.testng.annotations.AfterMethod;<br>import org.testng.annotations.BeforeMethod;<br>import org.testng.annotations.Test;<br><br>class HelloWorldSeleniumJupiterTest {<br>     WebDriver driver;<br>     @BeforeEach<br>    void setup() {<br>        driver = new ChromeDriver();<br>    }<br>     @Test<br>    void test() {<br>        // Test logic<br>    }<br><br>    @AfterEach<br>    void teardown() {<br>        driver.quit();<br>    }<br>}</pre><p><em>Example #1.2: basic test with Selenium — TestNG</em></p><pre>import org.openqa.selenium.WebDriver;<br>import org.openqa.selenium.chrome.ChromeDriver;<br>import org.testng.annotations.AfterMethod;<br>import org.testng.annotations.BeforeMethod;<br>import org.testng.annotations.Test;<br><br>public class HelloWorldSeleniumNGTest {<br>     WebDriver driver;<br>     @BeforeMethod<br>    public void setup() {<br>        driver = new ChromeDriver();<br>    }<br>     @Test<br>    public void test() {<br>        // Test logic<br>    }<br><br>    @AfterMethod<br>    public void teardown() {<br>        driver.quit();<br>    }<br>}</pre><h3>2. Parameterized tests</h3><p>A <strong>parameterized test</strong> is a test that runs multiple times with different input values, allowing us to reuse the same logic across varied datasets.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/952/1*uP0bkwCPCIH_7kXpEXXBgA.png" /></figure><p>To implement a parameterized test in JUnit, we need to:</p><ul><li>Use @ParameterizedTest (instead of @Test) or @ParameterizedClass (in addition to @Test)</li><li>Use an argument provider (to define the dataset)</li></ul><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*rU_NC1OF1Fnr9cUrMqevwg.png" /></figure><p>There are two ways of implementing parameterized tests in TestNG:</p><ul><li>Using @DataProvider (most common and scalable)</li><li>Using @Parameters + testng.xml (dataset in &lt;test&gt;&lt;/test&gt;)</li></ul><p><em>Example #2.1: data-driven test case with Selenium — JUnit</em></p><pre>class LoginJupiterTest extends BrowserParent {<br>     static Stream&lt;Arguments&gt; loginData() {<br>        return Stream.of(Arguments.of(&quot;user&quot;, &quot;user&quot;, &quot;Login successful&quot;),<br>                Arguments.of(&quot;bad-user&quot;, &quot;bad-passwd&quot;, &quot;Invalid credentials&quot;));<br>    }<br><br>    @ParameterizedTest<br>    @MethodSource(&quot;loginData&quot;)<br>    void testLogin(String username, String password, String expectedText) {<br>        // Test logic<br>    }<br>}</pre><p><em>Example #2.2: data-driven test case with Selenium — TestNG</em></p><pre>public class LoginNGTest extends BrowserParent {<br>     @DataProvider(name = &quot;loginData&quot;)<br>    public static Object[][] data() {<br>        return new Object[][] { { &quot;user&quot;, &quot;user&quot;, &quot;Login successful&quot; },<br>                { &quot;bad-user&quot;, &quot;bad-passwd&quot;, &quot;Invalid credentials&quot; } };<br>    }<br>     @Test(dataProvider = &quot;loginData&quot;)<br>    public void testLogin(String username, String password, String expectedText) {<br>        // Test logic<br>    }<br> }</pre><p><em>Example #3.1: cross-browser testing with Selenium — JUnit</em></p><pre>@ParameterizedClass<br>@ArgumentsSource(CrossBrowserProvider.class)<br>class CrossBrowserParent {<br>     @Parameter<br>    WebDriver driver;<br>     @AfterEach<br>    void teardown() {<br>        driver.quit();<br>    }<br> }</pre><pre>class CrossBrowserProvider implements ArgumentsProvider {<br>     @Override<br>    public Stream&lt;? extends Arguments&gt; provideArguments(<br>            ExtensionContext context) {<br>        ChromeDriver chrome = new ChromeDriver();<br>        FirefoxDriver firefox = new FirefoxDriver();<br>         return Stream.of(Arguments.of(chrome), Arguments.of(firefox));<br>    }<br> }</pre><pre>class CrossBrowserJUnitTest extends CrossBrowserParent {<br>     @Test<br>    void test() {<br>        // Test logic<br>    }<br>}</pre><p><em>Example #3.2: cross-browser testing with Selenium — TestNG</em></p><pre>public class CrossBrowserNGTest extends CrossBrowserParent {<br>     @Test(dataProvider = &quot;browserProvider&quot;)<br>    public void test(WebDriver driver) {<br>        this.driver = driver;<br>         // Test logic<br>    }<br> }</pre><pre>public class CrossBrowserParent {<br>     WebDriver driver;<br>     @DataProvider(name = &quot;browserProvider&quot;)<br>    public static Object[][] data() {<br>        ChromeDriver chrome = new ChromeDriver();<br>        FirefoxDriver firefox = new FirefoxDriver();<br>         return new Object[][] { { chrome }, { firefox } };<br>    }<br>     @AfterMethod<br>    void teardown() {<br>        driver.quit();<br>    }<br> }</pre><h3>3. Categorizing &amp; filtering tests</h3><p><strong>Categorizing and filtering</strong> allows us to group tests into categories and run only the ones that match specific criteria.</p><p>In JUnit, test classes and methods can be tagged in JUnit using @Tag. In TestNG, test methods can be grouped using the attribute groups in @Test. Those categories (tags or groups) can later be used to filter test discovery and execution.</p><p><em>Example #5.1: </em>grouping Selenium tests<em> — JUnit</em></p><pre>class CategoriesJUnitTest extends BrowserParent {<br>     @Test<br>    @Tag(&quot;WebForm&quot;)<br>    void testCategoriesWebForm() {<br>        // Test logic<br>    }<br>     @Test<br>    @Tag(&quot;HomePage&quot;)<br>    void testCategoriesHomePage() {<br>        // Test logic<br>    }<br> }</pre><pre>mvn test -Dgroups=HomePage<br><br>gradle test -Pgroups=HomePage</pre><p><em>Example #5.2: </em>grouping Selenium tests<em> — TestNG</em></p><pre>public class CategoriesNGTest extends BrowserGroupsParent {<br>     @Test(groups = { &quot;WebForm&quot; })<br>    public void testCategoriesWebForm() {<br>        // Test logic<br>    }<br>     @Test(groups = { &quot;HomePage&quot; })<br>    public void tesCategoriestHomePage() {<br>        // Test logic<br>    }<br>}</pre><pre>mvn test -Dtest=CategoriesNGTest -DexcludedGroups=HomePage<br><br>gradle test --tests CategoriesNGTest -PexcludedGroups=HomePage</pre><h3>4. Conditional test execution</h3><p><strong>Conditional test execution </strong>allows us to enable or skip tests based on predefined conditions. JUnit provides a rich set of built-in annotations for skipping tests (<a href="http://twitter.com/Disabled">@Disabled</a> and others). Also, we can use Assumptions to disable tests in runtime. TestNG provides the annotation <a href="http://twitter.com/Ignore">@Ignore</a> and attributes in <a href="http://twitter.com/Test">@Test</a> (e.g., enabled=false) to run conditionally. Also, we can use SkipException to disable tests in runtime.</p><p>The JUnit annotations for disabling tests are the following:</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*WGpH1isAeg5-P1txLTgiQg.png" /></figure><p><em>Example #6.1: skipping tests (I) — JUnit</em></p><pre>class DisabledJupiterTest {<br>    @Disabled(&quot;Optional reason for disabling&quot;)<br>    @Test<br>    public void testDisabled1() {<br>        // Test logic<br>    }<br>    @DisabledOnJre(JAVA_17)<br>    @Test<br>    public void testDisabled2() {<br>        // Test logic<br>    }<br>    @EnabledOnOs(MAC)<br>    @Test<br>    public void testDisabled3() {<br>        // Test logic<br>    }<br><br>}</pre><p><em>Example #6.2: skipping tests (I) — TestNG</em></p><pre>public class DisabledNGTest {<br>     @Ignore(&quot;Optional reason for disabling&quot;)<br>    @Test<br>    public void testDisabled1() {<br>        // Test logic<br>    }<br>     @Test(enabled = false)<br>    public void testDisabled2() {<br>        // Test logic<br>    }<br> }</pre><p><em>Example #7.1: skipping tests (II) — JUnit</em></p><pre>class ConditionalJupiterTest {<br>     @Test<br>    public void testConditional() {<br>        boolean condition = false; // runtime condition<br>        Assumptions.assumeTrue(condition);<br>         // Test logic<br>    }<br>}</pre><p><em>Example #7.2: skipping tests (II) — TestNG</em></p><pre>public class ConditionalNGTest {<br>     @Test<br>    public void testConditional() {<br>        boolean condition = false; // runtime condition<br>        if (!condition) {<br>            throw new SkipException(&quot;Skipping test&quot;);<br>        }<br>         // Test logic<br>    }<br> }</pre><h3>5. Ordering tests</h3><p><strong>Ordering tests </strong>is used to control the sequence in which tests are executed. The default order for test execution are:</p><ul><li>In JUnit, tests are run in an unspecified order (not guaranteed, deterministic algorithm that is but intentionally nonobvious meant to be independent)</li><li>In TestNG, tests are run in alphabetical order by method name</li></ul><p>To change this behavior:</p><ul><li>In JUnit, we use @TestMethodOrder with @Order.</li><li>TestNG, we use priority and dependsOnMethods in @Test, or class order in testng.xml</li></ul><p><em>Example #8.1: reuse the same browser to run tests in a given order — JUnit</em></p><pre>@TestInstance(Lifecycle.PER_CLASS)<br>@TestMethodOrder(OrderAnnotation.class)<br>class OrderJunitTest {<br>    WebDriver driver;<br><br>    @BeforeAll<br>    void setup() {<br>        driver = new ChromeDriver();<br>    }<br><br>    @Test<br>    @Order(1)<br>    void testA() {<br>        // Test logic<br>    }<br><br>    @Test<br>    @Order(2)<br>    void testB() {<br>        // Test logic<br>    }<br><br>    @AfterAll<br>    void teardown() {<br>        driver.quit();<br>    }<br>}</pre><p><em>Example #8.2: reuse the same browser to run tests in a given order — TestNG</em></p><pre>public class OrderNGTest {<br>    WebDriver driver;<br><br>    @BeforeClass<br>    public void setup() {<br>        driver = new ChromeDriver();<br>    }<br><br>    @Test(priority = 1)<br>    public void testA() {<br>        // Test logic<br>    }<br><br>    @Test(priority = 2)<br>    public void testB() {<br>        // Test logic<br>    }<br><br>    @AfterClass<br>    public void teardown() {<br>        driver.quit();<br>    }<br> }</pre><h3>6. Parallel execution</h3><p><strong>Parallel test execution</strong> allows us to run multiple tests simultaneously to speed up execution. JUnit provides different configuration parameters to tests in parallel:</p><ul><li>junit.jupiter.execution.parallel.enabled (to enable test parallelism)</li><li>junit.jupiter.execution.parallel.mode.classes.default (to run test classes in parallel)</li><li>junit.jupiter.execution.parallel.mode.default (to run test methods in parallel)</li></ul><p>These parameters can be specified using a configuration file or in runtime trough annotations (<a href="http://twitter.com/Execution">@Execution</a>).</p><p>On the ther hand, TestNG enables parallelism using the testng.xml config file.</p><p><em>Example #9.1: run Selenium tests in parallel — JUnit</em></p><pre>junit.jupiter.execution.parallel.enabled = true<br>junit.jupiter.execution.parallel.mode.default = concurrent<br>junit.jupiter.execution.parallel.mode.classes.default = same_thread</pre><pre>@Execution(ExecutionMode.CONCURRENT)<br>class Parallel1JupiterTest extends BrowserParent {<br>    @Test<br>    void testParallel1() {<br>        // Test logic<br>    }<br> }</pre><pre>@Execution(ExecutionMode.CONCURRENT)<br>class Parallel2JupiterTest extends BrowserParent {<br>    @Test<br>    void testParallel2() {<br>        // Test logic<br>    }<br> }</pre><p><em>Example #9.2: run Selenium tests in parallel — TestNG</em></p><pre>&lt;!DOCTYPE suite SYSTEM &quot;http://testng.org/testng-1.0.dtd&quot; &gt;<br>&lt;suite name=&quot;parallel-suite&quot; parallel=&quot;classes&quot; thread-count=&quot;2&quot;&gt;<br>    &lt;test name=&quot;parallel-tests&quot;&gt;<br>        &lt;classes&gt;<br>            &lt;class name=&quot;io.github.bonigarcia.testng.selenium.HelloWorldSeleniumNGTest&quot; /&gt;<br>            &lt;class name=&quot;io.github.bonigarcia.testng.selenium.BasicSeleniumNGTest&quot; /&gt;<br>        &lt;/classes&gt;<br>    &lt;/test&gt;<br>&lt;/suite&gt;</pre><h3>7. Advanced test lifecycle</h3><p>In JUnit 5+, the <strong>extension model </strong>provides comprehensive capabilities to customize and hook into the test lifecycle at various points.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*wbL_q8kzVbpX6YV093afCQ.png" /></figure><p>The following diagram shows the JUnit 5+ test execution lifecycle and the order in which user-defined annotations and extension callbacks run. Callbacks are extension hooks, and annotations are user code methods executed around each test lifecycle.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*VZ1Vy5AS8djwOsg8n6pYSw.png" /></figure><p>On the other hand, TestNG provides a rich set of listeners to intercept lifecycle events for tests and suites:</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*a_qjuubCnV9lJptfVwWKcw.png" /></figure><p><em>Example #10.1: retrying Selenium tests (to detect flakiness) — JUnit</em></p><pre>@ExtendWith(RetryExtension.class)<br>class RandomCalculatorJupiterTest extends BrowserParent {<br>    @Test<br>    void testRandomCalculator() {<br>        // Test logic<br>    }<br> }</pre><pre>class RandomCalculatorJupiterTest extends BrowserParent {<br><br>    @RegisterExtension<br>    Extension failureWatcher = new RetryExtension(5);<br><br>    @Test<br>    void testRandomCalculator() {<br>        // Test logic<br>    }<br> }</pre><pre>public class RetryExtension implements TestExecutionExceptionHandler {<br>    static final int DEFAULT_MAX_RETRIES = 3;<br>    final AtomicInteger retryCount = new AtomicInteger(1);<br>    final AtomicInteger maxRetries = new AtomicInteger(DEFAULT_MAX_RETRIES);<br><br>    public RetryExtension() {<br>        // Default constructor<br>    }<br><br>    public RetryExtension(int maxRetries) {<br>        this.maxRetries.set(maxRetries);<br>    }<br><br>    @Override<br>    public void handleTestExecutionException(ExtensionContext extensionContext,<br>            Throwable throwable) throws Throwable {<br>        // Manage throwable depending on the retry count<br>    }<br> }</pre><p><em>Example #10.2: retrying Selenium tests (to detect flakiness) — TestNG</em></p><pre>public class RandomCalculatorNGTest extends BrowserParent {<br>    @Test(retryAnalyzer = RetryAnalyzer.class)<br>    @Retry(5)<br>    public void testRandomCalculator() {<br>        // Test logic<br>    }<br> }</pre><pre>@Retention(RetentionPolicy.RUNTIME)<br>public @interface Retry {<br>    int value();<br>}</pre><pre>public class RetryAnalyzer implements IRetryAnalyzer {<br>    static final int DEFAULT_MAX_RETRIES = 3;<br>    final AtomicInteger retryCount = new AtomicInteger(1);<br><br>    @Override<br>    public boolean retry(ITestResult result) {<br>        Method method = result.getMethod().getConstructorOrMethod().getMethod();<br>        int maxRetries = DEFAULT_MAX_RETRIES;<br>        if (method.isAnnotationPresent(Retry.class)) {<br>            Retry retry = method.getAnnotation(Retry.class);<br>            maxRetries = retry.value();<br>        }<br>        if (retryCount.get() &lt;= maxRetries) {<br>            logError(result.getThrowable());<br>            retryCount.incrementAndGet();<br>            return true;<br>        }<br>        return false;<br>    }<br><br>    private void logError(Throwable e) {<br>        System.err.println(&quot;Attempt test execution #&quot; + retryCount.get()<br>                + &quot; failed (&quot; + e.getClass().getName() + &quot;thrown):  &quot;<br>                + e.getMessage());<br>    }<br> }</pre><p><em>Example #11.1: gather data (e.g., browser screenshot) if test fails — Java</em></p><pre>@ExtendWith(FailureWatcher.class)<br>class FailureJupiterTest extends BrowserParent {<br>    @Test<br>    void testFailure() {<br>        // Test logic<br>        fail(&quot;Forced error&quot;);<br>    }<br> }</pre><pre>public class FailureWatcher implements TestExecutionExceptionHandler {<br>    @Override<br>    public void handleTestExecutionException(ExtensionContext context,<br>            Throwable throwable) throws Throwable {<br>         context.getTestInstance().ifPresent(testInstance -&gt; {<br>            WebDriver driver = (WebDriver) SeleniumUtils<br>                    .getFieldFromTestInstance(testInstance, &quot;driver&quot;);<br>            SeleniumUtils.getScreenshotAsFile(driver, context.getDisplayName());<br>        });<br>         throw throwable;<br>    }<br>}</pre><p><em>Example #11.2: gather data (e.g., browser screenshot) if test fails — TestNG</em></p><pre>public class FailureNGTest {<br>    WebDriver driver;<br><br>    @BeforeMethod<br>    public void setup() {<br>        driver = new ChromeDriver();<br>    }<br><br>    @AfterMethod<br>    public void teardown(ITestResult result) {<br>        if (result.getStatus() == ITestResult.FAILURE) {<br>            SeleniumUtils.getScreenshotAsFile(driver, result.getName());<br>        }<br>         driver.quit();<br>    }<br><br>    @Test<br>    public void testFailure() {<br>        // Test logic<br>        fail(&quot;Forced error&quot;);<br>    }<br> }</pre><p><em>Example #12.1: reporting test suite — Java</em></p><pre>@ExtendWith(Reporter.class)<br>class Report1JupiterTest extends BrowserParent {<br>    @Test<br>    void testReport1() {<br>        // Test logic<br>    }<br> }</pre><pre>@ExtendWith(Reporter.class)<br>class Report2JupiterTest extends BrowserParent {<br>    @Test<br>    void testReport2() {<br>        // Test logic<br>    }<br> }</pre><pre>public class Reporter implements BeforeAllCallback, BeforeEachCallback,<br>        AfterTestExecutionCallback {<br>    static final String REPORT_NAME = &quot;report-junit.html&quot;;<br>    ExtentReports report;<br>    ExtentTest test;<br><br>    @Override<br>    public void beforeAll(ExtensionContext context) throws Exception {<br>        Store store = context.getRoot()<br>                .getStore(ExtensionContext.Namespace.create(STORE_NAMESPACE));<br>        report = store.get(STORE_NAME, ExtentReports.class);<br>        if (report == null) {<br>            report = new ExtentReports();<br>            store.put(STORE_NAME, report);<br>             Runtime.getRuntime().addShutdownHook(new Thread(report::flush));<br>        }<br>        ExtentSparkReporter htmlReporter = new ExtentSparkReporter(REPORT_NAME);<br>        report.attachReporter(htmlReporter);<br>    }<br><br>    @Override<br>    public void beforeEach(ExtensionContext context) throws Exception {<br>        test = report.createTest(context.getDisplayName());<br>    }<br><br>    @Override<br>    public void afterTestExecution(ExtensionContext context) throws Exception {<br>        context.getTestInstance().ifPresent(testInstance -&gt; {<br>            // Take screenshot <br>            test.addScreenCaptureFromBase64String(screenshot);<br>        });<br>    }<br> }</pre><p><em>Example #12.2: reporting test suite — TestNG</em></p><pre>@Listeners(Reporter.class)<br>public class Report1NGTest extends BrowserParent {<br>    @Test<br>    public void testReport1() {<br>        // Test logic<br>    }<br> }</pre><pre>@Listeners(Reporter.class)<br>public class Report2NGTest extends BrowserParent {<br>    @Test<br>    public void testReport2() {<br>        // Test logic<br>    }<br> }</pre><pre>public class Reporter implements ITestListener {<br>     static final String REPORT_NAME = &quot;report-testng.html&quot;;<br>    ExtentReports report;<br>    ExtentTest test;<br><br>    @Override<br>    public void onStart(ITestContext context) {<br>        ITestListener.super.onStart(context);<br>        report = new ExtentReports();<br>        ExtentSparkReporter htmlReporter = new ExtentSparkReporter(REPORT_NAME);<br>        report.attachReporter(htmlReporter);<br>    }<br><br>    @Override<br>    public void onTestStart(ITestResult result) {<br>        ITestListener.super.onTestStart(result);<br>        test = report.createTest(result.getName());<br>    }<br><br>    @Override<br>    public void onTestSuccess(ITestResult result) {<br>        ITestListener.super.onTestSuccess(result);<br>        // Take screenshot <br>        test.addScreenCaptureFromBase64String(screenshot);<br>    }<br><br>    @Override<br>    public void onFinish(ITestContext context) {<br>        ITestListener.super.onFinish(context);<br>        report.flush();<br>    }<br> }</pre><h3>Conclusions</h3><p>Both JUnit and TestNG provide a comprehensive programming model for developing advanced tests in Java. The similar aspects in JUnit and TestNG are the following:</p><ul><li>Basic test lifecycle</li><li>Categorizing and filtering tests</li><li>Ordering tests</li><li>Parallel test execution</li></ul><p>The strong points of JUnit are:</p><ul><li>Parameterized tests</li><li>Conditional test execution</li><li>Extension model</li></ul><p>The strong points in TestNG is:</p><ul><li>Test listeners</li></ul><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=9883debedf7e" width="1" height="1" alt="">]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[Browser Automation with Java]]></title>
            <link>https://medium.com/@boni.gg/browser-automation-with-java-794873cdc449?source=rss-7118f7714c53------2</link>
            <guid isPermaLink="false">https://medium.com/p/794873cdc449</guid>
            <category><![CDATA[browsers]]></category>
            <category><![CDATA[testing]]></category>
            <category><![CDATA[selenium]]></category>
            <category><![CDATA[playwrights]]></category>
            <category><![CDATA[automation]]></category>
            <dc:creator><![CDATA[Boni García]]></dc:creator>
            <pubDate>Mon, 13 Oct 2025 18:48:41 GMT</pubDate>
            <atom:updated>2025-10-13T18:48:41.466Z</atom:updated>
            <content:encoded><![CDATA[<p>Browser automation is a key ingredient for end-to-end testing. At the <a href="https://2025.javacro.hr/eng">JavaCro’25 conference</a>, Boni García delivered a presentation offering a deep dive into two of the most popular browser automation tools available today: Selenium and Playwright. This article captures the essence of that talk, providing an overview for anyone looking to get started on browser automation in the Java ecosystem. You can get the original slides of this talk <a href="https://bonigarcia.dev/slides/2025-JavaCro25-Browser_Automation_with_Java-v1.pdf">here</a>.</p><h3>What is Browser Automation?</h3><p>Browser automation is the process of using software or scripts to control a web browser and perform tasks automatically, without manual human intervention. The primary use cases for browser automation include:</p><ul><li>Test automation: This is the most common application, encompassing end-to-end testing to verify web applications.</li><li>Web scraping: Automating the extraction of large amounts of data from websites.</li><li>Automating repetitive tasks for web pages: Automating mundane tasks like filling out forms or generating reports from web interfaces.</li></ul><p>The world of browser automation is rich with tools, each with its own philosophy and strengths. Some of the key players include Selenium, Playwright, Cypress, Puppeteer, TestCafe, and WebdriverIO. This story focuses on the most prominent choices for Java developers: Selenium and Playwright.</p><h3>Selenium</h3><p>Selenium is a <strong>browser automation library</strong>, and it has been considered the de facto standard for browser automation for many years. Selenium is:</p><ul><li>Multi-language: Officially supported in Java, JavaScript, Python, .NET, and Ruby.</li><li>Cross-browser: Compatible with all major browsers like Chrome, Firefox, Safari, and Edge.</li><li>Open-source and community-driven since 2004.</li></ul><p>Selenium’s architecture is based on the <a href="https://www.w3.org/TR/webdriver2/">W3C WebDriver</a> standard, which defines a protocol for browser communication using JSON over HTTP. This standards-based approach is a key strength, ensuring broad compatibility.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*YZbxs_qfFUPa_HVCO0DGxw.png" /></figure><p>It’s important to understand that <strong>Selenium is not a testing framework </strong>because it doesn’t include a test runner, assertion library, or reporting features. For this, Selenium users relies on a rich ecosystem of tools like JUnit or TestNG (unit testing frameworks), AssertJ (fluent assertions), or Allure and ExtentReports (reporting), among others.</p><h4>Selenium Manager</h4><p>A significant recent improvement is <a href="https://www.selenium.dev/documentation/selenium_manager/">Selenium Manager</a>. Shipped with Selenium out of the box, it automatically discovers, downloads, and caches the browser drivers required for automation (e.g., chromedriver for Chrome, geckodriver for Firefox, etc.), greatly simplifying project setup. Consider the following <a href="https://github.com/bonigarcia/selenium-webdriver-java/blob/master/selenium-webdriver-junit5/src/test/java/io/github/bonigarcia/webdriver/jupiter/ch02/helloworld_selenium_manager/HelloWorldFirefoxJupiterTest.java">test example</a>. Internally, Selenium is using Selenium Manager to discover, download, and cache the needed driver, geckodriver, in this example:</p><pre>import static org.assertj.core.api.Assertions.assertThat;<br>import org.junit.jupiter.api.AfterEach;<br>import org.junit.jupiter.api.BeforeEach;<br>import org.junit.jupiter.api.Test;<br>import org.openqa.selenium.WebDriver;<br>import org.openqa.selenium.firefox.FirefoxDriver;<br><br>class HelloWorldFirefoxJupiterTest {<br><br>    WebDriver driver;<br><br>    @BeforeEach<br>    void setup() {<br>        driver = new FirefoxDriver();<br>    }<br><br>    @AfterEach<br>    void teardown() {<br>        driver.quit();<br>    }<br><br>    @Test<br>    void test() {<br>        driver.get(&quot;https://bonigarcia.dev/selenium-webdriver-java/&quot;);<br>        assertThat(driver.getTitle()).contains(&quot;Selenium WebDriver&quot;);<br>    }<br><br>}</pre><p>Moreover, Selenium Manager also automatically discovers, downloads, and caches the browsers driven with Selenium. For example, if Firefox is unavailable in the previous test, Selenium Manager will manage (discover, download, and cache) the latest version of Firefox. And in addition, specific browser versions can be specified as follows (including “beta”, “dev”, or “nightly”), for <a href="https://github.com/bonigarcia/selenium-examples/blob/main/src/test/java/io/github/bonigarcia/selenium/version/ChromeVersionTest.java">example</a>, as follows:</p><pre>import static org.assertj.core.api.Assertions.assertThat;<br>import org.junit.jupiter.api.AfterEach;<br>import org.junit.jupiter.api.BeforeEach;<br>import org.junit.jupiter.api.Test;<br>import org.openqa.selenium.WebDriver;<br>import org.openqa.selenium.chrome.ChromeDriver;<br>import org.openqa.selenium.chrome.ChromeOptions;<br><br>class ChromeVersionTest {<br><br>    WebDriver driver;<br><br>    @BeforeEach<br>    void setup() {<br>        ChromeOptions options = new ChromeOptions();<br>        options.setBrowserVersion(&quot;beta&quot;);<br>        driver = new ChromeDriver(options);<br>    }<br><br>    @Test<br>    void test() {<br>        driver.get(&quot;https://bonigarcia.dev/selenium-webdriver-java/&quot;);<br>        String title = driver.getTitle();<br>        assertThat(title).contains(&quot;Selenium WebDriver&quot;);<br>    }<br><br>    @AfterEach<br>    void teardown() {<br>        driver.quit();<br>    }<br><br>}</pre><h4>Selenium-Java Ecosystem</h4><p>A key strength for developing end-to-end tests with Selenium and Java is the richness of their respective ecosystem. The following examples demonstrate how. For instance, to implement <strong>cross-browser testing</strong> (i.e., reuse the same test logic for different browsers), we can use the parameterized test support by JUnit, as follows (see complete example <a href="https://github.com/bonigarcia/selenium-webdriver-java/tree/master/selenium-webdriver-junit5/src/test/java/io/github/bonigarcia/webdriver/jupiter/ch08/cross_browser">here</a>):</p><pre>import static org.assertj.core.api.Assertions.assertThat;<br>import org.junit.jupiter.api.Test;<br><br>class CrossBrowserTest extends CrossBrowserParent {<br><br>    @Test<br>    void test() {<br>        driver.get(&quot;https://bonigarcia.dev/selenium-webdriver-java/&quot;);<br>        assertThat(driver.getTitle()).contains(&quot;Selenium WebDriver&quot;);<br>    }<br><br>}</pre><pre>import org.junit.jupiter.api.AfterEach;<br>import org.junit.jupiter.params.Parameter;<br>import org.junit.jupiter.params.ParameterizedClass;<br>import org.junit.jupiter.params.provider.ArgumentsSource;<br>import org.openqa.selenium.WebDriver;<br><br>@ParameterizedClass<br>@ArgumentsSource(CrossBrowserProvider.class)<br>class CrossBrowserParent {<br><br>    @Parameter<br>    WebDriver driver;<br><br>    @AfterEach<br>    void teardown() {<br>        driver.quit();<br>    }<br><br>}</pre><pre>import java.util.stream.Stream;<br>import org.junit.jupiter.api.extension.ExtensionContext;<br>import org.junit.jupiter.params.provider.Arguments;<br>import org.junit.jupiter.params.provider.ArgumentsProvider;<br>import org.openqa.selenium.chrome.ChromeDriver;<br>import org.openqa.selenium.firefox.FirefoxDriver;<br><br>public class CrossBrowserProvider implements ArgumentsProvider {<br><br>    @Override<br>    public Stream&lt;? extends Arguments&gt; provideArguments(<br>            ExtensionContext context) {<br>        ChromeDriver chrome = new ChromeDriver();<br>        FirefoxDriver firefox = new FirefoxDriver();<br><br>        return Stream.of(Arguments.of(chrome), Arguments.of(firefox));<br>    }<br><br>}</pre><p>For video recording, you can use <a href="https://bonigarcia.dev/webdrivermanager/">WebDriverManager</a>, both using browsers in Docker containers (<a href="https://github.com/bonigarcia/selenium-examples/blob/main/src/test/java/io/github/bonigarcia/selenium/wdm/docker/DockerChromeRecordingTest.java">example</a>) or by using a web extension called <a href="https://bonigarcia.dev/browserwatcher/">BrowerWatcher</a> to record only the viewport (<a href="https://github.com/bonigarcia/selenium-examples/blob/main/src/test/java/io/github/bonigarcia/selenium/wdm/watch/RecordEdgeTest.java">example</a>).</p><pre>import static org.assertj.core.api.Assertions.assertThat;<br>import java.time.Duration;<br>import org.junit.jupiter.api.AfterEach;<br>import org.junit.jupiter.api.BeforeEach;<br>import org.junit.jupiter.api.Test;<br>import org.openqa.selenium.WebDriver;<br>import io.github.bonigarcia.wdm.WebDriverManager;<br><br>class DockerChromeRecordingTest {<br><br>    WebDriver driver;<br>    WebDriverManager wdm;<br><br>    @BeforeEach<br>    void setupTest() {<br>        wdm = WebDriverManager.chromedriver().browserInDocker()<br>                .enableRecording();<br>        driver = wdm.create();<br>    }<br><br>    @Test<br>    void test() {<br>        driver.get(&quot;https://bonigarcia.dev/selenium-webdriver-java/&quot;);<br>        assertThat(driver.getTitle()).contains(&quot;Selenium WebDriver&quot;);<br>    }<br><br>    @AfterEach<br>    void teardown() throws InterruptedException {<br>        // FIXME: pause for manual browser inspection<br>        Thread.sleep(Duration.ofSeconds(3).toMillis());<br>        wdm.quit();<br>    }<br><br>}</pre><pre>import java.nio.file.Path;<br>import java.time.Duration;<br>import org.junit.jupiter.api.AfterEach;<br>import org.junit.jupiter.api.BeforeEach;<br>import org.junit.jupiter.api.Test;<br>import org.openqa.selenium.By;<br>import org.openqa.selenium.WebDriver;<br>import org.openqa.selenium.support.ui.ExpectedConditions;<br>import org.openqa.selenium.support.ui.WebDriverWait;<br>import org.slf4j.Logger;<br>import io.github.bonigarcia.wdm.WebDriverManager;<br><br>class RecordEdgeTest {<br><br>    WebDriver driver;<br>    WebDriverManager wdm;<br><br>    @BeforeEach<br>    void setup() {<br>        wdm = WebDriverManager.edgedriver().watch();<br>        driver = wdm.create();<br>    }<br><br>    @AfterEach<br>    void teardown() {<br>        driver.quit();<br>    }<br><br>    @Test<br>    void test() {<br>        driver.get(<br>                &quot;https://bonigarcia.dev/selenium-webdriver-java/slow-calculator.html&quot;);<br><br>        wdm.startRecording();<br><br>        // 1 + 3<br>        driver.findElement(By.xpath(&quot;//span[text()=&#39;1&#39;]&quot;)).click();<br>        driver.findElement(By.xpath(&quot;//span[text()=&#39;+&#39;]&quot;)).click();<br>        driver.findElement(By.xpath(&quot;//span[text()=&#39;3&#39;]&quot;)).click();<br>        driver.findElement(By.xpath(&quot;//span[text()=&#39;=&#39;]&quot;)).click();<br><br>        // ... should be 4, wait for it<br>        WebDriverWait wait = new WebDriverWait(driver, Duration.ofSeconds(10));<br>        wait.until(ExpectedConditions.textToBe(By.className(&quot;screen&quot;), &quot;4&quot;));<br><br>        wdm.stopRecording();<br><br>        Path recordingPath = wdm.getRecordingPath();<br>        assertThat(recordingPath).exists();<br>    }<br><br>}</pre><h3>Playwright</h3><p>Playwright is a newer, modern alternative from Microsoft that has quickly gained popularity. Like Selenium, it is open-source, multi-language and cross-browser. We can define Playwright as follows:</p><ul><li>For Node.js, Playwright is a <strong>end-to-end testing framework</strong>.</li><li>For Java, Python, and .NET, Playwright is a <strong>browser automation library</strong>.</li></ul><p>The difference is caused by the Playwright Test Runner (@playwright/test), a full-featured testing framework bundled with Playwright in Node.js, so it is only available for JavaScript/TypeScript developers. The Playwright Test runner provides the following features:</p><ul><li>Test runner and assertions (similar to JUnit/TestNG)</li><li>Built-in fixtures (browser/page/context lifecycle)</li><li>Parallel test execution across multiple browsers/devices</li><li>Retries mechanism</li><li>HTML reporting</li><li>Video capture when failures</li><li>API testing (built-in request fixture)</li><li>Visual comparisons (expect(page).toHaveScreenshot())</li><li>Component testing (for React/Vue/Svelte/Angular)</li></ul><p>Regarding its architecture, Playwright takes a different architectural approach than Selenium. It maintains its own patched versions of Chromium, Firefox, and WebKit and it uses internally an extended version of the Chrome DevTools Protocol (CDP) and a custom WebSocket-based protocol to control these browsers uniformly.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/935/1*gaVyHEBWW5Uf-VYHaJuzsw.png" /></figure><p>This way, for creating complete end-to-end tests with Playwright and Java, we typically use the Playwright API plus other tools, such as a unit testing framework (e.g., JUnit, TestNG), reporting tools, etc. For <a href="https://github.com/bonigarcia/browser-automation-apis/blob/main/playwright/java/src/test/java/io/github/bonigarcia/playwright/basic/HelloWorldPlaywrightTest.java">example</a>:</p><pre>import static org.assertj.core.api.Assertions.assertThat;<br>import org.junit.jupiter.api.AfterEach;<br>import org.junit.jupiter.api.BeforeEach;<br>import org.junit.jupiter.api.Test;<br>import com.microsoft.playwright.Browser;<br>import com.microsoft.playwright.Page;<br>import com.microsoft.playwright.Playwright;<br><br>class HelloWorldPlaywrightTest {<br><br>    Browser browser;<br>    Page page;<br><br>    @BeforeEach<br>    void setup() {<br>        browser = Playwright.create().chromium().launch();<br>        page = browser.newContext().newPage();<br>    }<br><br>    @Test<br>    void test() {<br>        page.navigate(&quot;https://bonigarcia.dev/selenium-webdriver-java/&quot;);<br>        String title = page.title();<br>        assertThat(title).contains(&quot;Selenium WebDriver&quot;);<br>    }<br><br>    @AfterEach<br>    void teardown() {<br>        browser.close();<br>    }<br><br>}</pre><h4>Advanced features</h4><p>One of the most attractive features of Playwright compared to Selenium is its built-in <strong>automatic waiting </strong>mechanism. Playwright intelligently waits for elements to be ready before performing actions, such as clicking or typing, removing the need for explicit waits. The following <a href="https://github.com/bonigarcia/browser-automation-apis/blob/main/playwright/java/src/test/java/io/github/bonigarcia/playwright/basic/SlowLoginPlaywrightTest.java">test</a> illustrates this feature:</p><pre>import static org.assertj.core.api.Assertions.assertThat;<br>import java.nio.file.Paths;<br>import org.junit.jupiter.api.AfterEach;<br>import org.junit.jupiter.api.BeforeEach;<br>import org.junit.jupiter.api.Test;<br>import com.microsoft.playwright.Browser;<br>import com.microsoft.playwright.BrowserType;<br>import com.microsoft.playwright.Page;<br>import com.microsoft.playwright.Playwright;<br><br>class SlowLoginPlaywrightTest {<br><br>    Browser browser;<br>    Page page;<br><br>    @BeforeEach<br>    void setup() {<br>        browser = Playwright.create().chromium()<br>                .launch(new BrowserType.LaunchOptions().setHeadless(false));<br>        page = browser.newContext().newPage();<br>    }<br><br>    @Test<br>    void test() {<br>        // Open system under test (SUT)<br>        page.navigate(<br>                &quot;https://bonigarcia.dev/selenium-webdriver-java/login-slow.html&quot;);<br><br>        // Log in<br>        page.fill(&quot;#username&quot;, &quot;user&quot;);<br>        page.fill(&quot;#password&quot;, &quot;user&quot;);<br>        page.click(&quot;button[type=&#39;submit&#39;]&quot;);<br><br>        // Assert expected text<br>        String successText = page.textContent(&quot;#success&quot;);<br>        assertThat(successText).contains(&quot;Login successful&quot;);<br><br>        // Take screenshot<br>        page.screenshot(new Page.ScreenshotOptions()<br>                .setPath(Paths.get(&quot;slow-login-playwright.png&quot;)));<br>    }<br><br>    @AfterEach<br>    void teardown() {<br>        browser.close();<br>    }<br><br>}</pre><p>Another appealing feature of Playwright is the <strong>trace viewer</strong>, a powerful debugging tool that lets you replay your test execution step by step. It records snapshots, network activity, console logs, and DOM states during a test run, allowing you to visually inspect what happened at each point and easily identify the cause of failures. For <a href="https://github.com/bonigarcia/browser-automation-apis/blob/main/playwright/java/src/test/java/io/github/bonigarcia/playwright/basic/SlowLoginPlaywrightTest.java">example</a>:</p><pre>import static org.assertj.core.api.Assertions.assertThat;<br>import java.nio.file.Paths;<br>import org.junit.jupiter.api.AfterEach;<br>import org.junit.jupiter.api.BeforeEach;<br>import org.junit.jupiter.api.Test;<br>import com.microsoft.playwright.Browser;<br>import com.microsoft.playwright.BrowserType;<br>import com.microsoft.playwright.Page;<br>import com.microsoft.playwright.Playwright;<br><br>class SlowLoginPlaywrightTest {<br><br>    Browser browser;<br>    Page page;<br><br>    @BeforeEach<br>    void setup() {<br>        browser = Playwright.create().chromium()<br>                .launch(new BrowserType.LaunchOptions().setHeadless(false));<br>        page = browser.newContext().newPage();<br>    }<br><br>    @Test<br>    void test() {<br>        // Open system under test (SUT)<br>        page.navigate(<br>                &quot;https://bonigarcia.dev/selenium-webdriver-java/login-slow.html&quot;);<br><br>        // Log in<br>        page.fill(&quot;#username&quot;, &quot;user&quot;);<br>        page.fill(&quot;#password&quot;, &quot;user&quot;);<br>        page.click(&quot;button[type=&#39;submit&#39;]&quot;);<br><br>        // Assert expected text<br>        String successText = page.textContent(&quot;#success&quot;);<br>        assertThat(successText).contains(&quot;Login successful&quot;);<br><br>        // Take screenshot<br>        page.screenshot(new Page.ScreenshotOptions()<br>                .setPath(Paths.get(&quot;slow-login-playwright.png&quot;)));<br>    }<br><br>    @AfterEach<br>    void teardown() {<br>        browser.close();<br>    }<br><br>}</pre><p><strong>Video recording</strong> is another built-in feature in Playwright for Java, for <a href="https://github.com/bonigarcia/browser-automation-apis/blob/main/playwright/java/src/test/java/io/github/bonigarcia/playwright/recording/RecordingSlowLoginPlaywrightTest.java">example</a>, as follows:</p><pre>import static org.assertj.core.api.Assertions.assertThat;<br>import java.nio.file.Paths;<br>import org.junit.jupiter.api.AfterEach;<br>import org.junit.jupiter.api.BeforeEach;<br>import org.junit.jupiter.api.Test;<br>import com.microsoft.playwright.Browser;<br>import com.microsoft.playwright.BrowserType;<br>import com.microsoft.playwright.Page;<br>import com.microsoft.playwright.Playwright;<br><br>class RecordingSlowLoginPlaywrightTest {<br><br>    Browser browser;<br>    Page page;<br><br>    @BeforeEach<br>    void setup() {<br>        browser = Playwright.create().chromium()<br>                .launch(new BrowserType.LaunchOptions().setHeadless(false));<br>        Browser.NewContextOptions options = new Browser.NewContextOptions()<br>                .setRecordVideoDir(Paths.get(&quot;.&quot;));<br>        page = browser.newContext(options).newPage();<br>    }<br><br>    @Test<br>    void test() {<br>        // Open system under test (SUT)<br>        page.navigate(<br>                &quot;https://bonigarcia.dev/selenium-webdriver-java/login-slow.html&quot;);<br><br>        // Log in<br>        page.fill(&quot;#username&quot;, &quot;user&quot;);<br>        page.fill(&quot;#password&quot;, &quot;user&quot;);<br>        page.click(&quot;button[type=&#39;submit&#39;]&quot;);<br><br>        // Assert expected text<br>        String successText = page.textContent(&quot;#success&quot;);<br>        assertThat(successText).contains(&quot;Login successful&quot;);<br>    }<br><br>    @AfterEach<br>    void teardown() {<br>        browser.close();<br>    }<br><br>}</pre><h3>Conclusions</h3><p>Selenium and Playwright cannot be directly compared since they are naturally different. The following table summarizes their key differences:</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*MCGpAR6w_AiC4jt2ljXV5Q.png" /></figure><p>Finally, we can find both pros and cons in Selenium and Playwright in Java, namely:</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*Gehuu0jZ8xuBVqcPCqYzVw.png" /></figure><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=794873cdc449" width="1" height="1" alt="">]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[WebDriverManager 6: Automated driver management and other helper features for Selenium WebDriver in…]]></title>
            <link>https://medium.com/@boni.gg/webdrivermanager-6-automated-driver-management-and-other-helper-features-for-selenium-webdriver-in-842e6f1e76d8?source=rss-7118f7714c53------2</link>
            <guid isPermaLink="false">https://medium.com/p/842e6f1e76d8</guid>
            <dc:creator><![CDATA[Boni García]]></dc:creator>
            <pubDate>Wed, 19 Mar 2025 15:50:52 GMT</pubDate>
            <atom:updated>2025-03-19T15:50:52.054Z</atom:updated>
            <content:encoded><![CDATA[<h3><strong>WebDriverManager 6: Automated driver management and other helper features for Selenium WebDriver in Java</strong></h3><figure><img alt="" src="https://cdn-images-1.medium.com/max/700/1*0Zr49-5u6Xcub1ZZAHWcEg.png" /></figure><p><a href="https://github.com/bonigarcia/webdrivermanager/">WebDriverManager</a> was first released on 19 March 2015. At that time, Selenium was in version 2 (i.e., the first release of “Selenium WebDriver”), and it was considered a “non-batteries included” library. That means that, to control a browser programmatically with Selenium (such as Chrome or Firefox), Selenium users first should manage (i.e., download, setup, and maintain) the required drivers by Selenium (e.g., chromedriver for Chrome or geckodriver for Firefox) manually.</p><p>I found that manual process suboptimal. I used to think that, in an ideal world for browser automation, the driver management process should be automated. I looked for a solution to that, but I did not find any tool doing this. At the same time, I was working as a first-year lecturer at the university. I was chosen to give a course on “web programming.” Since Selenium was an important part of my career (I have used Selenium RC in my PhD dissertation and Selenium WebDriver in my research activities), I prepared a lecture about automated web testing using JUnit 4 and Selenium 2. But I didn’t like the driver thing. I wanted my students to go directly to the point and start playing with Selenium without wasting their energy in the driver setup. So, I created WebDriverManager 1.0.0 and recommended my students to use it.</p><p>In the following years, and thanks to the power of open source, WebDriverManager started to be used by others. I felt that WebDriverManager was addressing a real need of Selenium developers since the project began to grow, and people contributed to the WebDriverManager repo with pull requests, bug fixes, comments, etc. Moreover, the WebDriverManager concept was migrated from Java to other languages like <a href="https://www.npmjs.com/package/webdriver-manager">webdriver-manager</a> for JavaScript, <a href="https://pypi.org/project/webdriver-manager">webdriver-manager</a> for Python, or <a href="https://github.com/rosolko/WebDriverManager.Net">WebDriverManager.Net</a> for C#. This way, I have continued maintaining and incorporating more and more features to WebDriverManager in my leisure time since then. Nowadays, WebDriverManager is a well-known Java library used by hundreds of thousands of projects, with around 3 million downloads monthly in Maven Central.</p><h3><strong>WebDriverManager 6</strong></h3><p>To celebrate the 10th birthday of the project, <a href="https://bonigarcia.dev/webdrivermanager">WebDriverManager 6</a> has been released on 19 March 2025. This new major feature ships some important novelties.</p><h4>Support to docker-selenium</h4><p>One of the most relevant features shipped in WebDriverManager 5 was the ability to create browsers in Docker containers out of the box. To support this feature, WebDriverManager used the Docker images by WebDriverManager created and maintained by <a href="https://github.com/aerokube/images">Aerokube</a>. Unfortunately, these images have not been available since December 2024. Luckily, we have <a href="https://github.com/SeleniumHQ/docker-selenium">docker-selenium</a>, a project maintained by Selenium that provides Docker images for running Selenium tests in containers. I have always wanted to support docker-selenium in WebDriverManager, but the lack of time stopped me. However, the sudden unavailability of Aerokube’s images forced me to migrate to docker-selenium quickly.</p><p>Luckily, the WebDriverManager API is the same after this significant change. Appealing features, such as browser recording, noVNC access, or unstable versions (i.e., beta and dev releases), are still supported. Even better, WebDriverManager supports ARM64 Docker images with docker-selenium thanks to the <em>seleniarm </em>images.</p><iframe src="" width="0" height="0" frameborder="0" scrolling="no"><a href="https://medium.com/media/9504357ca68db27af8e10f8ca0cb426a/href">https://medium.com/media/9504357ca68db27af8e10f8ca0cb426a/href</a></iframe><h4>Improved browser version discovery</h4><p>Browser version discovery was a key feature first introduced in WebDriverManager 3. Knowing the local browser version is key to selecting the proper driver version. To that aim, WebDriverManager has an internal knowledge database to discover browser versions using shell commands (e.g., <em>google-chrome --version</em> for Chrome in Linux).</p><p>WebDriverManager used WMIC (Windows Management Instrumentation Command-line) in Windows for browser version discovery. However, Microsoft announced that <a href="https://techcommunity.microsoft.com/t5/windows-it-pro-blog/wmi-command-line-wmic-utility-deprecation-next-steps/ba-p/4039242">WMIC is deprecated</a> as of Windows 10, version 21H1, and will be removed in a future version. Therefore, WebDriverManger 6 started to use PowerShell commands to discover browser versions. This feature is transparent for end-users, and it will guarantee that automated driver management will continue adequately in the upcoming years.</p><p>Finally, WebDriverManager 6 included a new API method called <em>.browserBinary()</em> to provide a better configuration capability regarding browser version discovery. This method allows the specification of the browser binary path used for driver version discovery.</p><iframe src="" width="0" height="0" frameborder="0" scrolling="no"><a href="https://medium.com/media/b0fe0cd821e26087a86c125481458015/href">https://medium.com/media/b0fe0cd821e26087a86c125481458015/href</a></iframe><p>As usual, this feature can also be configured using Java properties per the different browsers: <em>wdm.chromeBinary</em>, <em>wdm.operaBinary</em>, <em>wdm.edgeBinary</em>, <em>wdm.firefoxBinary</em>, and <em>wdm.chromiumBinary</em>.</p><h4>Support to snap browsers/drivers</h4><p>Some browsers, like Firefox and Chromium, are distributed through a packaging and deployment system called <a href="https://snapcraft.io/">snap</a>. Snap packages are self-contained and sandboxed, including all the dependencies required to run the application. For browsers, that means that both the browser and the driver are included in the snap package. As of release 6, WebDriverManager can detect if the local browser has been installed using snap, using the proper driver (which should be already locally installed). Again, this feature is transparent for WebDriverManager users. If the browser and driver are available, it can be used automatically in a Selenium session managed by WebDriverManager.</p><h3><strong>Selenium Manager</strong></h3><p>But wait. What about <a href="https://www.selenium.dev/documentation/selenium_manager/">Selenium Manager</a>? Is it not the same than WebDriverManager? Let me explain the little history of Selenium Manager.</p><p>In 2021, the Selenium project published the results of the first official <a href="https://www.selenium.dev/blog/2021/selenium-survey-results/">Selenium survey</a>. One of the key findings in this study was that Selenium users wanted “batteries included.” In other words, they want Selenium to manage browsers and drivers, similarly to the features provided by WebDriverManager. This way, I joined Sauce Labs as a Staff Software Engineer in the <a href="https://opensource.saucelabs.com/">Open Source Program Office</a> from 2022 to 2023 and contributed to the Selenium project, particularly developing Selenium Manager. Selenium Manager has been developed in Rust following the lessons learned from WebDriverManager, implementing the same driver resolution algorithm first designed in WebDriverManager. Selenium Manager is fully integrated into Selenium and is used by all the official binding languages: Java, JavaScript, Python, Ruby, and .Net. As shown in its usage statistics (publicly available through <a href="https://plausible.io/manager.selenium.dev">Plausible</a>), Selenium Manager is used daily by hundreds of thousands of unique users worldwide. So, below, you can find some differences between WebDriverManager and Selenium Manager.</p><p><em>Is Selenium Manager a replacement for WebDriverManager?</em> For the use case of automated driver management, yes. In other words, if you use WebDriverManager only for driver management, you can safely switch to Selenium Manager.</p><p><em>What are the key differences between WebDriverManager and Selenium Manager? </em>Both projects provide automated driver management (chromedriver, geckodriver, etc.). However, WebDriverManager ships several unavailable features in Selenium Manager (e.g., self-managed browsers in Docker containers or custom monitoring features). On the other side, Selenium Manager provides automated browser management using the browser binary distributions for Windows, macOS, and Linux (e.g., based on Chrome for Testing).</p><p><em>What are the reasons to continue using WebDriverManager?</em> There can be different reasons, such as:</p><ul><li>Advanced features. WebDriverManager provides self-managed browsers in Docker containers (now through docker-selenium). This feature allows you to delegate all the browser infrastructure management (including dev and beta browser releases) to WebDriverManager with Docker. Also, it enables the screencasting of Selenium sessions, which can be a game-changing characteristic for failure analysis (troubleshooting). Besides, WebDriverManager provides custom monitoring features (through <a href="https://bonigarcia.dev/browserwatcher/">BrowserWatcher</a>), such as seamless console log gathering, Content Security Policy (CSP) disabling, or viewport screencasting (i.e., not record the whole desktop but only the browser viewport).</li><li>Rich configuration. One of the key aspects of WebDriverManager is that all these features can be <a href="https://bonigarcia.dev/webdrivermanager/#advanced-configuration">configured</a> through its API, using Java system properties and even environmental variables. These capabilities are very convenient for fine-tuning every feature provided by WebDriverManager, such as automated driver management, docker management, or browser monitoring.</li><li>Legacy support. The minimum version for the latest releases of Selenium 4 (i.e., those that have Selenium Manager) is Java 11. If you cannot bump to Java 11 yet (you should, but sometimes it is impossible), you can continue using WebDriverManager since even release 6 is compiled using Java 8.</li></ul><h3><strong>What’s coming next</strong></h3><p>I’m devoted to maintaining Selenium Manager and WebDriverManager in the upcoming years. Now I am part of the <a href="https://www.selenium.dev/project/structure/#tlc">Selenium Technical Leadership Committee (TLC)</a>, so my commitment is to continue improving the browser automation experience for Selenium users. Selenium Manager is still in beta but has already proven stable at this writing so that it will be released as stable in the upcoming Selenium 5. Also, in light of usage numbers, WebDriverManager is still a popular and helpful tool, so I will continue maintaining the project for the Java community.</p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=842e6f1e76d8" width="1" height="1" alt="">]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[WebDriverManager 5: Automated driver management and Docker builder for Selenium WebDriver]]></title>
            <link>https://medium.com/@boni.gg/webdrivermanager-5-automated-driver-management-and-docker-builder-for-selenium-webdriver-a0a0f747a35d?source=rss-7118f7714c53------2</link>
            <guid isPermaLink="false">https://medium.com/p/a0a0f747a35d</guid>
            <category><![CDATA[test-automation]]></category>
            <category><![CDATA[selenium]]></category>
            <category><![CDATA[software-testing]]></category>
            <category><![CDATA[selenium-webdriver]]></category>
            <category><![CDATA[java]]></category>
            <dc:creator><![CDATA[Boni García]]></dc:creator>
            <pubDate>Mon, 13 Sep 2021 12:37:48 GMT</pubDate>
            <atom:updated>2025-03-19T09:35:56.506Z</atom:updated>
            <content:encoded><![CDATA[<figure><img alt="" src="https://cdn-images-1.medium.com/max/700/1*gB3qBn3NKYC47F-DON_kvw.png" /></figure><p><a href="https://bonigarcia.dev/webdrivermanager/">WebDriverManager</a> is an open-source Java library that carries out the management (i.e., download, setup, and maintenance) of the drivers required by <a href="https://www.selenium.dev/documentation/webdriver/">Selenium WebDriver</a> (e.g., chromedriver, geckodriver, msedgedriver, etc.) in a fully automated manner.</p><p>WebDriverManager enables the development of portable WebDriver tests while reducing the development and maintenance efforts. For instance, the skeleton of a <a href="https://junit.org/junit5/docs/current/user-guide/">JUnit 5</a> test using Selenium WebDriver and WebDriverManager is as follows:</p><iframe src="" width="0" height="0" frameborder="0" scrolling="no"><a href="https://medium.com/media/fbfaef4c2161a6e116954973ca66a4f9/href">https://medium.com/media/fbfaef4c2161a6e116954973ca66a4f9/href</a></iframe><p>WebDriverManager implements a resolution algorithm for automated driver management. The foundations of this algorithm are:</p><ol><li>Browser version discovery.</li><li>Driver version match.</li><li>Driver and resolution cache.</li><li>Export driver path as system property setup.</li></ol><p>You can find all the internal details of this resolution algorithm in the paper <a href="https://link.springer.com/article/10.1007/s10664-021-09975-3">Automated driver management for Selenium WebDriver</a>, published in the Springer Journal of Empirical Software Engineering in 2021.</p><h3><strong>WebDriverManager 5: the next generation</strong></h3><p>WebDriverManager was first released in 2015. Nowadays, it is a well-known helper library for Selenium WebDriver, used in thousands of projects. WebDriverManager 5 was released in 2021. As usual, this release allows the automated driver management for Selenium WebDriver. Moreover, this release provides other features aimed to ease the development of Selenium WebDriver tests.</p><h4><strong>New documentation</strong></h4><p>The documentation has been completely rewritten in WebDriverManager 5:</p><p><a href="https://bonigarcia.dev/webdrivermanager/">https://bonigarcia.dev/webdrivermanager/</a></p><p>Internally, this site is done in <a href="https://asciidoc.org/">AsciiDoc</a>, and it is generated to <a href="https://bonigarcia.dev/webdrivermanager/">HTML</a>, <a href="https://bonigarcia.dev/webdrivermanager/webdrivermanager.pdf">PDF</a>, and <a href="https://bonigarcia.dev/webdrivermanager/webdrivermanager.epub">EPUB</a>.</p><h4><strong>Browser finder</strong></h4><p>As of version 5, WebDriverManager allows detecting if a given browser is installed or not in the local system. To this aim, each manager provides the method <em>getBrowserPath()</em>. This method returns an <em>Optional&lt;Path&gt;</em>, which is empty if a given browser is not installed in the system or the browser path (within the optional object) when detected. You can use this feature to skip tests using assumptions, for instance, as follows:</p><iframe src="" width="0" height="0" frameborder="0" scrolling="no"><a href="https://medium.com/media/a22bbc69928204c80e09fbd1c44640ea/href">https://medium.com/media/a22bbc69928204c80e09fbd1c44640ea/href</a></iframe><h4><strong>WebDriver builder</strong></h4><p>WebDriverManager 5 also allows instantiating WebDriver objects (e.g., ChromeDriver, FirefoxDriver, etc.) using the WebDriverManager API. This feature is available using the method <em>create()</em> of each manager. For instance:</p><iframe src="" width="0" height="0" frameborder="0" scrolling="no"><a href="https://medium.com/media/beaa816114e2edb33a16ff1cd97ab787/href">https://medium.com/media/beaa816114e2edb33a16ff1cd97ab787/href</a></iframe><h4><strong>Browsers in Docker</strong></h4><p>Another relevant new feature available in WebDriverManager 5 is the ability to create browsers in Docker containers out of the box. To use it, we need to invoke the method <em>browserInDocker()</em> in conjunction with <em>create()</em> of a given manager. For instance:</p><iframe src="" width="0" height="0" frameborder="0" scrolling="no"><a href="https://medium.com/media/7693a671b904b2126b5d7a647fdb8557/href">https://medium.com/media/7693a671b904b2126b5d7a647fdb8557/href</a></iframe><p>The usfed Docker images by WebDriverManager have been created and maintained by <a href="https://aerokube.com/images/latest/">Aerokube</a>. Therefore, Chrome (desktop and mobile), Firefox, Edge, Opera, and Safari (WebKit engine) are the available browsers to be executed as Docker containers in WebDriverManager. In addition, we can use the beta and development versions of Chrome and Firefox, thanks to a fork of the Aerokube images maintained by <a href="https://hub.docker.com/r/twilio/selenoid/">Twilio</a>.</p><iframe src="" width="0" height="0" frameborder="0" scrolling="no"><a href="https://medium.com/media/bc6937a4f6ca3c91618b803b977abd83/href">https://medium.com/media/bc6937a4f6ca3c91618b803b977abd83/href</a></iframe><p>WebDriverManager allows connecting to the remote desktop session simply invoking the method <em>enableVnc()</em> of a dockerized browser. In addition, we can use the method <em>enableRecording()</em> to record the browser session.</p><iframe src="" width="0" height="0" frameborder="0" scrolling="no"><a href="https://medium.com/media/dd7aad46649200489218514539e0f2ef/href">https://medium.com/media/dd7aad46649200489218514539e0f2ef/href</a></iframe><h4><strong>Other usages</strong></h4><p>WebDriverManager can be used as a <a href="https://bonigarcia.dev/webdrivermanager/#webdrivermanager-cli">CLI tool</a>, <a href="https://bonigarcia.dev/webdrivermanager/#webdrivermanager-agent">Java agent</a>, or <a href="https://bonigarcia.dev/webdrivermanager/#webdrivermanager-server">Selenium Server</a>. Take a look at the documentation for further details:</p><p><a href="https://bonigarcia.dev/webdrivermanager/">https://bonigarcia.dev/webdrivermanager/</a></p><h3><strong>Selenium-Jupiter</strong></h3><p>WebDriverManager is the foundation tool of <a href="https://bonigarcia.dev/selenium-jupiter">Selenium-Jupiter</a>, an open-source <a href="https://junit.org/junit5/docs/current/user-guide/">JUnit 5</a> extension for developing Selenium WebDriver tests. Thanks to the parameter resolution provided by JUnit 5, the required boilerplate of a WebDriver test is reduced to the minimum.</p><iframe src="" width="0" height="0" frameborder="0" scrolling="no"><a href="https://medium.com/media/9bbcd634ee25b1aa655b4c7efd58b495/href">https://medium.com/media/9bbcd634ee25b1aa655b4c7efd58b495/href</a></iframe><p>Selenium-Jupiter also provides seamless integration with Docker, recordings, VNC, and more. Check out the documentation for the complete reference and examples:</p><p><a href="https://bonigarcia.dev/selenium-jupiter">https://bonigarcia.dev/selenium-jupiter</a></p><iframe src="" width="0" height="0" frameborder="0" scrolling="no"><a href="https://medium.com/media/983e6a1372a2657e5c8cbbc33a33bd4b/href">https://medium.com/media/983e6a1372a2657e5c8cbbc33a33bd4b/href</a></iframe><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=a0a0f747a35d" width="1" height="1" alt="">]]></content:encoded>
        </item>
    </channel>
</rss>