diff --git a/instrumentation/http-url-connection/javaagent/src/test/groovy/HttpUrlConnectionResponseCodeOnlyTest.groovy b/instrumentation/http-url-connection/javaagent/src/test/groovy/HttpUrlConnectionResponseCodeOnlyTest.groovy
deleted file mode 100644
index 3f2ecc60df46..000000000000
--- a/instrumentation/http-url-connection/javaagent/src/test/groovy/HttpUrlConnectionResponseCodeOnlyTest.groovy
+++ /dev/null
@@ -1,57 +0,0 @@
-/*
- * Copyright The OpenTelemetry Authors
- * SPDX-License-Identifier: Apache-2.0
- */
-
-import io.opentelemetry.instrumentation.test.AgentTestTrait
-import io.opentelemetry.instrumentation.test.base.HttpClientTest
-
-class HttpUrlConnectionResponseCodeOnlyTest extends HttpClientTest<HttpURLConnection> implements AgentTestTrait {
-
-  @Override
-  HttpURLConnection buildRequest(String method, URI uri, Map<String, String> headers) {
-    return uri.toURL().openConnection() as HttpURLConnection
-  }
-
-  @Override
-  int sendRequest(HttpURLConnection connection, String method, URI uri, Map<String, String> headers) {
-    try {
-      connection.setRequestMethod(method)
-      connection.connectTimeout = CONNECT_TIMEOUT_MS
-      if (uri.toString().contains("/read-timeout")) {
-        connection.readTimeout = READ_TIMEOUT_MS
-      }
-      headers.each { connection.setRequestProperty(it.key, it.value) }
-      connection.setRequestProperty("Connection", "close")
-      return connection.getResponseCode()
-    } finally {
-      connection.disconnect()
-    }
-  }
-
-  @Override
-  int maxRedirects() {
-    20
-  }
-
-  @Override
-  Integer responseCodeOnRedirectError() {
-    return 302
-  }
-
-  @Override
-  boolean testReusedRequest() {
-    // HttpURLConnection can't be reused
-    return false
-  }
-
-  @Override
-  boolean testCallback() {
-    return false
-  }
-
-  @Override
-  boolean testReadTimeout() {
-    true
-  }
-}
diff --git a/instrumentation/http-url-connection/javaagent/src/test/groovy/HttpUrlConnectionTest.groovy b/instrumentation/http-url-connection/javaagent/src/test/groovy/HttpUrlConnectionTest.groovy
deleted file mode 100644
index 0b518a08d61c..000000000000
--- a/instrumentation/http-url-connection/javaagent/src/test/groovy/HttpUrlConnectionTest.groovy
+++ /dev/null
@@ -1,333 +0,0 @@
-/*
- * Copyright The OpenTelemetry Authors
- * SPDX-License-Identifier: Apache-2.0
- */
-
-import io.opentelemetry.api.trace.Span
-import io.opentelemetry.instrumentation.test.AgentTestTrait
-import io.opentelemetry.instrumentation.test.base.HttpClientTest
-import io.opentelemetry.semconv.trace.attributes.SemanticAttributes
-import spock.lang.Unroll
-
-import static io.opentelemetry.api.trace.SpanKind.CLIENT
-import static io.opentelemetry.api.trace.SpanKind.SERVER
-import static io.opentelemetry.semconv.trace.attributes.SemanticAttributes.NetTransportValues.IP_TCP
-
-class HttpUrlConnectionTest extends HttpClientTest<HttpURLConnection> implements AgentTestTrait {
-
-  static final RESPONSE = "Hello."
-  static final STATUS = 200
-
-  @Override
-  HttpURLConnection buildRequest(String method, URI uri, Map<String, String> headers) {
-    return uri.toURL().openConnection() as HttpURLConnection
-  }
-
-  @Override
-  int sendRequest(HttpURLConnection connection, String method, URI uri, Map<String, String> headers) {
-    if (uri.toString().contains("/read-timeout")) {
-      connection.readTimeout = READ_TIMEOUT_MS
-    }
-    try {
-      connection.setRequestMethod(method)
-      headers.each { connection.setRequestProperty(it.key, it.value) }
-      connection.setRequestProperty("Connection", "close")
-      connection.useCaches = true
-      connection.connectTimeout = CONNECT_TIMEOUT_MS
-      def parentSpan = Span.current()
-      def stream = connection.inputStream
-      assert Span.current() == parentSpan
-      stream.readLines()
-      stream.close()
-      return connection.getResponseCode()
-    } finally {
-      connection.disconnect()
-    }
-  }
-
-  @Override
-  int maxRedirects() {
-    20
-  }
-
-  @Override
-  Integer responseCodeOnRedirectError() {
-    return 302
-  }
-
-  @Override
-  boolean testReusedRequest() {
-    // HttpURLConnection can't be reused
-    return false
-  }
-
-  @Override
-  boolean testCallback() {
-    return false
-  }
-
-  @Override
-  boolean testReadTimeout() {
-    true
-  }
-
-  @Unroll
-  def "trace request (useCaches: #useCaches)"() {
-    setup:
-    def url = resolveAddress("/success").toURL()
-    runWithSpan("someTrace") {
-      HttpURLConnection connection = url.openConnection()
-      connection.useCaches = useCaches
-      assert Span.current().getSpanContext().isValid()
-      def stream = connection.inputStream
-      def lines = stream.readLines()
-      stream.close()
-      assert connection.getResponseCode() == STATUS
-      assert lines == [RESPONSE]
-
-      // call again to ensure the cycling is ok
-      connection = url.openConnection()
-      connection.useCaches = useCaches
-      assert Span.current().getSpanContext().isValid()
-      // call before input stream to test alternate behavior
-      assert connection.getResponseCode() == STATUS
-      connection.inputStream
-      stream = connection.inputStream // one more to ensure state is working
-      lines = stream.readLines()
-      stream.close()
-      assert lines == [RESPONSE]
-    }
-
-    expect:
-    assertTraces(1) {
-      trace(0, 5) {
-        span(0) {
-          name "someTrace"
-          hasNoParent()
-          attributes {
-          }
-        }
-        span(1) {
-          name "HTTP GET"
-          kind CLIENT
-          childOf span(0)
-          attributes {
-            "$SemanticAttributes.NET_TRANSPORT" IP_TCP
-            "$SemanticAttributes.NET_PEER_NAME" "localhost"
-            "$SemanticAttributes.NET_PEER_PORT" server.httpPort()
-            "$SemanticAttributes.HTTP_URL" "$url"
-            "$SemanticAttributes.HTTP_METHOD" "GET"
-            "$SemanticAttributes.HTTP_STATUS_CODE" STATUS
-            "$SemanticAttributes.HTTP_FLAVOR" "1.1"
-            "$SemanticAttributes.HTTP_RESPONSE_CONTENT_LENGTH" Long
-          }
-        }
-        span(2) {
-          name "test-http-server"
-          kind SERVER
-          childOf span(1)
-          attributes {
-          }
-        }
-        span(3) {
-          name "HTTP GET"
-          kind CLIENT
-          childOf span(0)
-          attributes {
-            "$SemanticAttributes.NET_TRANSPORT" IP_TCP
-            "$SemanticAttributes.NET_PEER_NAME" "localhost"
-            "$SemanticAttributes.NET_PEER_PORT" server.httpPort()
-            "$SemanticAttributes.HTTP_URL" "$url"
-            "$SemanticAttributes.HTTP_METHOD" "GET"
-            "$SemanticAttributes.HTTP_STATUS_CODE" STATUS
-            "$SemanticAttributes.HTTP_FLAVOR" "1.1"
-            "$SemanticAttributes.HTTP_RESPONSE_CONTENT_LENGTH" Long
-          }
-        }
-        span(4) {
-          name "test-http-server"
-          kind SERVER
-          childOf span(3)
-          attributes {
-          }
-        }
-      }
-    }
-
-    where:
-    useCaches << [false, true]
-  }
-
-  def "test broken API usage (#iteration)"() {
-    setup:
-    def url = resolveAddress("/success").toURL()
-    HttpURLConnection connection = runWithSpan("someTrace") {
-      HttpURLConnection con = url.openConnection()
-      con.setRequestProperty("Connection", "close")
-      assert Span.current().getSpanContext().isValid()
-      assert con.getResponseCode() == STATUS
-      return con
-    }
-
-    expect:
-    assertTraces(1) {
-      trace(0, 3) {
-        span(0) {
-          name "someTrace"
-          hasNoParent()
-          attributes {
-          }
-        }
-        span(1) {
-          name "HTTP GET"
-          kind CLIENT
-          childOf span(0)
-          attributes {
-            "$SemanticAttributes.NET_PEER_NAME" "localhost"
-            "$SemanticAttributes.NET_PEER_PORT" server.httpPort()
-            "$SemanticAttributes.NET_TRANSPORT" IP_TCP
-            "$SemanticAttributes.HTTP_URL" "$url"
-            "$SemanticAttributes.HTTP_METHOD" "GET"
-            "$SemanticAttributes.HTTP_STATUS_CODE" STATUS
-            "$SemanticAttributes.HTTP_FLAVOR" "1.1"
-            "$SemanticAttributes.HTTP_RESPONSE_CONTENT_LENGTH" Long
-          }
-        }
-        serverSpan(it, 2, span(1))
-      }
-    }
-
-    cleanup:
-    connection.disconnect()
-
-    where:
-    iteration << (1..10)
-  }
-
-  def "test post request"() {
-    setup:
-    def url = resolveAddress("/success").toURL()
-    runWithSpan("someTrace") {
-      HttpURLConnection connection = url.openConnection()
-      connection.setRequestMethod("POST")
-
-      String urlParameters = "q=ASDF&w=&e=&r=12345&t="
-
-      // Send post request
-      connection.setDoOutput(true)
-      DataOutputStream wr = new DataOutputStream(connection.getOutputStream())
-      wr.writeBytes(urlParameters)
-      wr.flush()
-      wr.close()
-
-      assert connection.getResponseCode() == STATUS
-
-      def stream = connection.inputStream
-      def lines = stream.readLines()
-      stream.close()
-      assert lines == [RESPONSE]
-    }
-
-    expect:
-    assertTraces(1) {
-      trace(0, 3) {
-        span(0) {
-          name "someTrace"
-          hasNoParent()
-          attributes {
-          }
-        }
-        span(1) {
-          name "HTTP POST"
-          kind CLIENT
-          childOf span(0)
-          attributes {
-            "$SemanticAttributes.NET_TRANSPORT" IP_TCP
-            "$SemanticAttributes.NET_PEER_NAME" "localhost"
-            "$SemanticAttributes.NET_PEER_PORT" server.httpPort()
-            "$SemanticAttributes.HTTP_URL" "$url"
-            "$SemanticAttributes.HTTP_METHOD" "POST"
-            "$SemanticAttributes.HTTP_STATUS_CODE" STATUS
-            "$SemanticAttributes.HTTP_FLAVOR" "1.1"
-            "$SemanticAttributes.HTTP_REQUEST_CONTENT_LENGTH" Long
-            "$SemanticAttributes.HTTP_RESPONSE_CONTENT_LENGTH" Long
-          }
-        }
-        span(2) {
-          name "test-http-server"
-          kind SERVER
-          childOf span(1)
-          attributes {
-          }
-        }
-      }
-    }
-  }
-
-  def "sun.net.www.protocol.http.HttpURLConnection.getOutputStream should transform GET into POST"() {
-    setup:
-    def url = resolveAddress("/success").toURL()
-    runWithSpan("someTrace") {
-
-      HttpURLConnection connection = url.openConnection()
-
-      def connectionClassName = connection.getClass().getName()
-
-      assert "sun.net.www.protocol.http.HttpURLConnection".equals(connectionClassName)
-
-      connection.setRequestMethod("GET")
-
-      String urlParameters = "q=ASDF&w=&e=&r=12345&t="
-
-      // Send POST request
-      connection.setDoOutput(true)
-      DataOutputStream wr = new DataOutputStream(connection.getOutputStream())
-      wr.writeBytes(urlParameters)
-      wr.flush()
-      wr.close()
-
-      assert connection.getResponseCode() == STATUS
-
-      def stream = connection.inputStream
-      def lines = stream.readLines()
-      stream.close()
-      assert lines == [RESPONSE]
-    }
-
-    expect:
-    assertTraces(1) {
-      trace(0, 3) {
-        span(0) {
-          name "someTrace"
-          hasNoParent()
-          attributes {
-          }
-        }
-        span(1) {
-          name "HTTP POST"
-          kind CLIENT
-          childOf span(0)
-          attributes {
-            "$SemanticAttributes.NET_TRANSPORT" IP_TCP
-            "$SemanticAttributes.NET_PEER_NAME" "localhost"
-            "$SemanticAttributes.NET_PEER_PORT" server.httpPort()
-            "$SemanticAttributes.HTTP_URL" "$url"
-            "$SemanticAttributes.HTTP_METHOD" "POST"
-            "$SemanticAttributes.HTTP_STATUS_CODE" STATUS
-            "$SemanticAttributes.HTTP_FLAVOR" "1.1"
-            "$SemanticAttributes.HTTP_REQUEST_CONTENT_LENGTH" Long
-            "$SemanticAttributes.HTTP_RESPONSE_CONTENT_LENGTH" Long
-          }
-        }
-        span(2) {
-          name "test-http-server"
-          kind SERVER
-          childOf span(1)
-          attributes {
-          }
-        }
-      }
-    }
-  }
-
-}
diff --git a/instrumentation/http-url-connection/javaagent/src/test/groovy/HttpUrlConnectionUseCachesFalseTest.groovy b/instrumentation/http-url-connection/javaagent/src/test/groovy/HttpUrlConnectionUseCachesFalseTest.groovy
deleted file mode 100644
index 71fea8f80bbc..000000000000
--- a/instrumentation/http-url-connection/javaagent/src/test/groovy/HttpUrlConnectionUseCachesFalseTest.groovy
+++ /dev/null
@@ -1,64 +0,0 @@
-/*
- * Copyright The OpenTelemetry Authors
- * SPDX-License-Identifier: Apache-2.0
- */
-
-import io.opentelemetry.api.trace.Span
-import io.opentelemetry.instrumentation.test.AgentTestTrait
-import io.opentelemetry.instrumentation.test.base.HttpClientTest
-
-class HttpUrlConnectionUseCachesFalseTest extends HttpClientTest<HttpURLConnection> implements AgentTestTrait {
-
-  @Override
-  HttpURLConnection buildRequest(String method, URI uri, Map<String, String> headers) {
-    return uri.toURL().openConnection() as HttpURLConnection
-  }
-
-  @Override
-  int sendRequest(HttpURLConnection connection, String method, URI uri, Map<String, String> headers) {
-    try {
-      connection.setRequestMethod(method)
-      headers.each { connection.setRequestProperty(it.key, it.value) }
-      connection.setRequestProperty("Connection", "close")
-      connection.useCaches = false
-      connection.connectTimeout = CONNECT_TIMEOUT_MS
-      if (uri.toString().contains("/read-timeout")) {
-        connection.readTimeout = READ_TIMEOUT_MS
-      }
-      def parentSpan = Span.current()
-      def stream = connection.inputStream
-      assert Span.current() == parentSpan
-      stream.readLines()
-      stream.close()
-      return connection.getResponseCode()
-    } finally {
-      connection.disconnect()
-    }
-  }
-
-  @Override
-  int maxRedirects() {
-    20
-  }
-
-  @Override
-  Integer responseCodeOnRedirectError() {
-    return 302
-  }
-
-  @Override
-  boolean testReusedRequest() {
-    // HttpURLConnection can't be reused
-    return false
-  }
-
-  @Override
-  boolean testCallback() {
-    return false
-  }
-
-  @Override
-  boolean testReadTimeout() {
-    true
-  }
-}
diff --git a/instrumentation/http-url-connection/javaagent/src/test/groovy/UrlConnectionTest.groovy b/instrumentation/http-url-connection/javaagent/src/test/groovy/UrlConnectionTest.groovy
deleted file mode 100644
index f2c7bcda7524..000000000000
--- a/instrumentation/http-url-connection/javaagent/src/test/groovy/UrlConnectionTest.groovy
+++ /dev/null
@@ -1,62 +0,0 @@
-/*
- * Copyright The OpenTelemetry Authors
- * SPDX-License-Identifier: Apache-2.0
- */
-
-import io.opentelemetry.api.trace.Span
-import io.opentelemetry.instrumentation.test.AgentInstrumentationSpecification
-import io.opentelemetry.semconv.trace.attributes.SemanticAttributes
-
-import static io.opentelemetry.api.trace.SpanKind.CLIENT
-import static io.opentelemetry.api.trace.StatusCode.ERROR
-import static io.opentelemetry.instrumentation.test.utils.PortUtils.UNUSABLE_PORT
-import static io.opentelemetry.semconv.trace.attributes.SemanticAttributes.NetTransportValues.IP_TCP
-
-class UrlConnectionTest extends AgentInstrumentationSpecification {
-
-  def "trace request with connection failure #scheme"() {
-    when:
-    runWithSpan("someTrace") {
-      URLConnection connection = url.openConnection()
-      connection.setConnectTimeout(10000)
-      connection.setReadTimeout(10000)
-      assert Span.current() != null
-      connection.inputStream
-    }
-
-    then:
-    thrown ConnectException
-
-    expect:
-    assertTraces(1) {
-      trace(0, 2) {
-        span(0) {
-          name "someTrace"
-          hasNoParent()
-          status ERROR
-          errorEvent ConnectException, String
-        }
-        span(1) {
-          name "HTTP GET"
-          kind CLIENT
-          childOf span(0)
-          status ERROR
-          errorEvent ConnectException, String
-          attributes {
-            "$SemanticAttributes.NET_TRANSPORT" IP_TCP
-            "$SemanticAttributes.NET_PEER_NAME" "localhost"
-            "$SemanticAttributes.NET_PEER_PORT" UNUSABLE_PORT
-            "$SemanticAttributes.HTTP_URL" "$url"
-            "$SemanticAttributes.HTTP_METHOD" "GET"
-            "$SemanticAttributes.HTTP_FLAVOR" "1.1"
-          }
-        }
-      }
-    }
-
-    where:
-    scheme << ["http", "https"]
-
-    url = new URI("$scheme://localhost:$UNUSABLE_PORT").toURL()
-  }
-}
diff --git a/instrumentation/http-url-connection/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/httpurlconnection/HttpUrlConnectionResponseCodeOnlyTest.java b/instrumentation/http-url-connection/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/httpurlconnection/HttpUrlConnectionResponseCodeOnlyTest.java
new file mode 100644
index 000000000000..13e6ef3e5842
--- /dev/null
+++ b/instrumentation/http-url-connection/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/httpurlconnection/HttpUrlConnectionResponseCodeOnlyTest.java
@@ -0,0 +1,70 @@
+/*
+ * Copyright The OpenTelemetry Authors
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+package io.opentelemetry.javaagent.instrumentation.httpurlconnection;
+
+import io.opentelemetry.instrumentation.testing.junit.InstrumentationExtension;
+import io.opentelemetry.instrumentation.testing.junit.http.AbstractHttpClientTest;
+import io.opentelemetry.instrumentation.testing.junit.http.HttpClientInstrumentationExtension;
+import java.net.HttpURLConnection;
+import java.net.URI;
+import java.util.Map;
+import org.junit.jupiter.api.extension.RegisterExtension;
+
+class HttpUrlConnectionResponseCodeOnlyTest extends AbstractHttpClientTest<HttpURLConnection> {
+  @RegisterExtension
+  static final InstrumentationExtension testing = HttpClientInstrumentationExtension.forAgent();
+
+  @Override
+  public HttpURLConnection buildRequest(String method, URI uri, Map<String, String> headers)
+      throws Exception {
+    return (HttpURLConnection) uri.toURL().openConnection();
+  }
+
+  @Override
+  public int sendRequest(
+      HttpURLConnection connection, String method, URI uri, Map<String, String> headers)
+      throws Exception {
+    try {
+      connection.setRequestMethod(method);
+      connection.setConnectTimeout((int) CONNECTION_TIMEOUT.toMillis());
+      if (uri.toString().contains("/read-timeout")) {
+        connection.setReadTimeout((int) READ_TIMEOUT.toMillis());
+      }
+
+      headers.forEach(connection::setRequestProperty);
+      connection.setRequestProperty("Connection", "close");
+      return connection.getResponseCode();
+    } finally {
+      connection.disconnect();
+    }
+  }
+
+  @Override
+  public int maxRedirects() {
+    return 20;
+  }
+
+  @Override
+  public Integer responseCodeOnRedirectError() {
+    return 302;
+  }
+
+  @Override
+  public boolean testReusedRequest() {
+    // HttpURLConnection can't be reused
+    return false;
+  }
+
+  @Override
+  public boolean testCallback() {
+    return false;
+  }
+
+  @Override
+  public boolean testReadTimeout() {
+    return true;
+  }
+}
diff --git a/instrumentation/http-url-connection/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/httpurlconnection/HttpUrlConnectionTest.java b/instrumentation/http-url-connection/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/httpurlconnection/HttpUrlConnectionTest.java
new file mode 100644
index 000000000000..bacbda1dabdb
--- /dev/null
+++ b/instrumentation/http-url-connection/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/httpurlconnection/HttpUrlConnectionTest.java
@@ -0,0 +1,316 @@
+/*
+ * Copyright The OpenTelemetry Authors
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+package io.opentelemetry.javaagent.instrumentation.httpurlconnection;
+
+import static io.opentelemetry.api.trace.SpanKind.CLIENT;
+import static io.opentelemetry.api.trace.SpanKind.SERVER;
+import static io.opentelemetry.javaagent.instrumentation.httpurlconnection.StreamUtils.readLines;
+import static io.opentelemetry.sdk.testing.assertj.OpenTelemetryAssertions.equalTo;
+import static io.opentelemetry.sdk.testing.assertj.OpenTelemetryAssertions.satisfies;
+import static io.opentelemetry.semconv.trace.attributes.SemanticAttributes.NetTransportValues.IP_TCP;
+import static org.assertj.core.api.Assertions.assertThat;
+
+import io.opentelemetry.api.trace.Span;
+import io.opentelemetry.instrumentation.testing.junit.InstrumentationExtension;
+import io.opentelemetry.instrumentation.testing.junit.http.AbstractHttpClientTest;
+import io.opentelemetry.instrumentation.testing.junit.http.HttpClientInstrumentationExtension;
+import io.opentelemetry.semconv.trace.attributes.SemanticAttributes;
+import java.io.DataOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.net.HttpURLConnection;
+import java.net.URI;
+import java.net.URL;
+import java.util.Collections;
+import java.util.List;
+import java.util.Map;
+import org.assertj.core.api.AbstractLongAssert;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.RegisterExtension;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.ValueSource;
+
+class HttpUrlConnectionTest extends AbstractHttpClientTest<HttpURLConnection> {
+
+  @RegisterExtension
+  static final InstrumentationExtension testing = HttpClientInstrumentationExtension.forAgent();
+
+  static final List<String> RESPONSE = Collections.singletonList("Hello.");
+  static final int STATUS = 200;
+
+  @Override
+  public HttpURLConnection buildRequest(String method, URI uri, Map<String, String> headers)
+      throws Exception {
+    return (HttpURLConnection) uri.toURL().openConnection();
+  }
+
+  @Override
+  public int sendRequest(
+      HttpURLConnection connection, String method, URI uri, Map<String, String> headers)
+      throws Exception {
+    if (uri.toString().contains("/read-timeout")) {
+      connection.setReadTimeout((int) READ_TIMEOUT.toMillis());
+    }
+    try {
+      connection.setRequestMethod(method);
+      headers.forEach(connection::setRequestProperty);
+      connection.setRequestProperty("Connection", "close");
+      connection.setUseCaches(true);
+      connection.setConnectTimeout((int) CONNECTION_TIMEOUT.toMillis());
+      Span parentSpan = Span.current();
+      InputStream stream = connection.getInputStream();
+      assertThat(Span.current()).isEqualTo(parentSpan);
+      stream.close();
+      return connection.getResponseCode();
+    } finally {
+      connection.disconnect();
+    }
+  }
+
+  @Override
+  public int maxRedirects() {
+    return 20;
+  }
+
+  @Override
+  public Integer responseCodeOnRedirectError() {
+    return 302;
+  }
+
+  @Override
+  public boolean testReusedRequest() {
+    // HttpURLConnection can't be reused
+    return false;
+  }
+
+  @Override
+  public boolean testCallback() {
+    return false;
+  }
+
+  @Override
+  public boolean testReadTimeout() {
+    return true;
+  }
+
+  @ParameterizedTest
+  @ValueSource(booleans = {false, true})
+  public void traceRequest(boolean useCache) throws IOException {
+    URL url = resolveAddress("/success").toURL();
+
+    testing.runWithSpan(
+        "someTrace",
+        () -> {
+          HttpURLConnection connection = (HttpURLConnection) url.openConnection();
+          connection.setUseCaches(useCache);
+          assertThat(Span.current().getSpanContext().isValid()).isTrue();
+          InputStream stream = connection.getInputStream();
+          List<String> lines = readLines(stream);
+          stream.close();
+          assertThat(connection.getResponseCode()).isEqualTo(STATUS);
+          assertThat(lines).isEqualTo(RESPONSE);
+
+          // call again to ensure the cycling is ok
+          connection = (HttpURLConnection) url.openConnection();
+          connection.setUseCaches(useCache);
+          assertThat(Span.current().getSpanContext().isValid()).isTrue();
+          // call before input stream to test alternate behavior
+          assertThat(connection.getResponseCode()).isEqualTo(STATUS);
+          connection.getInputStream();
+          stream = connection.getInputStream(); // one more to ensure state is working
+          lines = readLines(stream);
+          stream.close();
+          assertThat(lines).isEqualTo(RESPONSE);
+        });
+
+    testing.waitAndAssertTraces(
+        trace ->
+            trace.hasSpansSatisfyingExactly(
+                span -> span.hasName("someTrace").hasNoParent(),
+                span ->
+                    span.hasName("HTTP GET")
+                        .hasKind(CLIENT)
+                        .hasParent(trace.getSpan(0))
+                        .hasAttributesSatisfyingExactly(
+                            equalTo(SemanticAttributes.NET_TRANSPORT, IP_TCP),
+                            equalTo(SemanticAttributes.NET_PEER_NAME, "localhost"),
+                            equalTo(SemanticAttributes.NET_PEER_PORT, url.getPort()),
+                            equalTo(SemanticAttributes.HTTP_URL, url.toString()),
+                            equalTo(SemanticAttributes.HTTP_METHOD, "GET"),
+                            equalTo(SemanticAttributes.HTTP_STATUS_CODE, STATUS),
+                            equalTo(SemanticAttributes.HTTP_FLAVOR, "1.1"),
+                            satisfies(
+                                SemanticAttributes.HTTP_RESPONSE_CONTENT_LENGTH,
+                                AbstractLongAssert::isNotNegative)),
+                span ->
+                    span.hasName("test-http-server").hasKind(SERVER).hasParent(trace.getSpan(1)),
+                span ->
+                    span.hasName("HTTP GET")
+                        .hasKind(CLIENT)
+                        .hasParent(trace.getSpan(0))
+                        .hasAttributesSatisfyingExactly(
+                            equalTo(SemanticAttributes.NET_TRANSPORT, IP_TCP),
+                            equalTo(SemanticAttributes.NET_PEER_NAME, "localhost"),
+                            equalTo(SemanticAttributes.NET_PEER_PORT, url.getPort()),
+                            equalTo(SemanticAttributes.HTTP_URL, url.toString()),
+                            equalTo(SemanticAttributes.HTTP_METHOD, "GET"),
+                            equalTo(SemanticAttributes.HTTP_STATUS_CODE, STATUS),
+                            equalTo(SemanticAttributes.HTTP_FLAVOR, "1.1"),
+                            satisfies(
+                                SemanticAttributes.HTTP_RESPONSE_CONTENT_LENGTH,
+                                AbstractLongAssert::isNotNegative)),
+                span ->
+                    span.hasName("test-http-server").hasKind(SERVER).hasParent(trace.getSpan(3))));
+  }
+
+  @ParameterizedTest
+  @ValueSource(ints = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10})
+  public void testBrokenApiUsage() throws IOException {
+    URL url = resolveAddress("/success").toURL();
+    HttpURLConnection connection =
+        testing.runWithSpan(
+            "someTrace",
+            () -> {
+              HttpURLConnection con = (HttpURLConnection) url.openConnection();
+              con.setRequestProperty("Connection", "close");
+              assertThat(Span.current().getSpanContext().isValid()).isTrue();
+              assertThat(con.getResponseCode()).isEqualTo(STATUS);
+              return con;
+            });
+
+    testing.waitAndAssertTraces(
+        trace ->
+            trace.hasSpansSatisfyingExactly(
+                span -> span.hasName("someTrace").hasNoParent(),
+                span ->
+                    span.hasName("HTTP GET")
+                        .hasKind(CLIENT)
+                        .hasParent(trace.getSpan(0))
+                        .hasAttributesSatisfyingExactly(
+                            equalTo(SemanticAttributes.NET_TRANSPORT, IP_TCP),
+                            equalTo(SemanticAttributes.NET_PEER_NAME, "localhost"),
+                            equalTo(SemanticAttributes.NET_PEER_PORT, url.getPort()),
+                            equalTo(SemanticAttributes.HTTP_URL, url.toString()),
+                            equalTo(SemanticAttributes.HTTP_METHOD, "GET"),
+                            equalTo(SemanticAttributes.HTTP_STATUS_CODE, STATUS),
+                            equalTo(SemanticAttributes.HTTP_FLAVOR, "1.1"),
+                            satisfies(
+                                SemanticAttributes.HTTP_RESPONSE_CONTENT_LENGTH,
+                                AbstractLongAssert::isNotNegative)),
+                span ->
+                    span.hasName("test-http-server").hasKind(SERVER).hasParent(trace.getSpan(1))));
+
+    connection.disconnect();
+  }
+
+  @Test
+  public void testPostRequest() throws IOException {
+    URL url = resolveAddress("/success").toURL();
+    testing.runWithSpan(
+        "someTrace",
+        () -> {
+          HttpURLConnection connection = (HttpURLConnection) url.openConnection();
+          connection.setRequestMethod("POST");
+
+          String urlParameters = "q=ASDF&w=&e=&r=12345&t=";
+
+          // Send post request
+          connection.setDoOutput(true);
+          DataOutputStream wr = new DataOutputStream(connection.getOutputStream());
+          wr.writeBytes(urlParameters);
+          wr.flush();
+          wr.close();
+
+          assertThat(connection.getResponseCode()).isEqualTo(STATUS);
+
+          InputStream stream = connection.getInputStream();
+          List<String> lines = readLines(stream);
+          stream.close();
+          assertThat(lines).isEqualTo(RESPONSE);
+        });
+
+    testing.waitAndAssertTraces(
+        trace ->
+            trace.hasSpansSatisfyingExactly(
+                span -> span.hasName("someTrace").hasNoParent(),
+                span ->
+                    span.hasName("HTTP POST")
+                        .hasKind(CLIENT)
+                        .hasParent(trace.getSpan(0))
+                        .hasAttributesSatisfyingExactly(
+                            equalTo(SemanticAttributes.NET_TRANSPORT, IP_TCP),
+                            equalTo(SemanticAttributes.NET_PEER_NAME, "localhost"),
+                            equalTo(SemanticAttributes.NET_PEER_PORT, url.getPort()),
+                            equalTo(SemanticAttributes.HTTP_URL, url.toString()),
+                            equalTo(SemanticAttributes.HTTP_METHOD, "POST"),
+                            equalTo(SemanticAttributes.HTTP_STATUS_CODE, STATUS),
+                            equalTo(SemanticAttributes.HTTP_FLAVOR, "1.1"),
+                            satisfies(
+                                SemanticAttributes.HTTP_REQUEST_CONTENT_LENGTH,
+                                AbstractLongAssert::isNotNegative),
+                            satisfies(
+                                SemanticAttributes.HTTP_RESPONSE_CONTENT_LENGTH,
+                                AbstractLongAssert::isNotNegative)),
+                span ->
+                    span.hasName("test-http-server").hasKind(SERVER).hasParent(trace.getSpan(1))));
+  }
+
+  @Test
+  public void getOutputStreamShouldTransformGetIntoPost() throws IOException {
+    URL url = resolveAddress("/success").toURL();
+    testing.runWithSpan(
+        "someTrace",
+        () -> {
+          HttpURLConnection connection = (HttpURLConnection) url.openConnection();
+
+          assertThat(connection.getClass().getName())
+              .isEqualTo("sun.net.www.protocol.http.HttpURLConnection");
+
+          connection.setRequestMethod("GET");
+
+          String urlParameters = "q=ASDF&w=&e=&r=12345&t=";
+
+          // Send POST request
+          connection.setDoOutput(true);
+          DataOutputStream wr = new DataOutputStream(connection.getOutputStream());
+          wr.writeBytes(urlParameters);
+          wr.flush();
+          wr.close();
+
+          assertThat(connection.getResponseCode()).isEqualTo(STATUS);
+
+          InputStream stream = connection.getInputStream();
+          List<String> lines = readLines(stream);
+          stream.close();
+          assertThat(lines).isEqualTo(RESPONSE);
+        });
+
+    testing.waitAndAssertTraces(
+        trace ->
+            trace.hasSpansSatisfyingExactly(
+                span -> span.hasName("someTrace").hasNoParent(),
+                span ->
+                    span.hasName("HTTP POST")
+                        .hasKind(CLIENT)
+                        .hasParent(trace.getSpan(0))
+                        .hasAttributesSatisfyingExactly(
+                            equalTo(SemanticAttributes.NET_TRANSPORT, IP_TCP),
+                            equalTo(SemanticAttributes.NET_PEER_NAME, "localhost"),
+                            equalTo(SemanticAttributes.NET_PEER_PORT, url.getPort()),
+                            equalTo(SemanticAttributes.HTTP_URL, url.toString()),
+                            equalTo(SemanticAttributes.HTTP_METHOD, "POST"),
+                            equalTo(SemanticAttributes.HTTP_STATUS_CODE, STATUS),
+                            equalTo(SemanticAttributes.HTTP_FLAVOR, "1.1"),
+                            satisfies(
+                                SemanticAttributes.HTTP_REQUEST_CONTENT_LENGTH,
+                                AbstractLongAssert::isNotNegative),
+                            satisfies(
+                                SemanticAttributes.HTTP_RESPONSE_CONTENT_LENGTH,
+                                AbstractLongAssert::isNotNegative)),
+                span ->
+                    span.hasName("test-http-server").hasKind(SERVER).hasParent(trace.getSpan(1))));
+  }
+}
diff --git a/instrumentation/http-url-connection/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/httpurlconnection/HttpUrlConnectionUseCachesFalseTest.java b/instrumentation/http-url-connection/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/httpurlconnection/HttpUrlConnectionUseCachesFalseTest.java
new file mode 100644
index 000000000000..c922459eece9
--- /dev/null
+++ b/instrumentation/http-url-connection/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/httpurlconnection/HttpUrlConnectionUseCachesFalseTest.java
@@ -0,0 +1,80 @@
+/*
+ * Copyright The OpenTelemetry Authors
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+package io.opentelemetry.javaagent.instrumentation.httpurlconnection;
+
+import static io.opentelemetry.javaagent.instrumentation.httpurlconnection.StreamUtils.readLines;
+import static org.assertj.core.api.Assertions.assertThat;
+
+import io.opentelemetry.api.trace.Span;
+import io.opentelemetry.instrumentation.testing.junit.InstrumentationExtension;
+import io.opentelemetry.instrumentation.testing.junit.http.AbstractHttpClientTest;
+import io.opentelemetry.instrumentation.testing.junit.http.HttpClientInstrumentationExtension;
+import java.io.InputStream;
+import java.net.HttpURLConnection;
+import java.net.URI;
+import java.util.Map;
+import org.junit.jupiter.api.extension.RegisterExtension;
+
+class HttpUrlConnectionUseCachesFalseTest extends AbstractHttpClientTest<HttpURLConnection> {
+  @RegisterExtension
+  static final InstrumentationExtension testing = HttpClientInstrumentationExtension.forAgent();
+
+  @Override
+  public HttpURLConnection buildRequest(String method, URI uri, Map<String, String> headers)
+      throws Exception {
+    return (HttpURLConnection) uri.toURL().openConnection();
+  }
+
+  @Override
+  public int sendRequest(
+      HttpURLConnection connection, String method, URI uri, Map<String, String> headers)
+      throws Exception {
+    try {
+      connection.setRequestMethod(method);
+      headers.forEach(connection::setRequestProperty);
+      connection.setRequestProperty("Connection", "close");
+      connection.setUseCaches(false);
+      connection.setConnectTimeout((int) CONNECTION_TIMEOUT.toMillis());
+      if (uri.toString().contains("/read-timeout")) {
+        connection.setReadTimeout((int) READ_TIMEOUT.toMillis());
+      }
+      Span parentSpan = Span.current();
+      InputStream stream = connection.getInputStream();
+      assertThat(Span.current()).isEqualTo(parentSpan);
+      readLines(stream);
+      stream.close();
+      return connection.getResponseCode();
+    } finally {
+      connection.disconnect();
+    }
+  }
+
+  @Override
+  public int maxRedirects() {
+    return 20;
+  }
+
+  @Override
+  public Integer responseCodeOnRedirectError() {
+    return 302;
+  }
+
+  @Override
+  public boolean testReusedRequest() {
+    // HttpURLConnection can't be reused
+    return false;
+  }
+
+  @Override
+  public boolean testCallback() {
+    return false;
+  }
+
+  @Override
+  public boolean testReadTimeout() {
+    return true;
+  }
+}
diff --git a/instrumentation/http-url-connection/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/httpurlconnection/StreamUtils.java b/instrumentation/http-url-connection/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/httpurlconnection/StreamUtils.java
new file mode 100644
index 000000000000..1af8d4212936
--- /dev/null
+++ b/instrumentation/http-url-connection/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/httpurlconnection/StreamUtils.java
@@ -0,0 +1,33 @@
+/*
+ * Copyright The OpenTelemetry Authors
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+package io.opentelemetry.javaagent.instrumentation.httpurlconnection;
+
+import static java.nio.charset.StandardCharsets.UTF_8;
+
+import com.google.common.base.Strings;
+import java.io.BufferedReader;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.InputStreamReader;
+import java.util.ArrayList;
+import java.util.List;
+
+final class StreamUtils {
+  static List<String> readLines(InputStream stream) throws IOException {
+    List<String> lines = new ArrayList<>();
+    BufferedReader reader = new BufferedReader(new InputStreamReader(stream, UTF_8));
+    while (reader.ready()) {
+      String line = reader.readLine();
+      if (!Strings.isNullOrEmpty(line)) {
+        lines.add(line);
+      }
+    }
+
+    return lines;
+  }
+
+  private StreamUtils() {}
+}
diff --git a/instrumentation/http-url-connection/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/httpurlconnection/UrlConnectionTest.java b/instrumentation/http-url-connection/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/httpurlconnection/UrlConnectionTest.java
new file mode 100644
index 000000000000..f3e7de236a06
--- /dev/null
+++ b/instrumentation/http-url-connection/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/httpurlconnection/UrlConnectionTest.java
@@ -0,0 +1,74 @@
+/*
+ * Copyright The OpenTelemetry Authors
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+package io.opentelemetry.javaagent.instrumentation.httpurlconnection;
+
+import static io.opentelemetry.api.trace.SpanKind.CLIENT;
+import static io.opentelemetry.api.trace.SpanKind.INTERNAL;
+import static io.opentelemetry.sdk.testing.assertj.OpenTelemetryAssertions.equalTo;
+import static io.opentelemetry.semconv.trace.attributes.SemanticAttributes.NetTransportValues.IP_TCP;
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.catchThrowable;
+
+import io.opentelemetry.api.trace.Span;
+import io.opentelemetry.instrumentation.test.utils.PortUtils;
+import io.opentelemetry.instrumentation.testing.junit.AgentInstrumentationExtension;
+import io.opentelemetry.instrumentation.testing.junit.InstrumentationExtension;
+import io.opentelemetry.sdk.trace.data.StatusData;
+import io.opentelemetry.semconv.trace.attributes.SemanticAttributes;
+import java.net.URI;
+import java.net.URL;
+import java.net.URLConnection;
+import org.junit.jupiter.api.extension.RegisterExtension;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.ValueSource;
+
+class UrlConnectionTest {
+  @RegisterExtension
+  static final InstrumentationExtension testing = AgentInstrumentationExtension.create();
+
+  @ParameterizedTest
+  @ValueSource(strings = {"http", "https"})
+  public void traceRequestWithConnectionFailure(String scheme) {
+    String uri = scheme + "://localhost:" + PortUtils.UNUSABLE_PORT;
+
+    Throwable thrown =
+        catchThrowable(
+            () ->
+                testing.runWithSpan(
+                    "someTrace",
+                    () -> {
+                      URL url = new URI(uri).toURL();
+                      URLConnection connection = url.openConnection();
+                      connection.setConnectTimeout(10000);
+                      connection.setReadTimeout(10000);
+                      assertThat(Span.current().getSpanContext().isValid()).isTrue();
+                      connection.getInputStream();
+                    }));
+
+    testing.waitAndAssertTraces(
+        trace ->
+            trace.hasSpansSatisfyingExactly(
+                span ->
+                    span.hasName("someTrace")
+                        .hasKind(INTERNAL)
+                        .hasNoParent()
+                        .hasStatus(StatusData.error())
+                        .hasException(thrown),
+                span ->
+                    span.hasName("HTTP GET")
+                        .hasKind(CLIENT)
+                        .hasParent(trace.getSpan(0))
+                        .hasStatus(StatusData.error())
+                        .hasException(thrown)
+                        .hasAttributesSatisfyingExactly(
+                            equalTo(SemanticAttributes.NET_TRANSPORT, IP_TCP),
+                            equalTo(SemanticAttributes.NET_PEER_NAME, "localhost"),
+                            equalTo(SemanticAttributes.NET_PEER_PORT, PortUtils.UNUSABLE_PORT),
+                            equalTo(SemanticAttributes.HTTP_URL, uri),
+                            equalTo(SemanticAttributes.HTTP_METHOD, "GET"),
+                            equalTo(SemanticAttributes.HTTP_FLAVOR, "1.1"))));
+  }
+}