Issue
Does anyone know why I sometimes get exception when I use Selenium together with Testcontainers. See below:
Exception has occurred: CLR/OpenQA.Selenium.WebDriverException An exception of type 'OpenQA.Selenium.WebDriverException' occurred in WebDriver.dll but was not handled in user code: 'An unknown exception was encountered sending an HTTP request to the remote WebDriver server for URL http://localhost:4444/session. The exception message was: An error occurred while sending the request.' Inner exceptions found, see $exception in variables window for more details. Innermost exception System.IO.IOException : The response ended prematurely. at System.Net.Http.HttpConnection.d__61.MoveNext()
This happens half the time when i run the following test constructor (C# / xUnit.net):
public DockerShould()
{
var gridNetwork = new NetworkBuilder()
.WithName("gridNetwork")
.Build();
const int SessionPort = 4444;
var containerHub = new ContainerBuilder()
.WithImage("selenium/hub:4.8")
.WithName("selenium-hub")
.WithPortBinding(4442, 4442)
.WithPortBinding(4443, 4443)
.WithPortBinding(SessionPort, SessionPort)
.WithNetwork(gridNetwork)
.Build();
var firefoxEnvironment = new Dictionary<string, string>()
{
{"SE_EVENT_BUS_HOST", "selenium-hub"},
{"SE_EVENT_BUS_PUBLISH_PORT", "4442"},
{"SE_EVENT_BUS_SUBSCRIBE_PORT", "4443"}
};
var containerFirefox = new ContainerBuilder()
.WithImage("selenium/node-firefox:4.8")
.WithEnvironment(firefoxEnvironment)
.WithNetwork(gridNetwork)
.Build();
var firefoxVideoEnvironment = new Dictionary<string, string>()
{
{"DISPLAY_CONTAINER_NAME", "firefox"},
{"FILE_NAME", "firefox.mp4"}
};
var containerFirefoxVideo = new ContainerBuilder()
.WithImage("selenium/video:ffmpeg-4.3.1-20230210")
.WithNetwork(gridNetwork)
.WithEnvironment(firefoxVideoEnvironment)
// .WithWaitStrategy(Wait.ForUnixContainer().UntilPortIsAvailable(SessionPort))
.Build();
gridNetwork.CreateAsync().Wait();
containerHub.StartAsync().Wait();
containerFirefox.StartAsync().Wait();
containerFirefoxVideo.StartAsync().Wait();
Thread.Sleep(5000);
_remoteWebDriver = new RemoteWebDriver(new Uri("http://localhost:4444"), new FirefoxOptions());
}
The exception occurs when creating the new RemoteWebDriver. I've added a thread.sleep to give a bit of a delay before the variable is created. I'm not sure it's really helping much. Is there a more elegant way to ensure all containers are started up before attempting to create the web driver (which i'm assuming is the problem)?
Solution
Your configuration has a few shortcomings. I am uncertain as to which one is ultimately causing the issue, but I have provided a working example below. The crucial parts have been commented. Please note that the example does not incorporate a wait strategy to determine the readiness of the container or the service inside it. That is an aspect that you will still need to address. But first lets take a look at some basics.
- Please consider reading the article Consuming the Task-based Asynchronous Pattern. Testcontainers for .NET utilizes the Task-based Asynchronous Pattern (TAP). I noticed that you tend to block the asynchronous context frequently.
- You do not need to bind ports for container-to-container communication.
- Avoid using fixed port bindings such as
WithPortBinding(4444, 4444)
. To prevent port conflicts, assign a random host port by usingWithPortBinding(4444, true)
and retrieve it from the container instance usingGetMappedPublicPort(4444)
. - Do not use fixed container names for the same reason mentioned in 3. Use
WithNetworkAliases(string)
instead. - Do not use
localhost
to access services running inside containers. The endpoint varies according to the Docker environment. Use theHostname
property instead.
public sealed class StackOverflow : IAsyncLifetime
{
private const ushort WebDriverPort = 4444;
private readonly INetwork _network;
private readonly IContainer _selenium;
private readonly IContainer _firefox;
private readonly IContainer _ffmpg;
public StackOverflow()
{
_network = new NetworkBuilder()
.Build();
_selenium = new ContainerBuilder()
.WithImage("selenium/hub:4.8")
// Use random assigned host ports to access the service running inside the containers.
.WithPortBinding(WebDriverPort, true)
.WithNetwork(_network)
// Use a network-alias to communication between containers.
.WithNetworkAliases(nameof(_selenium))
.Build();
_firefox = new ContainerBuilder()
.WithImage("selenium/node-firefox:4.8")
.WithEnvironment("SE_EVENT_BUS_HOST", nameof(_selenium))
.WithEnvironment("SE_EVENT_BUS_PUBLISH_PORT", "4442")
.WithEnvironment("SE_EVENT_BUS_SUBSCRIBE_PORT", "4443")
.WithNetwork(_network)
.WithNetworkAliases(nameof(_firefox))
.Build();
_ffmpg = new ContainerBuilder()
.WithImage("selenium/video:ffmpeg-4.3.1-20230210")
.WithEnvironment("DISPLAY_CONTAINER_NAME", nameof(_firefox))
.WithEnvironment("FILE_NAME", nameof(_firefox) + ".mp4")
.WithNetwork(_network)
.WithNetworkAliases(nameof(_ffmpg))
.Build();
}
public async Task InitializeAsync()
{
await _network.CreateAsync()
.ConfigureAwait(false);
// You can await Task.WhenAll(params Task[]) too.
await _selenium.StartAsync()
.ConfigureAwait(false);
await _firefox.StartAsync()
.ConfigureAwait(false);
await _ffmpg.StartAsync()
.ConfigureAwait(false);
}
public async Task DisposeAsync()
{
await _selenium.DisposeAsync()
.ConfigureAwait(false);
await _firefox.DisposeAsync()
.ConfigureAwait(false);
await _ffmpg.DisposeAsync()
.ConfigureAwait(false);
await _network.DeleteAsync()
.ConfigureAwait(false);
}
[Fact]
public async Task Question()
{
// TODO: The container configurations mentioned above lack a wait strategy. It is crucial that you add a wait strategy to each of them to determine readiness (afterwards remove this line).
await Task.Delay(TimeSpan.FromSeconds(15))
.ConfigureAwait(false);
// Use the Hostname property instead of localhost. Get the random assigned host port with GetMappedPublicPort(ushort).
var webDriver = new RemoteWebDriver(new UriBuilder(Uri.UriSchemeHttp, _selenium.Hostname, _selenium.GetMappedPublicPort(WebDriverPort)).Uri, new FirefoxOptions());
Assert.NotNull(webDriver.SessionId);
}
}
Update:
Meanwhile, there is a Testcontainers WebDriver module (depends on Selenium) that sets everything up.
Answered By - Andre Hofmeister
0 comments:
Post a Comment
Note: Only a member of this blog may post a comment.