diff --git a/instrumentation/servlet/servlet-3.0/javaagent-unit-tests/build.gradle.kts b/instrumentation/servlet/servlet-3.0/javaagent-unit-tests/build.gradle.kts
new file mode 100644
index 000000000000..c6f646595e21
--- /dev/null
+++ b/instrumentation/servlet/servlet-3.0/javaagent-unit-tests/build.gradle.kts
@@ -0,0 +1,8 @@
+plugins {
+ id("otel.java-conventions")
+}
+
+dependencies {
+ testImplementation("javax.servlet:javax.servlet-api:3.0.1")
+ testImplementation(project(":instrumentation:servlet:servlet-3.0:javaagent"))
+}
diff --git a/instrumentation/servlet/servlet-3.0/javaagent-unit-tests/src/test/java/io/opentelemetry/javaagent/instrumentation/servlet/v3_0/snippet/SnippetPrintWriterTest.java b/instrumentation/servlet/servlet-3.0/javaagent-unit-tests/src/test/java/io/opentelemetry/javaagent/instrumentation/servlet/v3_0/snippet/SnippetPrintWriterTest.java
new file mode 100644
index 000000000000..eedc842a5792
--- /dev/null
+++ b/instrumentation/servlet/servlet-3.0/javaagent-unit-tests/src/test/java/io/opentelemetry/javaagent/instrumentation/servlet/v3_0/snippet/SnippetPrintWriterTest.java
@@ -0,0 +1,158 @@
+/*
+ * Copyright The OpenTelemetry Authors
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+package io.opentelemetry.javaagent.instrumentation.servlet.v3_0.snippet;
+
+import static io.opentelemetry.javaagent.instrumentation.servlet.v3_0.snippet.TestUtil.readFileAsString;
+import static org.assertj.core.api.AssertionsForClassTypes.assertThat;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+
+import java.io.IOException;
+import java.io.PrintWriter;
+import java.io.StringWriter;
+import java.nio.charset.Charset;
+import javax.servlet.http.HttpServletResponse;
+import javax.servlet.http.HttpServletResponseWrapper;
+import org.junit.jupiter.api.Test;
+
+class SnippetPrintWriterTest {
+
+ @Test
+ void testInjectToTextHtml() throws IOException {
+ String snippet = "\n ";
+ String html = readFileAsString("beforeSnippetInjection.html");
+
+ InMemoryHttpServletResponse response = createInMemoryHttpServletResponse("text/html");
+ SnippetInjectingResponseWrapper responseWrapper =
+ new SnippetInjectingResponseWrapper(response, snippet);
+
+ responseWrapper.getWriter().write(html);
+ responseWrapper.getWriter().flush();
+
+ String expectedHtml = readFileAsString("afterSnippetInjection.html");
+ assertThat(response.getStringContent()).isEqualTo(expectedHtml);
+ }
+
+ @Test
+ void testInjectToChineseTextHtml() throws IOException {
+ String snippet = "\n ";
+ String html = readFileAsString("beforeSnippetInjectionChinese.html");
+
+ InMemoryHttpServletResponse response = createInMemoryHttpServletResponse("text/html");
+ SnippetInjectingResponseWrapper responseWrapper =
+ new SnippetInjectingResponseWrapper(response, snippet);
+
+ responseWrapper.getWriter().write(html);
+ responseWrapper.getWriter().flush();
+
+ String expectedHtml = readFileAsString("afterSnippetInjectionChinese.html");
+ assertThat(response.getStringContent()).isEqualTo(expectedHtml);
+ }
+
+ @Test
+ void shouldNotInjectToTextHtml() throws IOException {
+ String snippet = "\n ";
+ String html = readFileAsString("beforeSnippetInjection.html");
+
+ InMemoryHttpServletResponse response = createInMemoryHttpServletResponse("not/text");
+
+ SnippetInjectingResponseWrapper responseWrapper =
+ new SnippetInjectingResponseWrapper(response, snippet);
+
+ responseWrapper.getWriter().write(html);
+ responseWrapper.getWriter().flush();
+
+ assertThat(response.getStringContent()).isEqualTo(html);
+ }
+
+ @Test
+ void testWriteInt() throws IOException {
+ String snippet = "\n ";
+ String html = readFileAsString("beforeSnippetInjection.html");
+
+ InMemoryHttpServletResponse response = createInMemoryHttpServletResponse("text/html");
+ SnippetInjectingResponseWrapper responseWrapper =
+ new SnippetInjectingResponseWrapper(response, snippet);
+
+ byte[] originalBytes = html.getBytes(Charset.defaultCharset());
+ for (byte originalByte : originalBytes) {
+ responseWrapper.getWriter().write(originalByte);
+ }
+ responseWrapper.getWriter().flush();
+
+ String expectedHtml = readFileAsString("afterSnippetInjection.html");
+ assertThat(response.getStringContent()).isEqualTo(expectedHtml);
+ }
+
+ @Test
+ void testWriteCharArray() throws IOException {
+ String snippet = "\n ";
+ String html = readFileAsString("beforeSnippetInjectionChinese.html");
+
+ InMemoryHttpServletResponse response = createInMemoryHttpServletResponse("text/html");
+ SnippetInjectingResponseWrapper responseWrapper =
+ new SnippetInjectingResponseWrapper(response, snippet);
+
+ char[] originalChars = html.toCharArray();
+ responseWrapper.getWriter().write(originalChars, 0, originalChars.length);
+ responseWrapper.getWriter().flush();
+
+ String expectedHtml = readFileAsString("afterSnippetInjectionChinese.html");
+ assertThat(response.getStringContent()).isEqualTo(expectedHtml);
+ }
+
+ @Test
+ void testWriteWithOffset() throws IOException {
+ String snippet = "\n ";
+ String html = readFileAsString("beforeSnippetInjectionChinese.html");
+ String extraBuffer = "this buffer should not be print out";
+ html = extraBuffer + html;
+
+ InMemoryHttpServletResponse response = createInMemoryHttpServletResponse("text/html");
+ SnippetInjectingResponseWrapper responseWrapper =
+ new SnippetInjectingResponseWrapper(response, snippet);
+
+ responseWrapper
+ .getWriter()
+ .write(html, extraBuffer.length(), html.length() - extraBuffer.length());
+ responseWrapper.getWriter().flush();
+
+ String expectedHtml = readFileAsString("afterSnippetInjectionChinese.html");
+ assertThat(response.getStringContent()).isEqualTo(expectedHtml);
+ }
+
+ private static InMemoryHttpServletResponse createInMemoryHttpServletResponse(String contentType) {
+ HttpServletResponse response = mock(HttpServletResponse.class);
+ when(response.getContentType()).thenReturn(contentType);
+ when(response.getStatus()).thenReturn(200);
+ when(response.containsHeader("content-type")).thenReturn(true);
+ return new InMemoryHttpServletResponse(response);
+ }
+
+ private static class InMemoryHttpServletResponse extends HttpServletResponseWrapper {
+
+ private PrintWriter printWriter;
+ private StringWriter stringWriter;
+
+ InMemoryHttpServletResponse(HttpServletResponse delegate) {
+ super(delegate);
+ }
+
+ @Override
+ public PrintWriter getWriter() {
+ if (printWriter == null) {
+ stringWriter = new StringWriter();
+ printWriter = new PrintWriter(stringWriter);
+ }
+ return printWriter;
+ }
+
+ String getStringContent() {
+ printWriter.flush();
+ return stringWriter.toString();
+ }
+ }
+}
diff --git a/instrumentation/servlet/servlet-3.0/javaagent-unit-tests/src/test/java/io/opentelemetry/javaagent/instrumentation/servlet/v3_0/snippet/SnippetServletOutputStreamTest.java b/instrumentation/servlet/servlet-3.0/javaagent-unit-tests/src/test/java/io/opentelemetry/javaagent/instrumentation/servlet/v3_0/snippet/SnippetServletOutputStreamTest.java
new file mode 100644
index 000000000000..46018baba9eb
--- /dev/null
+++ b/instrumentation/servlet/servlet-3.0/javaagent-unit-tests/src/test/java/io/opentelemetry/javaagent/instrumentation/servlet/v3_0/snippet/SnippetServletOutputStreamTest.java
@@ -0,0 +1,139 @@
+/*
+ * Copyright The OpenTelemetry Authors
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+package io.opentelemetry.javaagent.instrumentation.servlet.v3_0.snippet;
+
+import static io.opentelemetry.javaagent.instrumentation.servlet.v3_0.snippet.TestUtil.readFileAsBytes;
+import static java.nio.charset.StandardCharsets.UTF_8;
+import static org.assertj.core.api.AssertionsForClassTypes.assertThat;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.nio.charset.Charset;
+import javax.servlet.ServletOutputStream;
+import javax.servlet.http.HttpServletResponse;
+import org.junit.jupiter.api.Test;
+
+class SnippetServletOutputStreamTest {
+
+ @Test
+ void testInjectionForStringContainHeadTag() throws IOException {
+ String snippet = "\n ";
+ byte[] html = readFileAsBytes("beforeSnippetInjection.html");
+
+ InjectionState obj = createInjectionStateForTesting(snippet, UTF_8);
+ InMemoryServletOutputStream out = new InMemoryServletOutputStream();
+
+ OutputStreamSnippetInjectionHelper helper = new OutputStreamSnippetInjectionHelper(snippet);
+ boolean injected = helper.handleWrite(obj, out, html, 0, html.length);
+ assertThat(obj.getHeadTagBytesSeen()).isEqualTo(-1);
+ assertThat(injected).isEqualTo(true);
+
+ byte[] expectedHtml = readFileAsBytes("afterSnippetInjection.html");
+ assertThat(out.getBytes()).isEqualTo(expectedHtml);
+ }
+
+ @Test
+ void testInjectionForChinese() throws IOException {
+ String snippet = "\n ";
+ byte[] html = readFileAsBytes("beforeSnippetInjectionChinese.html");
+
+ InjectionState obj = createInjectionStateForTesting(snippet, UTF_8);
+ InMemoryServletOutputStream out = new InMemoryServletOutputStream();
+
+ OutputStreamSnippetInjectionHelper helper = new OutputStreamSnippetInjectionHelper(snippet);
+ boolean injected = helper.handleWrite(obj, out, html, 0, html.length);
+
+ byte[] expectedHtml = readFileAsBytes("afterSnippetInjectionChinese.html");
+ assertThat(injected).isTrue();
+ assertThat(obj.getHeadTagBytesSeen()).isEqualTo(-1);
+ assertThat(out.getBytes()).isEqualTo(expectedHtml);
+ }
+
+ @Test
+ void testInjectionForStringWithoutHeadTag() throws IOException {
+ String snippet = "\n ";
+ byte[] html = readFileAsBytes("htmlWithoutHeadTag.html");
+
+ InjectionState obj = createInjectionStateForTesting(snippet, UTF_8);
+ InMemoryServletOutputStream out = new InMemoryServletOutputStream();
+
+ OutputStreamSnippetInjectionHelper helper = new OutputStreamSnippetInjectionHelper(snippet);
+ boolean injected = helper.handleWrite(obj, out, html, 0, html.length);
+
+ assertThat(injected).isFalse();
+ assertThat(obj.getHeadTagBytesSeen()).isEqualTo(0);
+ assertThat(out.getBytes()).isEmpty();
+ }
+
+ @Test
+ void testHeadTagSplitAcrossTwoWrites() throws IOException {
+ String snippet = "\n ";
+ String htmlFirstPart = "\n\n\n"
+ + " \n"
+ + " Title\n"
+ + "\n"
+ + "\n"
+ + "\n"
+ + "\n"
+ + "