diff --git a/docs/guide/protocols/websocket.md b/docs/guide/protocols/websocket.md
index b62c4a63..ca23abf8 100644
--- a/docs/guide/protocols/websocket.md
+++ b/docs/guide/protocols/websocket.md
@@ -49,10 +49,33 @@ public class Test {
}
```
+::: tip
+You can use JMeter variables or functions in the connect URL parts (host, port, path or query). Prefer this over putting the whole URL in a single variable:
+
+```java
+vars()
+ .set("HOST", "ws.postman-echo.com")
+ .set("PORT", "443")
+ .set("TOKEN", "abc123"),
+websocketConnect("wss://${HOST}:${PORT}/raw?token=${TOKEN}")
+```
+
+You can also use a Java variable when the full URL is known when building the test plan:
+
+```java
+String wsUrl = "wss://ws.postman-echo.com/raw";
+websocketConnect(wsUrl)
+```
+:::
+
::: warning
Only `ws://` and `wss://` protocols are supported. Using any other scheme will throw an `IllegalArgumentException`.
:::
+::: warning
+A full URL as a single JMeter expression (for example `websocketConnect("${URL}")`) is not supported. The underlying WebSocket plugin requires separate server, port, path and TLS fields, and the DSL parses the URL at build time to fill them. Without a literal `ws://` or `wss://` scheme in the string, those fields cannot be set correctly. Use expressions in URL parts (for example `ws://${HOST}:${PORT}/path`) instead.
+:::
+
::: tip
You can use a non-blocking read if necessary in the following way:
@@ -65,4 +88,4 @@ In this case, it is not recommended to add an assertion because the response cou
::: warning
The WebSocket plugin only supports one connection per thread at a time. If you want to change the WebSocket server during execution, you should add a disconnect sampler and then establish a new connection.
-:::
\ No newline at end of file
+:::
diff --git a/jmeter-java-dsl-websocket/src/main/java/us/abstracta/jmeter/javadsl/websocket/WebsocketJMeterDsl.java b/jmeter-java-dsl-websocket/src/main/java/us/abstracta/jmeter/javadsl/websocket/WebsocketJMeterDsl.java
index 1a82411e..35f4c100 100644
--- a/jmeter-java-dsl-websocket/src/main/java/us/abstracta/jmeter/javadsl/websocket/WebsocketJMeterDsl.java
+++ b/jmeter-java-dsl-websocket/src/main/java/us/abstracta/jmeter/javadsl/websocket/WebsocketJMeterDsl.java
@@ -11,8 +11,6 @@
import eu.luminis.jmeter.wssampler.SingleWriteWebSocketSampler;
import eu.luminis.jmeter.wssampler.SingleWriteWebSocketSamplerGui;
import java.lang.reflect.Method;
-import java.net.URI;
-import java.net.URISyntaxException;
import java.util.Arrays;
import java.util.List;
import org.apache.jmeter.testelement.TestElement;
@@ -25,6 +23,7 @@
import us.abstracta.jmeter.javadsl.codegeneration.params.EnumParam;
import us.abstracta.jmeter.javadsl.codegeneration.params.StringParam;
import us.abstracta.jmeter.javadsl.core.samplers.BaseSampler;
+import us.abstracta.jmeter.javadsl.http.JmeterUrl;
/**
* Provides factory methods to create WebSocket samplers for performance
@@ -71,6 +70,10 @@ public class WebsocketJMeterDsl {
private WebsocketJMeterDsl() {
}
+ private static boolean containsJmeterExpression(String value) {
+ return value != null && value.contains("${");
+ }
+
/**
* Creates a WebSocket connect sampler to establish a connection to the server.
*
@@ -84,9 +87,26 @@ private WebsocketJMeterDsl() {
* URL Format: {@code ws://host:port/path?query} or
* {@code wss://host:port/path?query}
*
+ * The URL may be a fixed value or include JMeter expressions (variables or
+ * functions) in its parts. For example:
+ *
+ * - {@code ws://localhost:8080/raw} - fixed URL
+ * - {@code ws://${HOST}:${PORT}/raw} - host and port from JMeter variables
+ * - {@code wss://${SERVER}/chat?token=${TOKEN}} - host, path and query with
+ * expressions
+ *
+ * The DSL parses the URL at build time into the WebSocket plugin fields (server,
+ * port, path and TLS). JMeter then resolves expressions in each field at
+ * runtime.
+ *
+ * A full URL as a single JMeter expression (e.g. {@code ${URL}}) is not
+ * supported: without a literal {@code ws://} or {@code wss://} scheme the URL
+ * cannot be split into those fields. Prefer expressions in URL parts as shown
+ * above, or use a Java variable when the full URL is known at plan build time.
*
* @param url the WebSocket server URL. Supported schemes: {@code ws://} (plain)
- * and {@code wss://} (TLS)
+ * and {@code wss://} (TLS). May contain JMeter expressions in host,
+ * port, path or query.
* @return the connect sampler for further configuration or usage
* @since 2.2
*/
@@ -123,6 +143,31 @@ public static DslWriteSampler websocketWrite(String requestData) {
return new DslWriteSampler(requestData);
}
+ /**
+ * Same as {@link #websocketWrite(String)} but allowing to specify the payload data type.
+ *
+ * @param requestData the message to send to the WebSocket server
+ * @param dataType the payload data type
+ * @return the write sampler for further configuration or usage
+ * @since 2.2
+ */
+ public static DslWriteSampler websocketWrite(String requestData, PayloadDataType dataType) {
+ return new DslWriteSampler(requestData, dataType);
+ }
+
+ /**
+ * Same as {@link #websocketWrite(String, PayloadDataType)} but allowing to use JMeter
+ * expressions (variables or functions) to solve the actual data type value.
+ *
+ * @param requestData the message to send to the WebSocket server
+ * @param dataType a JMeter expression that returns the payload data type
+ * @return the write sampler for further configuration or usage
+ * @since 2.2
+ */
+ public static DslWriteSampler websocketWrite(String requestData, String dataType) {
+ return new DslWriteSampler(requestData, dataType);
+ }
+
/**
* Creates a WebSocket read sampler to receive a message from the server.
*
@@ -140,6 +185,56 @@ public static DslReadSampler websocketRead() {
return new DslReadSampler();
}
+ /**
+ * Same as {@link #websocketRead()} but allowing to specify the data type to read from the
+ * server.
+ *
+ * @param dataType the data type to read from the server
+ * @return the read sampler for further configuration or usage
+ * @since 2.2
+ */
+ public static DslReadSampler websocketRead(PayloadDataType dataType) {
+ return new DslReadSampler(dataType);
+ }
+
+ /**
+ * Same as {@link #websocketRead(PayloadDataType)} but allowing to use JMeter expressions
+ * (variables or functions) to solve the actual data type value.
+ *
+ * @param dataType a JMeter expression that returns the data type to read from the server
+ * @return the read sampler for further configuration or usage
+ * @since 2.2
+ */
+ public static DslReadSampler websocketRead(String dataType) {
+ return new DslReadSampler(dataType);
+ }
+
+ public enum PayloadDataType implements EnumParam.EnumPropertyValue {
+ TEXT("Text"),
+ BINARY("Binary");
+
+ private final String propertyValue;
+
+ PayloadDataType(String propertyValue) {
+ this.propertyValue = propertyValue;
+ }
+
+ @Override
+ public String propertyValue() {
+ return propertyValue;
+ }
+
+ public static boolean isText(String value) {
+ return value == null || TEXT.propertyValue().equalsIgnoreCase(value)
+ || "text".equalsIgnoreCase(value);
+ }
+
+ public static boolean isValidDataType(String value) {
+ return isText(value) || BINARY.propertyValue().equalsIgnoreCase(value)
+ || "binary".equalsIgnoreCase(value);
+ }
+ }
+
public static class DslConnectSampler extends BaseSampler {
private String connectionTimeoutMillis;
private String responseTimeoutMillis;
@@ -150,43 +245,36 @@ public static class DslConnectSampler extends BaseSampler {
private DslConnectSampler(String url) {
super("WebSocket Open Connection", OpenWebSocketSamplerGui.class);
- try {
- URI uri = new URI(url);
+ parseUrl(url);
+ }
- String scheme = uri.getScheme();
- if (scheme == null || (!"ws".equals(scheme) && !"wss".equals(scheme))) {
+ private void parseUrl(String url) {
+ JmeterUrl parsed = JmeterUrl.valueOf(url);
+ String scheme = parsed.protocol();
+ if (scheme != null && !containsJmeterExpression(scheme)) {
+ if (!"ws".equals(scheme) && !"wss".equals(scheme)) {
throw new IllegalArgumentException(
"Invalid WebSocket URL. Must start with 'ws://' or 'wss://'");
}
-
- this.tls = "wss".equals(scheme);
- this.server = uri.getHost();
- if (this.server == null) {
- throw new IllegalArgumentException("Invalid WebSocket URL. Host is required");
- }
-
- int port = uri.getPort();
- if (port == -1) {
- this.port = this.tls ? "443" : "80";
- } else {
- this.port = String.valueOf(port);
- }
-
- String path = uri.getPath();
- if (path == null || path.isEmpty()) {
- this.path = "/";
+ tls = "wss".equals(scheme);
+ }
+ server = parsed.host();
+ if ((server == null || server.isEmpty()) && scheme != null
+ && !containsJmeterExpression(scheme)) {
+ throw new IllegalArgumentException("Invalid WebSocket URL. Host is required");
+ }
+ String parsedPort = parsed.port();
+ if (parsedPort == null || parsedPort.isEmpty()) {
+ if (scheme != null && !containsJmeterExpression(scheme)) {
+ port = tls ? "443" : "80";
} else {
- this.path = path;
- }
-
- String query = uri.getQuery();
- if (query != null && !query.isEmpty()) {
- this.path = this.path + "?" + query;
+ port = "";
}
-
- } catch (URISyntaxException e) {
- throw new IllegalArgumentException("Invalid WebSocket URL: " + url, e);
+ } else {
+ port = parsedPort;
}
+ String parsedPath = parsed.path();
+ path = (parsedPath == null || parsedPath.isEmpty()) ? "/" : parsedPath;
}
@Override
@@ -199,9 +287,15 @@ protected TestElement buildTestElement() {
ret.setReadTimeout(responseTimeoutMillis);
}
ret.setTLS(tls);
- ret.setServer(server);
- ret.setPort(port);
- ret.setPath(path);
+ if (server != null) {
+ ret.setServer(server);
+ }
+ if (port != null) {
+ ret.setPort(port);
+ }
+ if (path != null) {
+ ret.setPath(path);
+ }
return ret;
}
@@ -424,16 +518,44 @@ protected MethodCall buildMethodCall(CloseWebSocketSampler testElement,
public static class DslWriteSampler extends BaseSampler {
private String requestData;
+ private String dataType;
private DslWriteSampler(String requestData) {
+ this(requestData, PayloadDataType.TEXT);
+ }
+
+ private DslWriteSampler(String requestData, PayloadDataType dataType) {
+ this(requestData, dataType.propertyValue());
+ }
+
+ private DslWriteSampler(String requestData, String dataType) {
super("WebSocket Single Write", SingleWriteWebSocketSamplerGui.class);
this.requestData = requestData;
+ this.dataType = dataType;
+ validateDataType(dataType);
+ }
+
+ private static void validateDataType(String dataType) {
+ if (dataType != null && !containsJmeterExpression(dataType)
+ && !PayloadDataType.isValidDataType(dataType)) {
+ throw new IllegalArgumentException("Invalid data type. Must be 'text' or 'binary'");
+ }
+ }
+
+ private static void setWriteDataType(SingleWriteWebSocketSampler write, String dataType) {
+ if (containsJmeterExpression(dataType)) {
+ write.setProperty("payloadType", dataType);
+ } else if (PayloadDataType.isText(dataType)) {
+ write.setType(DataPayloadType.Text);
+ } else {
+ write.setType(DataPayloadType.Binary);
+ }
}
@Override
protected TestElement buildTestElement() {
SingleWriteWebSocketSampler write = new SingleWriteWebSocketSampler();
- write.setType(DataPayloadType.Text);
+ setWriteDataType(write, dataType);
write.setRequestData(requestData);
write.setCreateNewConnection(false);
return write;
@@ -451,8 +573,14 @@ protected MethodCall buildMethodCall(SingleWriteWebSocketSampler testElement,
MethodCallContext context) {
TestElementParamBuilder paramBuilder = new TestElementParamBuilder(testElement);
MethodParam requestData = paramBuilder.stringParam("requestData", "");
- return new MethodCall("websocketWrite", DslWriteSampler.class,
+ MethodParam dataTypeParam = paramBuilder.enumParam("payloadType", PayloadDataType.TEXT);
+ MethodCall ret = new MethodCall("websocketWrite", DslWriteSampler.class,
new StringParam(requestData.getExpression()));
+ if (!dataTypeParam.isDefault()) {
+ ret = new MethodCall("websocketWrite", DslWriteSampler.class,
+ new StringParam(requestData.getExpression()), dataTypeParam);
+ }
+ return ret;
}
}
}
@@ -460,9 +588,37 @@ protected MethodCall buildMethodCall(SingleWriteWebSocketSampler testElement,
public static class DslReadSampler extends BaseSampler {
private String responseTimeoutMillis;
private boolean waitForResponse = true;
+ private String dataType;
private DslReadSampler() {
+ this(PayloadDataType.TEXT);
+ }
+
+ private DslReadSampler(PayloadDataType dataType) {
+ this(dataType.propertyValue());
+ }
+
+ private DslReadSampler(String dataType) {
super("WebSocket Single Read", SingleReadWebSocketSamplerGui.class);
+ this.dataType = dataType;
+ validateDataType(dataType);
+ }
+
+ private static void validateDataType(String dataType) {
+ if (dataType != null && !containsJmeterExpression(dataType)
+ && !PayloadDataType.isValidDataType(dataType)) {
+ throw new IllegalArgumentException("Invalid data type. Must be 'text' or 'binary'");
+ }
+ }
+
+ private static void setReadDataType(SingleReadWebSocketSampler read, String dataType) {
+ if (containsJmeterExpression(dataType)) {
+ read.setProperty("dataType", dataType);
+ } else if (PayloadDataType.isText(dataType)) {
+ read.setDataType(DataType.Text);
+ } else {
+ read.setDataType(DataType.Binary);
+ }
}
@Override
@@ -471,7 +627,7 @@ protected TestElement buildTestElement() {
if (responseTimeoutMillis != null) {
read.setReadTimeout(responseTimeoutMillis);
}
- read.setDataType(DataType.Text);
+ setReadDataType(read, dataType);
read.setOptional(!waitForResponse);
read.setCreateNewConnection(false);
return read;
@@ -535,7 +691,12 @@ protected MethodCall buildMethodCall(SingleReadWebSocketSampler testElement,
TestElementParamBuilder paramBuilder = new TestElementParamBuilder(testElement);
boolean optionalParam = !paramBuilder.boolParam("optional", false)
.getExpression().equals("true");
- return new MethodCall("websocketRead", DslReadSampler.class)
+ MethodParam dataTypeParam = paramBuilder.enumParam("dataType", PayloadDataType.TEXT);
+ MethodCall ret = new MethodCall("websocketRead", DslReadSampler.class);
+ if (!dataTypeParam.isDefault()) {
+ ret = new MethodCall("websocketRead", DslReadSampler.class, dataTypeParam);
+ }
+ return ret
.chain("responseTimeout", paramBuilder.intParam("readTimeout", 6000))
.chain("waitForResponse", new BoolParam(optionalParam, true))
.chain("createNewConnection", paramBuilder.boolParam("createNewConnection", false));
diff --git a/jmeter-java-dsl-websocket/src/test/java/DslWebsocketSamplerTest.java b/jmeter-java-dsl-websocket/src/test/java/DslWebsocketSamplerTest.java
index a1122587..9030d66a 100644
--- a/jmeter-java-dsl-websocket/src/test/java/DslWebsocketSamplerTest.java
+++ b/jmeter-java-dsl-websocket/src/test/java/DslWebsocketSamplerTest.java
@@ -1,11 +1,14 @@
+import java.net.InetSocketAddress;
+import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
import static org.assertj.core.api.Assertions.assertThat;
import static org.junit.jupiter.api.Assertions.assertThrows;
-import static us.abstracta.jmeter.javadsl.JmeterDsl.responseAssertion;
-import static us.abstracta.jmeter.javadsl.JmeterDsl.testPlan;
-import static us.abstracta.jmeter.javadsl.JmeterDsl.threadGroup;
+import static us.abstracta.jmeter.javadsl.JmeterDsl.*;
import static us.abstracta.jmeter.javadsl.websocket.WebsocketJMeterDsl.*;
+import org.java_websocket.WebSocket;
+import org.java_websocket.handshake.ClientHandshake;
+import org.java_websocket.server.WebSocketServer;
import org.junit.jupiter.api.Test;
import us.abstracta.jmeter.javadsl.core.TestPlanStats;
@@ -19,13 +22,15 @@ public void shouldConnectAndEchoMessageWhenWebSocketTestPlanWithEchoServer() thr
String wsUri = echoServer.getUri();
TestPlanStats stats = testPlan(
threadGroup(1, 1,
- websocketConnect(wsUri),
+ vars().set("stream_key", "1234567890"),
+ websocketConnect(wsUri + "/test?stream_key=${stream_key}"),
websocketWrite("Hello WebSocket Test!"),
websocketRead()
.children(
responseAssertion()
.containsSubstrings("Hello WebSocket Test!")),
- websocketDisconnect()))
+ websocketDisconnect()
+ ))
.run();
assertThat(stats.overall().errorsCount()).isEqualTo(0);
}
@@ -83,4 +88,46 @@ public void shouldErrorSamplerWhenDisconnectOperationWhenNoPreviousConnection()
.run();
assertThat(stats.overall().errorsCount()).isEqualTo(1);
}
+
+ private static class WebSocketEchoServer extends WebSocketServer {
+
+ private final CountDownLatch startLatch = new CountDownLatch(1);
+
+ WebSocketEchoServer(int port) {
+ super(new InetSocketAddress(port));
+ }
+
+ @Override
+ public void onOpen(WebSocket conn, ClientHandshake handshake) {
+ }
+
+ @Override
+ public void onClose(WebSocket conn, int code, String reason, boolean remote) {
+ }
+
+ @Override
+ public void onMessage(WebSocket conn, String message) {
+ conn.send(message);
+ }
+
+ @Override
+ public void onError(WebSocket conn, Exception ex) {
+ ex.printStackTrace();
+ }
+
+ @Override
+ public void onStart() {
+ startLatch.countDown();
+ }
+
+ void awaitStart(long timeout, TimeUnit unit) throws InterruptedException {
+ if (!startLatch.await(timeout, unit)) {
+ throw new RuntimeException("WebSocket server failed to start within timeout");
+ }
+ }
+
+ String getUri() {
+ return "ws://localhost:" + getPort();
+ }
+ }
}
\ No newline at end of file