Repository: quic-go/quic-go
Pull request #5068 (2025-04-20 08:58:32)

http3: migrate the headers tests away from Ginkgo
Part of #3652.

codecov[bot]: ## [Codecov](https://app.codecov.io/gh/quic-go/quic-go/pull/5068?dropdown=coverage&src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=quic-go) Report
All modified and coverable lines are covered by tests :white_check_mark:
> Project coverage is 84.02%. Comparing base [(`d35b5ac`)](https://app.codecov.io/gh/quic-go/quic-go/commit/d35b5ac187da3f56eb0026fc79c27254b95ca366?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=quic-go) to head [(`16b53a6`)](https://app.codecov.io/gh/quic-go/quic-go/commit/16b53a65c1c69784d8ebee93c51fe5e3d5e3b04c?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=quic-go).

<details><summary>Additional details and impacted files</summary>


```diff
@@            Coverage Diff             @@
##           master    #5068      +/-   ##
==========================================
- Coverage   84.19%   84.02%   -0.17%     
==========================================
  Files         153      150       -3     
  Lines       16994    16649     -345     
==========================================
- Hits        14308    13989     -319     
+ Misses       2156     2142      -14     
+ Partials      530      518      -12     
```

</details>

[:umbrella: View full report in Codecov by Sentry](https://app.codecov.io/gh/quic-go/quic-go/pull/5068?dropdown=coverage&src=pr&el=continue&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=quic-go).   
:loudspeaker: Have feedback on the report? [Share it here](https://about.codecov.io/codecov-pr-comment-feedback/?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=quic-go).

<details><summary> :rocket: New features to boost your workflow: </summary>

- :snowflake: [Test Analytics](https://docs.codecov.com/docs/test-analytics): Detect flaky tests, report on failures, and find test suite problems.
</details>

Patch:
diff --git a/http3/headers.go b/http3/headers.go
index 27af9500..db9afc9f 100644
--- a/http3/headers.go
+++ b/http3/headers.go
@@ -211,13 +211,6 @@ func requestFromHeaders(headerFields []qpack.HeaderField) (*http.Request, error)
 	}, nil
 }
 
-func hostnameFromURL(url *url.URL) string {
-	if url != nil {
-		return url.Host
-	}
-	return ""
-}
-
 // updateResponseFromHeaders sets up http.Response as an HTTP/3 response,
 // using the decoded qpack header filed.
 // It is only called for the HTTP header (and not the HTTP trailer).
@@ -228,7 +221,7 @@ func updateResponseFromHeaders(rsp *http.Response, headerFields []qpack.HeaderFi
 		return err
 	}
 	if hdr.Status == "" {
-		return errors.New("missing status field")
+		return errors.New("missing :status field")
 	}
 	rsp.Proto = "HTTP/3.0"
 	rsp.ProtoMajor = 3
diff --git a/http3/transport.go b/http3/transport.go
index bdedb524..dd73a8bb 100644
--- a/http3/transport.go
+++ b/http3/transport.go
@@ -10,6 +10,7 @@ import (
 	"net"
 	"net/http"
 	"net/http/httptrace"
+	"net/url"
 	"strings"
 	"sync"
 	"sync/atomic"
@@ -428,6 +429,13 @@ func (t *Transport) Close() error {
 	return nil
 }
 
+func hostnameFromURL(url *url.URL) string {
+	if url != nil {
+		return url.Host
+	}
+	return ""
+}
+
 func validMethod(method string) bool {
 	/*
 				     Method         = "OPTIONS"                ; Section 9.2


Test patch:
diff --git a/config_test.go b/config_test.go
index 7dc10801..71a003cf 100644
--- a/config_test.go
+++ b/config_test.go
@@ -2,7 +2,6 @@ package quic
 
 import (
 	"context"
-	"errors"
 	"reflect"
 	"testing"
 	"time"
@@ -11,6 +10,7 @@ import (
 	"github.com/quic-go/quic-go/logging"
 	"github.com/quic-go/quic-go/quicvarint"
 
+	"github.com/stretchr/testify/assert"
 	"github.com/stretchr/testify/require"
 )
 
@@ -137,7 +137,7 @@ func TestConfigCloning(t *testing.T) {
 	t.Run("function fields", func(t *testing.T) {
 		var calledAllowConnectionWindowIncrease, calledTracer bool
 		c1 := &Config{
-			GetConfigForClient:            func(info *ClientInfo) (*Config, error) { return nil, errors.New("nope") },
+			GetConfigForClient:            func(info *ClientInfo) (*Config, error) { return nil, assert.AnError },
 			AllowConnectionWindowIncrease: func(Connection, uint64) bool { calledAllowConnectionWindowIncrease = true; return true },
 			Tracer: func(context.Context, logging.Perspective, ConnectionID) *logging.ConnectionTracer {
 				calledTracer = true
@@ -148,7 +148,7 @@ func TestConfigCloning(t *testing.T) {
 		c2.AllowConnectionWindowIncrease(nil, 1234)
 		require.True(t, calledAllowConnectionWindowIncrease)
 		_, err := c2.GetConfigForClient(&ClientInfo{})
-		require.EqualError(t, err, "nope")
+		require.ErrorIs(t, err, assert.AnError)
 		c2.Tracer(context.Background(), logging.PerspectiveClient, protocol.ConnectionID{})
 		require.True(t, calledTracer)
 	})
diff --git a/connection_test.go b/connection_test.go
index caaa8ff5..36990e78 100644
--- a/connection_test.go
+++ b/connection_test.go
@@ -257,19 +257,18 @@ func TestConnectionHandleReceiveStreamFrames(t *testing.T) {
 		mockCtrl := gomock.NewController(t)
 		streamsMap := NewMockStreamManager(mockCtrl)
 		tc := newServerTestConnection(t, mockCtrl, nil, false, connectionOptStreamManager(streamsMap))
-		testErr := errors.New("test err")
 		// STREAM frame
-		streamsMap.EXPECT().GetOrOpenReceiveStream(streamID).Return(nil, testErr)
+		streamsMap.EXPECT().GetOrOpenReceiveStream(streamID).Return(nil, assert.AnError)
 		_, err := tc.conn.handleFrame(f, protocol.Encryption1RTT, connID, now)
-		require.ErrorIs(t, err, testErr)
+		require.ErrorIs(t, err, assert.AnError)
 		// RESET_STREAM frame
-		streamsMap.EXPECT().GetOrOpenReceiveStream(streamID).Return(nil, testErr)
+		streamsMap.EXPECT().GetOrOpenReceiveStream(streamID).Return(nil, assert.AnError)
 		_, err = tc.conn.handleFrame(rsf, protocol.Encryption1RTT, connID, now)
-		require.ErrorIs(t, err, testErr)
+		require.ErrorIs(t, err, assert.AnError)
 		// STREAM_DATA_BLOCKED frames are not passed to the stream
-		streamsMap.EXPECT().GetOrOpenReceiveStream(streamID).Return(nil, testErr)
+		streamsMap.EXPECT().GetOrOpenReceiveStream(streamID).Return(nil, assert.AnError)
 		_, err = tc.conn.handleFrame(sdbf, protocol.Encryption1RTT, connID, now)
-		require.ErrorIs(t, err, testErr)
+		require.ErrorIs(t, err, assert.AnError)
 	})
 }
 
@@ -315,15 +314,14 @@ func TestConnectionHandleSendStreamFrames(t *testing.T) {
 		mockCtrl := gomock.NewController(t)
 		streamsMap := NewMockStreamManager(mockCtrl)
 		tc := newServerTestConnection(t, mockCtrl, nil, false, connectionOptStreamManager(streamsMap))
-		testErr := errors.New("test err")
 		// STOP_SENDING frame
-		streamsMap.EXPECT().GetOrOpenSendStream(streamID).Return(nil, testErr)
+		streamsMap.EXPECT().GetOrOpenSendStream(streamID).Return(nil, assert.AnError)
 		_, err := tc.conn.handleFrame(ss, protocol.Encryption1RTT, connID, now)
-		require.ErrorIs(t, err, testErr)
+		require.ErrorIs(t, err, assert.AnError)
 		// MAX_STREAM_DATA frame
-		streamsMap.EXPECT().GetOrOpenSendStream(streamID).Return(nil, testErr)
+		streamsMap.EXPECT().GetOrOpenSendStream(streamID).Return(nil, assert.AnError)
 		_, err = tc.conn.handleFrame(msd, protocol.Encryption1RTT, connID, now)
-		require.ErrorIs(t, err, testErr)
+		require.ErrorIs(t, err, assert.AnError)
 	})
 }
 
@@ -437,7 +435,7 @@ func TestConnectionTransportError(t *testing.T) {
 	expectedErr := &qerr.TransportError{
 		ErrorCode:    1337,
 		FrameType:    42,
-		ErrorMessage: "test error",
+		ErrorMessage: "foobar",
 	}
 	tc.connRunner.EXPECT().Remove(gomock.Any()).AnyTimes()
 	b := getPacketBuffer()
@@ -471,7 +469,7 @@ func TestConnectionApplicationClose(t *testing.T) {
 	errChan := make(chan error, 1)
 	expectedErr := &qerr.ApplicationError{
 		ErrorCode:    1337,
-		ErrorMessage: "test error",
+		ErrorMessage: "foobar",
 	}
 	tc.connRunner.EXPECT().Remove(gomock.Any()).AnyTimes()
 	b := getPacketBuffer()
@@ -485,7 +483,7 @@ func TestConnectionApplicationClose(t *testing.T) {
 	)
 
 	go func() { errChan <- tc.conn.run() }()
-	tc.conn.CloseWithError(1337, "test error")
+	tc.conn.CloseWithError(1337, "foobar")
 
 	select {
 	case err := <-errChan:
@@ -866,8 +864,7 @@ func TestConnectionUnpackFailureDropped(t *testing.T) {
 	})
 
 	t.Run("header parse error", func(t *testing.T) {
-		testErr := errors.New("foo")
-		testConnectionUnpackFailureDropped(t, &headerParseError{err: testErr}, logging.PacketDropHeaderParseError)
+		testConnectionUnpackFailureDropped(t, &headerParseError{err: assert.AnError}, logging.PacketDropHeaderParseError)
 	})
 }
 
diff --git a/datagram_queue_test.go b/datagram_queue_test.go
index dc621183..c223e9c4 100644
--- a/datagram_queue_test.go
+++ b/datagram_queue_test.go
@@ -2,13 +2,13 @@ package quic
 
 import (
 	"context"
-	"errors"
 	"testing"
 	"time"
 
 	"github.com/quic-go/quic-go/internal/utils"
 	"github.com/quic-go/quic-go/internal/wire"
 
+	"github.com/stretchr/testify/assert"
 	"github.com/stretchr/testify/require"
 )
 
@@ -143,18 +143,18 @@ func TestDatagramQueueClose(t *testing.T) {
 		errChan2 <- err
 	}()
 
-	queue.CloseWithError(errors.New("test error"))
+	queue.CloseWithError(assert.AnError)
 
 	select {
 	case err := <-errChan1:
-		require.EqualError(t, err, "test error")
+		require.ErrorIs(t, err, assert.AnError)
 	case <-time.After(time.Second):
 		t.Fatal("timeout")
 	}
 
 	select {
 	case err := <-errChan2:
-		require.EqualError(t, err, "test error")
+		require.ErrorIs(t, err, assert.AnError)
 	case <-time.After(time.Second):
 		t.Fatal("timeout")
 	}
diff --git a/http3/error_test.go b/http3/error_test.go
index 2ee08739..de7a85fd 100644
--- a/http3/error_test.go
+++ b/http3/error_test.go
@@ -1,24 +1,22 @@
 package http3
 
 import (
-	"errors"
 	"testing"
 
 	"github.com/quic-go/quic-go"
 
+	"github.com/stretchr/testify/assert"
 	"github.com/stretchr/testify/require"
 )
 
 func TestErrorConversion(t *testing.T) {
-	regularErr := errors.New("foobar")
-
 	tests := []struct {
 		name     string
 		input    error
 		expected error
 	}{
 		{name: "nil error", input: nil, expected: nil},
-		{name: "regular error", input: regularErr, expected: regularErr},
+		{name: "regular error", input: assert.AnError, expected: assert.AnError},
 		{
 			name:     "stream error",
 			input:    &quic.StreamError{ErrorCode: 1337, Remote: true},
diff --git a/http3/headers_test.go b/http3/headers_test.go
index 3f23cc3b..ef5ac278 100644
--- a/http3/headers_test.go
+++ b/http3/headers_test.go
@@ -3,454 +3,452 @@ package http3
 import (
 	"fmt"
 	"net/http"
-	"net/url"
+	"testing"
 
-	. "github.com/onsi/ginkgo/v2"
-	. "github.com/onsi/gomega"
 	"github.com/quic-go/qpack"
+	"github.com/stretchr/testify/require"
 )
 
-var _ = Describe("Request", func() {
-	It("populates requests", func() {
+func TestRequestHeaderParsing(t *testing.T) {
+	t.Run("regular path", func(t *testing.T) {
+		testRequestHeaderParsing(t, "/foo")
+	})
+	// see https://github.com/quic-go/quic-go/pull/1898
+	t.Run("path starting with //", func(t *testing.T) {
+		testRequestHeaderParsing(t, "//foo")
+	})
+}
+
+func testRequestHeaderParsing(t *testing.T, path string) {
+	headers := []qpack.HeaderField{
+		{Name: ":path", Value: path},
+		{Name: ":authority", Value: "quic-go.net"},
+		{Name: ":method", Value: http.MethodGet},
+		{Name: "content-length", Value: "42"},
+	}
+	req, err := requestFromHeaders(headers)
+	require.NoError(t, err)
+	require.Equal(t, http.MethodGet, req.Method)
+	require.Equal(t, path, req.URL.Path)
+	require.Equal(t, "", req.URL.Host)
+	require.Equal(t, "HTTP/3.0", req.Proto)
+	require.Equal(t, 3, req.ProtoMajor)
+	require.Zero(t, req.ProtoMinor)
+	require.Equal(t, int64(42), req.ContentLength)
+	require.Equal(t, 1, len(req.Header))
+	require.Equal(t, "42", req.Header.Get("Content-Length"))
+	require.Nil(t, req.Body)
+	require.Equal(t, "quic-go.net", req.Host)
+	require.Equal(t, path, req.RequestURI)
+}
+
+func TestRequestHeadersContentLength(t *testing.T) {
+	t.Run("no content length", func(t *testing.T) {
 		headers := []qpack.HeaderField{
-			{Name: ":path", Value: "/foo"},
-			{Name: ":authority", Value: "quic.clemente.io"},
-			{Name: ":method", Value: "GET"},
-			{Name: "content-length", Value: "42"},
-		}
-		req, err := requestFromHeaders(headers)
-		Expect(err).NotTo(HaveOccurred())
-		Expect(req.Method).To(Equal("GET"))
-		Expect(req.URL.Path).To(Equal("/foo"))
-		Expect(req.URL.Host).To(BeEmpty())
-		Expect(req.Proto).To(Equal("HTTP/3.0"))
-		Expect(req.ProtoMajor).To(Equal(3))
-		Expect(req.ProtoMinor).To(BeZero())
-		Expect(req.ContentLength).To(Equal(int64(42)))
-		Expect(req.Header).To(HaveLen(1))
-		Expect(req.Header.Get("Content-Length")).To(Equal("42"))
-		Expect(req.Body).To(BeNil())
-		Expect(req.Host).To(Equal("quic.clemente.io"))
-		Expect(req.RequestURI).To(Equal("/foo"))
-	})
-
-	It("sets the ContentLength to -1", func() {
-		headers := []qpack.HeaderField{
-			{Name: ":path", Value: "/foo"},
-			{Name: ":authority", Value: "quic.clemente.io"},
-			{Name: ":method", Value: "GET"},
+			{Name: ":path", Value: "/"},
+			{Name: ":authority", Value: "quic-go.net"},
+			{Name: ":method", Value: http.MethodGet},
 		}
 		req, err := requestFromHeaders(headers)
-		Expect(err).ToNot(HaveOccurred())
-		Expect(req.ContentLength).To(BeEquivalentTo(-1))
-	})
-
-	It("rejects upper-case fields", func() {
-		headers := []qpack.HeaderField{
-			{Name: ":path", Value: "/foo"},
-			{Name: ":authority", Value: "quic.clemente.io"},
-			{Name: ":method", Value: "GET"},
-			{Name: "Content-Length", Value: "42"},
-		}
-		_, err := requestFromHeaders(headers)
-		Expect(err).To(MatchError("header field is not lower-case: Content-Length"))
-	})
-
-	It("rejects unknown pseudo headers", func() {
-		headers := []qpack.HeaderField{
-			{Name: ":path", Value: "/foo"},
-			{Name: ":authority", Value: "quic.clemente.io"},
-			{Name: ":method", Value: "GET"},
-			{Name: ":foo", Value: "bar"},
-		}
-		_, err := requestFromHeaders(headers)
-		Expect(err).To(MatchError("unknown pseudo header: :foo"))
-	})
-
-	It("rejects invalid field names", func() {
-		headers := []qpack.HeaderField{
-			{Name: ":path", Value: "/foo"},
-			{Name: ":authority", Value: "quic.clemente.io"},
-			{Name: ":method", Value: "GET"},
-			{Name: "@", Value: "42"},
-		}
-		_, err := requestFromHeaders(headers)
-		Expect(err).To(MatchError(`invalid header field name: "@"`))
-	})
-
-	It("rejects invalid field values", func() {
-		headers := []qpack.HeaderField{
-			{Name: ":path", Value: "/foo"},
-			{Name: ":authority", Value: "quic.clemente.io"},
-			{Name: ":method", Value: "GET"},
-			{Name: "content", Value: "\n"},
-		}
-		_, err := requestFromHeaders(headers)
-		Expect(err).To(MatchError(`invalid header field value for content: "\n"`))
-	})
-
-	It("rejects pseudo header fields after regular header fields", func() {
-		headers := []qpack.HeaderField{
-			{Name: ":path", Value: "/foo"},
-			{Name: "content-length", Value: "42"},
-			{Name: ":authority", Value: "quic.clemente.io"},
-			{Name: ":method", Value: "GET"},
-		}
-		_, err := requestFromHeaders(headers)
-		Expect(err).To(MatchError("received pseudo header :authority after a regular header field"))
-	})
-
-	It("rejects negative Content-Length values", func() {
-		headers := []qpack.HeaderField{
-			{Name: ":path", Value: "/foo"},
-			{Name: ":authority", Value: "quic.clemente.io"},
-			{Name: ":method", Value: "GET"},
-			{Name: "content-length", Value: "-42"},
-		}
-		_, err := requestFromHeaders(headers)
-		Expect(err).To(HaveOccurred())
-		Expect(err.Error()).To(ContainSubstring("invalid content length"))
+		require.NoError(t, err)
+		require.Equal(t, int64(-1), req.ContentLength)
 	})
 
-	It("rejects multiple Content-Length headers, if they differ", func() {
+	t.Run("multiple content lengths", func(t *testing.T) {
 		headers := []qpack.HeaderField{
-			{Name: ":path", Value: "/foo"},
-			{Name: ":authority", Value: "quic.clemente.io"},
-			{Name: ":method", Value: "GET"},
+			{Name: ":path", Value: "/"},
+			{Name: ":authority", Value: "quic-go.net"},
+			{Name: ":method", Value: http.MethodGet},
 			{Name: "content-length", Value: "42"},
-			{Name: "content-length", Value: "1337"},
-		}
-		_, err := requestFromHeaders(headers)
-		Expect(err).To(MatchError("contradicting content lengths (42 and 1337)"))
-	})
-
-	It("deduplicates multiple Content-Length headers, if they're the same", func() {
-		headers := []qpack.HeaderField{
-			{Name: ":path", Value: "/foo"},
-			{Name: ":authority", Value: "quic.clemente.io"},
-			{Name: ":method", Value: "GET"},
 			{Name: "content-length", Value: "42"},
-			{Name: "content-length", Value: "42"},
-		}
-		req, err := requestFromHeaders(headers)
-		Expect(err).ToNot(HaveOccurred())
-		Expect(req.ContentLength).To(Equal(int64(42)))
-		Expect(req.Header.Get("Content-Length")).To(Equal("42"))
-	})
-
-	It("rejects pseudo header fields defined for responses", func() {
-		headers := []qpack.HeaderField{
-			{Name: ":path", Value: "/foo"},
-			{Name: ":authority", Value: "quic.clemente.io"},
-			{Name: ":method", Value: "GET"},
-			{Name: ":status", Value: "404"},
-		}
-		_, err := requestFromHeaders(headers)
-		Expect(err).To(MatchError("invalid request pseudo header: :status"))
-	})
-
-	It("parses path with leading double slashes", func() {
-		headers := []qpack.HeaderField{
-			{Name: ":path", Value: "//foo"},
-			{Name: ":authority", Value: "quic.clemente.io"},
-			{Name: ":method", Value: "GET"},
-		}
-		req, err := requestFromHeaders(headers)
-		Expect(err).NotTo(HaveOccurred())
-		Expect(req.Header).To(BeEmpty())
-		Expect(req.Body).To(BeNil())
-		Expect(req.URL.Path).To(Equal("//foo"))
-		Expect(req.URL.Host).To(BeEmpty())
-		Expect(req.Host).To(Equal("quic.clemente.io"))
-		Expect(req.RequestURI).To(Equal("//foo"))
-	})
-
-	It("concatenates the cookie headers", func() {
-		headers := []qpack.HeaderField{
-			{Name: ":path", Value: "/foo"},
-			{Name: ":authority", Value: "quic.clemente.io"},
-			{Name: ":method", Value: "GET"},
-			{Name: "cookie", Value: "cookie1=foobar1"},
-			{Name: "cookie", Value: "cookie2=foobar2"},
-		}
-		req, err := requestFromHeaders(headers)
-		Expect(err).NotTo(HaveOccurred())
-		Expect(req.Header).To(Equal(http.Header{
-			"Cookie": []string{"cookie1=foobar1; cookie2=foobar2"},
-		}))
-	})
-
-	It("handles Other headers", func() {
-		headers := []qpack.HeaderField{
-			{Name: ":path", Value: "/foo"},
-			{Name: ":authority", Value: "quic.clemente.io"},
-			{Name: ":method", Value: "GET"},
-			{Name: "cache-control", Value: "max-age=0"},
-			{Name: "duplicate-header", Value: "1"},
-			{Name: "duplicate-header", Value: "2"},
 		}
 		req, err := requestFromHeaders(headers)
-		Expect(err).NotTo(HaveOccurred())
-		Expect(req.Header).To(Equal(http.Header{
-			"Cache-Control":    []string{"max-age=0"},
-			"Duplicate-Header": []string{"1", "2"},
-		}))
-	})
-
-	It("errors with missing path", func() {
-		headers := []qpack.HeaderField{
-			{Name: ":authority", Value: "quic.clemente.io"},
-			{Name: ":method", Value: "GET"},
-		}
-		_, err := requestFromHeaders(headers)
-		Expect(err).To(MatchError(":path, :authority and :method must not be empty"))
-	})
-
-	It("errors with missing method", func() {
-		headers := []qpack.HeaderField{
-			{Name: ":path", Value: "/foo"},
-			{Name: ":authority", Value: "quic.clemente.io"},
-		}
-		_, err := requestFromHeaders(headers)
-		Expect(err).To(MatchError(":path, :authority and :method must not be empty"))
-	})
-
-	It("errors with missing authority", func() {
-		headers := []qpack.HeaderField{
-			{Name: ":path", Value: "/foo"},
-			{Name: ":method", Value: "GET"},
-		}
-		_, err := requestFromHeaders(headers)
-		Expect(err).To(MatchError(":path, :authority and :method must not be empty"))
-	})
-
-	It("errors with invalid protocol", func() {
-		headers := []qpack.HeaderField{
-			{Name: ":path", Value: "/foo"},
-			{Name: ":authority", Value: "quic.clemente.io"},
-			{Name: ":method", Value: "GET"},
-			{Name: ":protocol", Value: "connect-udp"},
-		}
-		_, err := requestFromHeaders(headers)
-		Expect(err).To(MatchError(":protocol must be empty"))
-	})
-
-	It("errors with duplicate pseudo header in request", func() {
-		headers := []qpack.HeaderField{
-			{Name: ":path", Value: "/foo"},
-			{Name: ":authority", Value: "quic.clemente.io"},
-			{Name: ":method", Value: "GET"},
-			{Name: ":scheme", Value: "https"},
-			{Name: ":method", Value: "POST"},
-		}
-		_, err := requestFromHeaders(headers)
-		Expect(err).To(MatchError("duplicate pseudo header: :method"))
-	})
-
-	Context("regular HTTP CONNECT", func() {
-		It("handles CONNECT method", func() {
-			headers := []qpack.HeaderField{
-				{Name: ":authority", Value: "quic.clemente.io"},
-				{Name: ":method", Value: http.MethodConnect},
+		require.NoError(t, err)
+		require.Equal(t, "42", req.Header.Get("Content-Length"))
+	})
+}
+
+func TestRequestHeadersContentLengthValidation(t *testing.T) {
+	for _, tc := range []struct {
+		name        string
+		headers     []qpack.HeaderField
+		err         string
+		errContains string
+	}{
+		{
+			name: "negative content length",
+			headers: []qpack.HeaderField{
+				{Name: "content-length", Value: "-42"},
+			},
+			errContains: "invalid content length",
+		},
+		{
+			name: "multiple differing content lengths",
+			headers: []qpack.HeaderField{
+				{Name: "content-length", Value: "42"},
+				{Name: "content-length", Value: "1337"},
+			},
+			err: "contradicting content lengths (42 and 1337)",
+		},
+	} {
+		t.Run(tc.name, func(t *testing.T) {
+			_, err := requestFromHeaders(tc.headers)
+			if tc.errContains != "" {
+				require.ErrorContains(t, err, tc.errContains)
 			}
-			req, err := requestFromHeaders(headers)
-			Expect(err).NotTo(HaveOccurred())
-			Expect(req.Method).To(Equal(http.MethodConnect))
-			Expect(req.Proto).To(Equal("HTTP/3.0"))
-			Expect(req.RequestURI).To(Equal("quic.clemente.io"))
-		})
-
-		It("errors with missing authority in CONNECT method", func() {
-			headers := []qpack.HeaderField{
-				{Name: ":method", Value: http.MethodConnect},
+			if tc.err != "" {
+				require.EqualError(t, err, tc.err)
 			}
-			_, err := requestFromHeaders(headers)
-			Expect(err).To(MatchError(":path must be empty and :authority must not be empty"))
 		})
-
-		It("errors with extra path in CONNECT method", func() {
-			headers := []qpack.HeaderField{
+	}
+}
+
+func TestRequestHeadersValidation(t *testing.T) {
+	for _, tc := range []struct {
+		name    string
+		headers []qpack.HeaderField
+		err     string
+	}{
+		{
+			name: "upper-case field name",
+			headers: []qpack.HeaderField{
+				{Name: "Content-Length", Value: "42"},
+			},
+			err: "header field is not lower-case: Content-Length",
+		},
+		{
+			name: "unknown pseudo header",
+			headers: []qpack.HeaderField{
+				{Name: ":foo", Value: "bar"},
+			},
+			err: "unknown pseudo header: :foo",
+		},
+		{
+			name: "pseudo header after regular header",
+			headers: []qpack.HeaderField{
 				{Name: ":path", Value: "/foo"},
-				{Name: ":authority", Value: "quic.clemente.io"},
-				{Name: ":method", Value: http.MethodConnect},
-			}
-			_, err := requestFromHeaders(headers)
-			Expect(err).To(MatchError(":path must be empty and :authority must not be empty"))
-		})
-	})
-
-	Context("Extended CONNECT", func() {
-		It("handles Extended CONNECT method", func() {
-			headers := []qpack.HeaderField{
-				{Name: ":protocol", Value: "webtransport"},
-				{Name: ":scheme", Value: "ftp"},
-				{Name: ":method", Value: http.MethodConnect},
-				{Name: ":authority", Value: "quic.clemente.io"},
-				{Name: ":path", Value: "/foo?val=1337"},
-			}
-			req, err := requestFromHeaders(headers)
-			Expect(err).NotTo(HaveOccurred())
-			Expect(req.Method).To(Equal(http.MethodConnect))
-			Expect(req.Proto).To(Equal("webtransport"))
-			Expect(req.URL.String()).To(Equal("ftp://quic.clemente.io/foo?val=1337"))
-			Expect(req.URL.Query().Get("val")).To(Equal("1337"))
+				{Name: "content-length", Value: "42"},
+				{Name: ":authority", Value: "quic-go.net"},
+			},
+			err: "received pseudo header :authority after a regular header field",
+		},
+		{
+			name: "invalid field name",
+			headers: []qpack.HeaderField{
+				{Name: "@", Value: "42"},
+			},
+			err: `invalid header field name: "@"`,
+		},
+		{
+			name: "invalid field value",
+			headers: []qpack.HeaderField{
+				{Name: "content", Value: "\n"},
+			},
+			err: `invalid header field value for content: "\n"`,
+		},
+		{
+			name: ":status header field", // :status is a response pseudo header
+			headers: []qpack.HeaderField{
+				{Name: ":status", Value: "404"},
+			},
+			err: "invalid request pseudo header: :status",
+		},
+		{
+			name: "missing :path",
+			headers: []qpack.HeaderField{
+				{Name: ":authority", Value: "quic-go.net"},
+				{Name: ":method", Value: http.MethodGet},
+			},
+			err: ":path, :authority and :method must not be empty",
+		},
+		{
+			name: "missing :authority",
+			headers: []qpack.HeaderField{
+				{Name: ":path", Value: "/foo"},
+				{Name: ":method", Value: http.MethodGet},
+			},
+			err: ":path, :authority and :method must not be empty",
+		},
+		{
+			name: "missing :method",
+			headers: []qpack.HeaderField{
+				{Name: ":path", Value: "/foo"},
+				{Name: ":authority", Value: "quic-go.net"},
+			},
+			err: ":path, :authority and :method must not be empty",
+		},
+		{
+			name: "duplicate :path",
+			headers: []qpack.HeaderField{
+				{Name: ":path", Value: "/foo"},
+				{Name: ":path", Value: "/foo"},
+			},
+			err: "duplicate pseudo header: :path",
+		},
+		{
+			name: "duplicate :authority",
+			headers: []qpack.HeaderField{
+				{Name: ":authority", Value: "quic-go.net"},
+				{Name: ":authority", Value: "quic-go.net"},
+			},
+			err: "duplicate pseudo header: :authority",
+		},
+		{
+			name: "duplicate :method",
+			headers: []qpack.HeaderField{
+				{Name: ":method", Value: http.MethodGet},
+				{Name: ":method", Value: http.MethodGet},
+			},
+			err: "duplicate pseudo header: :method",
+		},
+		{
+			name: "invalid :protocol",
+			headers: []qpack.HeaderField{
+				{Name: ":path", Value: "/foo"},
+				{Name: ":authority", Value: "quic-go.net"},
+				{Name: ":method", Value: http.MethodGet},
+				{Name: ":protocol", Value: "connect-udp"},
+			},
+			err: ":protocol must be empty",
+		},
+	} {
+		t.Run(tc.name, func(t *testing.T) {
+			_, err := requestFromHeaders(tc.headers)
+			require.EqualError(t, err, tc.err)
 		})
-
-		It("errors with missing scheme", func() {
-			headers := []qpack.HeaderField{
-				{Name: ":protocol", Value: "webtransport"},
+	}
+}
+
+func TestCookieHeader(t *testing.T) {
+	headers := []qpack.HeaderField{
+		{Name: ":path", Value: "/foo"},
+		{Name: ":authority", Value: "quic-go.net"},
+		{Name: ":method", Value: http.MethodGet},
+		{Name: "cookie", Value: "cookie1=foobar1"},
+		{Name: "cookie", Value: "cookie2=foobar2"},
+	}
+	req, err := requestFromHeaders(headers)
+	require.NoError(t, err)
+	require.Equal(t, http.Header{
+		"Cookie": []string{"cookie1=foobar1; cookie2=foobar2"},
+	}, req.Header)
+}
+
+func TestHeadersConcatenation(t *testing.T) {
+	headers := []qpack.HeaderField{
+		{Name: ":path", Value: "/foo"},
+		{Name: ":authority", Value: "quic-go.net"},
+		{Name: ":method", Value: http.MethodGet},
+		{Name: "cache-control", Value: "max-age=0"},
+		{Name: "duplicate-header", Value: "1"},
+		{Name: "duplicate-header", Value: "2"},
+	}
+	req, err := requestFromHeaders(headers)
+	require.NoError(t, err)
+	require.Equal(t, http.Header{
+		"Cache-Control":    []string{"max-age=0"},
+		"Duplicate-Header": []string{"1", "2"},
+	}, req.Header)
+}
+
+func TestRequestHeadersConnect(t *testing.T) {
+	headers := []qpack.HeaderField{
+		{Name: ":authority", Value: "quic-go.net"},
+		{Name: ":method", Value: http.MethodConnect},
+	}
+	req, err := requestFromHeaders(headers)
+	require.NoError(t, err)
+	require.Equal(t, http.MethodConnect, req.Method)
+	require.Equal(t, "HTTP/3.0", req.Proto)
+	require.Equal(t, "quic-go.net", req.RequestURI)
+}
+
+func TestRequestHeadersConnectValidation(t *testing.T) {
+	for _, tc := range []struct {
+		name    string
+		headers []qpack.HeaderField
+		err     string
+	}{
+		{
+			name: "missing :authority",
+			headers: []qpack.HeaderField{
 				{Name: ":method", Value: http.MethodConnect},
-				{Name: ":authority", Value: "quic.clemente.io"},
+			},
+			err: ":path must be empty and :authority must not be empty",
+		},
+		{
+			name: ":path set",
+			headers: []qpack.HeaderField{
 				{Name: ":path", Value: "/foo"},
-			}
-			_, err := requestFromHeaders(headers)
-			Expect(err).To(MatchError("extended CONNECT: :scheme, :path and :authority must not be empty"))
-		})
-	})
-
-	Context("extracting the hostname from a request", func() {
-		var url *url.URL
-
-		BeforeEach(func() {
-			var err error
-			url, err = url.Parse("https://quic.clemente.io:1337")
-			Expect(err).ToNot(HaveOccurred())
-		})
-
-		It("uses URL.Host", func() {
-			Expect(hostnameFromURL(url)).To(Equal("quic.clemente.io:1337"))
+				{Name: ":method", Value: http.MethodConnect},
+			},
+			err: ":path must be empty and :authority must not be empty",
+		},
+	} {
+		t.Run(tc.name, func(t *testing.T) {
+			_, err := requestFromHeaders(tc.headers)
+			require.EqualError(t, err, tc.err)
 		})
-
-		It("returns an empty hostname if nothing is set", func() {
-			Expect(hostnameFromURL(nil)).To(BeEmpty())
+	}
+}
+
+func TestRequestHeadersExtendedConnect(t *testing.T) {
+	headers := []qpack.HeaderField{
+		{Name: ":protocol", Value: "webtransport"},
+		{Name: ":scheme", Value: "ftp"},
+		{Name: ":method", Value: http.MethodConnect},
+		{Name: ":authority", Value: "quic-go.net"},
+		{Name: ":path", Value: "/foo?val=1337"},
+	}
+	req, err := requestFromHeaders(headers)
+	require.NoError(t, err)
+	require.Equal(t, http.MethodConnect, req.Method)
+	require.Equal(t, "webtransport", req.Proto)
+	require.Equal(t, "ftp://quic-go.net/foo?val=1337", req.URL.String())
+	require.Equal(t, "1337", req.URL.Query().Get("val"))
+}
+
+func TestRequestHeadersExtendedConnectRequestValidation(t *testing.T) {
+	headers := []qpack.HeaderField{
+		{Name: ":protocol", Value: "webtransport"},
+		{Name: ":method", Value: http.MethodConnect},
+		{Name: ":authority", Value: "quic.clemente.io"},
+		{Name: ":path", Value: "/foo"},
+	}
+	_, err := requestFromHeaders(headers)
+	require.EqualError(t, err, "extended CONNECT: :scheme, :path and :authority must not be empty")
+}
+
+func TestResponseHeaderParsing(t *testing.T) {
+	headers := []qpack.HeaderField{
+		{Name: ":status", Value: "200"},
+		{Name: "content-length", Value: "42"},
+	}
+	rsp := &http.Response{}
+	require.NoError(t, updateResponseFromHeaders(rsp, headers))
+	require.Equal(t, "HTTP/3.0", rsp.Proto)
+	require.Equal(t, 3, rsp.ProtoMajor)
+	require.Zero(t, rsp.ProtoMinor)
+	require.Equal(t, int64(42), rsp.ContentLength)
+	require.Equal(t, 1, len(rsp.Header))
+	require.Equal(t, "42", rsp.Header.Get("Content-Length"))
+	require.Nil(t, rsp.Body)
+	require.Equal(t, 200, rsp.StatusCode)
+	require.Equal(t, "200 OK", rsp.Status)
+}
+
+func TestResponseHeaderParsingValidation(t *testing.T) {
+	for _, tc := range []struct {
+		name        string
+		headers     []qpack.HeaderField
+		err         string
+		errContains string
+	}{
+		{
+			name: "missing :status",
+			headers: []qpack.HeaderField{
+				{Name: "content-length", Value: "42"},
+			},
+			err: "missing :status field",
+		},
+		{
+			name: "invalid status code",
+			headers: []qpack.HeaderField{
+				{Name: ":status", Value: "foobar"},
+			},
+			errContains: "invalid status code",
+		},
+		{
+			name: ":method header field", // :method is a request pseudo header
+			headers: []qpack.HeaderField{
+				{Name: ":method", Value: http.MethodGet},
+			},
+			err: "invalid response pseudo header: :method",
+		},
+		{
+			name: "duplicate :status",
+			headers: []qpack.HeaderField{
+				{Name: ":status", Value: "200"},
+				{Name: ":status", Value: "404"},
+			},
+			err: "duplicate pseudo header: :status",
+		},
+	} {
+		t.Run(tc.name, func(t *testing.T) {
+			err := updateResponseFromHeaders(&http.Response{}, tc.headers)
+			if tc.errContains != "" {
+				require.ErrorContains(t, err, tc.errContains)
+			}
+			if tc.err != "" {
+				require.EqualError(t, err, tc.err)
+			}
 		})
-	})
-})
-
-var _ = Describe("Response", func() {
-	It("populates responses", func() {
-		headers := []qpack.HeaderField{
-			{Name: ":status", Value: "200"},
-			{Name: "content-length", Value: "42"},
-		}
-		rsp := &http.Response{}
-		err := updateResponseFromHeaders(rsp, headers)
-		Expect(err).NotTo(HaveOccurred())
-		Expect(rsp.Proto).To(Equal("HTTP/3.0"))
-		Expect(rsp.ProtoMajor).To(Equal(3))
-		Expect(rsp.ProtoMinor).To(BeZero())
-		Expect(rsp.ContentLength).To(Equal(int64(42)))
-		Expect(rsp.Header).To(HaveLen(1))
-		Expect(rsp.Header.Get("Content-Length")).To(Equal("42"))
-		Expect(rsp.Body).To(BeNil())
-		Expect(rsp.StatusCode).To(BeEquivalentTo(200))
-		Expect(rsp.Status).To(Equal("200 OK"))
-	})
-
-	It("parses trailer", func() {
-		headers := []qpack.HeaderField{
-			{Name: ":status", Value: "200"},
-			{Name: "trailer", Value: "Trailer1, Trailer2"},
-			{Name: "trailer", Value: "TRAILER3"},
-		}
-		rsp := &http.Response{}
-		err := updateResponseFromHeaders(rsp, headers)
-		Expect(err).NotTo(HaveOccurred())
-		Expect(rsp.Header).To(HaveLen(0))
-		Expect(rsp.Trailer).To(Equal(http.Header(map[string][]string{
-			"Trailer1": nil,
-			"Trailer2": nil,
-			"Trailer3": nil,
-		})))
-	})
-
-	It("rejects pseudo header fields after regular header fields", func() {
-		headers := []qpack.HeaderField{
-			{Name: "content-length", Value: "42"},
-			{Name: ":status", Value: "200"},
-		}
-		err := updateResponseFromHeaders(&http.Response{}, headers)
-		Expect(err).To(MatchError("received pseudo header :status after a regular header field"))
-	})
-
-	It("rejects response with no status field", func() {
-		headers := []qpack.HeaderField{
-			{Name: "content-length", Value: "42"},
-		}
-		err := updateResponseFromHeaders(&http.Response{}, headers)
-		Expect(err).To(MatchError("missing status field"))
-	})
-
-	It("rejects invalid status codes", func() {
-		headers := []qpack.HeaderField{
-			{Name: ":status", Value: "foobar"},
-		}
-		err := updateResponseFromHeaders(&http.Response{}, headers)
-		Expect(err).To(HaveOccurred())
-		Expect(err.Error()).To(ContainSubstring("invalid status code"))
-	})
-
-	It("rejects pseudo header fields defined for requests", func() {
-		headers := []qpack.HeaderField{
-			{Name: ":status", Value: "404"},
-			{Name: ":method", Value: "GET"},
-		}
-		err := updateResponseFromHeaders(&http.Response{}, headers)
-		Expect(err).To(MatchError("invalid response pseudo header: :method"))
-	})
-
-	DescribeTable("rejecting invalid header fields",
-		func(invalidField string) {
+	}
+
+	for _, tc := range []struct {
+		name         string
+		invalidField string
+	}{
+		{name: "connection", invalidField: "connection"},
+		{name: "keep-alive", invalidField: "keep-alive"},
+		{name: "proxy-connection", invalidField: "proxy-connection"},
+		{name: "transfer-encoding", invalidField: "transfer-encoding"},
+		{name: "upgrade", invalidField: "upgrade"},
+	} {
+		t.Run("invalid field: "+tc.name, func(t *testing.T) {
 			headers := []qpack.HeaderField{
 				{Name: ":status", Value: "404"},
-				{Name: invalidField, Value: "some-value"},
+				{Name: tc.invalidField, Value: "some-value"},
 			}
 			err := updateResponseFromHeaders(&http.Response{}, headers)
-			Expect(err).To(MatchError(fmt.Sprintf("invalid header field name: %q", invalidField)))
-		},
-		Entry("connection", "connection"),
-		Entry("keep-alive", "keep-alive"),
-		Entry("proxy-connection", "proxy-connection"),
-		Entry("transfer-encoding", "transfer-encoding"),
-		Entry("upgrade", "upgrade"),
-	)
-
-	It("rejects the TE header field, unless it is set to trailers", func() {
-		headers := []qpack.HeaderField{
-			{Name: ":status", Value: "404"},
-			{Name: "te", Value: "trailers"},
-		}
-		Expect(updateResponseFromHeaders(&http.Response{}, headers)).To(Succeed())
-		headers = []qpack.HeaderField{
-			{Name: ":status", Value: "404"},
-			{Name: "te", Value: "not-trailers"},
-		}
-		Expect(updateResponseFromHeaders(&http.Response{}, headers)).To(MatchError("invalid TE header field value: \"not-trailers\""))
-	})
-
-	It("parses trailers", func() {
-		headers := []qpack.HeaderField{
-			{Name: "content-length", Value: "42"},
-		}
-		hdr, err := parseTrailers(headers)
-		Expect(err).ToNot(HaveOccurred())
-		Expect(hdr.Get("Content-Length")).To(Equal("42"))
-	})
-
-	It("parses trailers", func() {
-		headers := []qpack.HeaderField{
-			{Name: ":status", Value: "200"},
-		}
-		_, err := parseTrailers(headers)
-		Expect(err).To(MatchError("http3: received pseudo header in trailer: :status"))
-	})
-
-	It("errors with duplicate status header in response", func() {
-		headers := []qpack.HeaderField{
-			{Name: ":status", Value: "200"},
-			{Name: ":status", Value: "400"},
-		}
-		err := updateResponseFromHeaders(&http.Response{}, headers)
-		Expect(err).To(MatchError("duplicate pseudo header: :status"))
-	})
-})
+			require.EqualError(t, err, fmt.Sprintf("invalid header field name: %q", tc.invalidField))
+		})
+	}
+}
+
+func TestResponseTrailerFields(t *testing.T) {
+	headers := []qpack.HeaderField{
+		{Name: ":status", Value: "200"},
+		{Name: "trailer", Value: "Trailer1, Trailer2"},
+		{Name: "trailer", Value: "TRAILER3"},
+	}
+	var rsp http.Response
+	require.NoError(t, updateResponseFromHeaders(&rsp, headers))
+	require.Equal(t, 0, len(rsp.Header))
+	require.Equal(t, http.Header(map[string][]string{
+		"Trailer1": nil,
+		"Trailer2": nil,
+		"Trailer3": nil,
+	}), rsp.Trailer)
+}
+
+func TestResponseTrailerParsingTE(t *testing.T) {
+	headers := []qpack.HeaderField{
+		{Name: ":status", Value: "404"},
+		{Name: "te", Value: "trailers"},
+	}
+	require.NoError(t, updateResponseFromHeaders(&http.Response{}, headers))
+	headers = []qpack.HeaderField{
+		{Name: ":status", Value: "404"},
+		{Name: "te", Value: "not-trailers"},
+	}
+	require.EqualError(t,
+		updateResponseFromHeaders(&http.Response{}, headers),
+		`invalid TE header field value: "not-trailers"`)
+}
+
+func TestResponseTrailerParsing(t *testing.T) {
+	trailerHdr, err := parseTrailers([]qpack.HeaderField{
+		{Name: "content-length", Value: "42"},
+	})
+	require.NoError(t, err)
+	require.Equal(t, "42", trailerHdr.Get("Content-Length"))
+}
+
+func TestResponseTrailerParsingValidation(t *testing.T) {
+	headers := []qpack.HeaderField{
+		{Name: ":status", Value: "200"},
+	}
+	_, err := parseTrailers(headers)
+	require.EqualError(t, err, "http3: received pseudo header in trailer: :status")
+}
diff --git a/http3/request_writer_test.go b/http3/request_writer_test.go
index 4f2af74b..5e5d7bd4 100644
--- a/http3/request_writer_test.go
+++ b/http3/request_writer_test.go
@@ -4,116 +4,116 @@ import (
 	"bytes"
 	"io"
 	"net/http"
+	"testing"
 
+	"github.com/quic-go/qpack"
 	mockquic "github.com/quic-go/quic-go/internal/mocks/quic"
 
-	"github.com/quic-go/qpack"
+	"github.com/stretchr/testify/require"
 	"go.uber.org/mock/gomock"
-
-	. "github.com/onsi/ginkgo/v2"
-	. "github.com/onsi/gomega"
 )
 
-var _ = Describe("Request Writer", func() {
-	var (
-		rw     *requestWriter
-		str    *mockquic.MockStream
-		strBuf *bytes.Buffer
-	)
-
-	decode := func(str io.Reader) map[string]string {
-		fp := frameParser{r: str}
-		frame, err := fp.ParseNext()
-		ExpectWithOffset(1, err).ToNot(HaveOccurred())
-		ExpectWithOffset(1, frame).To(BeAssignableToTypeOf(&headersFrame{}))
-		headersFrame := frame.(*headersFrame)
-		data := make([]byte, headersFrame.Length)
-		_, err = io.ReadFull(str, data)
-		ExpectWithOffset(1, err).ToNot(HaveOccurred())
-		decoder := qpack.NewDecoder(nil)
-		hfs, err := decoder.DecodeFull(data)
-		ExpectWithOffset(1, err).ToNot(HaveOccurred())
-		values := make(map[string]string)
-		for _, hf := range hfs {
-			values[hf.Name] = hf.Value
-		}
-		return values
+func decodeRequest(t *testing.T, str io.Reader) map[string]string {
+	t.Helper()
+	fp := frameParser{r: str}
+	frame, err := fp.ParseNext()
+	require.NoError(t, err)
+	require.IsType(t, &headersFrame{}, frame)
+	headersFrame := frame.(*headersFrame)
+	data := make([]byte, headersFrame.Length)
+	_, err = io.ReadFull(str, data)
+	require.NoError(t, err)
+	decoder := qpack.NewDecoder(nil)
+	hfs, err := decoder.DecodeFull(data)
+	require.NoError(t, err)
+	values := make(map[string]string)
+	for _, hf := range hfs {
+		values[hf.Name] = hf.Value
 	}
+	return values
+}
 
-	BeforeEach(func() {
-		rw = newRequestWriter()
-		strBuf = &bytes.Buffer{}
-		str = mockquic.NewMockStream(mockCtrl)
-		str.EXPECT().Write(gomock.Any()).DoAndReturn(strBuf.Write).AnyTimes()
+func TestRequestWriterGetRequestGzip(t *testing.T) {
+	t.Run("gzip", func(t *testing.T) {
+		testRequestWriterGzip(t, true)
 	})
-
-	It("writes a GET request", func() {
-		req, err := http.NewRequest(http.MethodGet, "https://quic.clemente.io/index.html?foo=bar", nil)
-		Expect(err).ToNot(HaveOccurred())
-		Expect(rw.WriteRequestHeader(str, req, false)).To(Succeed())
-		headerFields := decode(strBuf)
-		Expect(headerFields).To(HaveKeyWithValue(":authority", "quic.clemente.io"))
-		Expect(headerFields).To(HaveKeyWithValue(":method", "GET"))
-		Expect(headerFields).To(HaveKeyWithValue(":path", "/index.html?foo=bar"))
-		Expect(headerFields).To(HaveKeyWithValue(":scheme", "https"))
-		Expect(headerFields).ToNot(HaveKey("accept-encoding"))
+	t.Run("no gzip", func(t *testing.T) {
+		testRequestWriterGzip(t, false)
 	})
+}
 
-	It("rejects invalid host headers", func() {
-		req, err := http.NewRequest(http.MethodGet, "https://quic.clemente.io/index.html?foo=bar", nil)
-		Expect(err).ToNot(HaveOccurred())
-		req.Host = "foo@bar" // @ is invalid
-		Expect(rw.WriteRequestHeader(str, req, false)).To(MatchError("http3: invalid Host header"))
-	})
+func testRequestWriterGzip(t *testing.T, gzip bool) {
+	req, err := http.NewRequest(http.MethodGet, "https://quic-go.net/index.html?foo=bar", nil)
+	require.NoError(t, err)
+	req.AddCookie(&http.Cookie{Name: "foo", Value: "bar"})
+	req.AddCookie(&http.Cookie{Name: "baz", Value: "lorem ipsum"})
 
-	It("sends cookies", func() {
-		req, err := http.NewRequest(http.MethodGet, "https://quic.clemente.io/", nil)
-		Expect(err).ToNot(HaveOccurred())
-		cookie1 := &http.Cookie{
-			Name:  "Cookie #1",
-			Value: "Value #1",
-		}
-		cookie2 := &http.Cookie{
-			Name:  "Cookie #2",
-			Value: "Value #2",
-		}
-		req.AddCookie(cookie1)
-		req.AddCookie(cookie2)
-		Expect(rw.WriteRequestHeader(str, req, false)).To(Succeed())
-		headerFields := decode(strBuf)
-		Expect(headerFields).To(HaveKeyWithValue("cookie", `Cookie #1="Value #1"; Cookie #2="Value #2"`))
-	})
+	rw := newRequestWriter()
+	strBuf := &bytes.Buffer{}
+	mockCtrl := gomock.NewController(t)
+	str := mockquic.NewMockStream(mockCtrl)
+	str.EXPECT().Write(gomock.Any()).DoAndReturn(strBuf.Write).AnyTimes()
+	require.NoError(t, rw.WriteRequestHeader(str, req, gzip))
+	headerFields := decodeRequest(t, strBuf)
+	require.Equal(t, "quic-go.net", headerFields[":authority"])
+	require.Equal(t, http.MethodGet, headerFields[":method"])
+	require.Equal(t, "/index.html?foo=bar", headerFields[":path"])
+	require.Equal(t, "https", headerFields[":scheme"])
+	require.Equal(t, `foo=bar; baz="lorem ipsum"`, headerFields["cookie"])
+	switch gzip {
+	case true:
+		require.Equal(t, "gzip", headerFields["accept-encoding"])
+	case false:
+		require.NotContains(t, headerFields, "accept-encoding")
+	}
+}
 
-	It("adds the header for gzip support", func() {
-		req, err := http.NewRequest(http.MethodGet, "https://quic.clemente.io/", nil)
-		Expect(err).ToNot(HaveOccurred())
-		Expect(rw.WriteRequestHeader(str, req, true)).To(Succeed())
-		headerFields := decode(strBuf)
-		Expect(headerFields).To(HaveKeyWithValue("accept-encoding", "gzip"))
-	})
+func TestRequestWriterInvalidHostHeader(t *testing.T) {
+	req, err := http.NewRequest(http.MethodGet, "https://quic-go.net/index.html?foo=bar", nil)
+	require.NoError(t, err)
+	req.Host = "foo@bar" // @ is invalid
+	rw := newRequestWriter()
+	strBuf := &bytes.Buffer{}
+	mockCtrl := gomock.NewController(t)
+	str := mockquic.NewMockStream(mockCtrl)
+	str.EXPECT().Write(gomock.Any()).DoAndReturn(strBuf.Write).AnyTimes()
+	require.EqualError(t,
+		rw.WriteRequestHeader(str, req, false),
+		"http3: invalid Host header",
+	)
+}
 
-	It("writes a CONNECT request", func() {
-		req, err := http.NewRequest(http.MethodConnect, "https://quic.clemente.io/", nil)
-		Expect(err).ToNot(HaveOccurred())
-		Expect(rw.WriteRequestHeader(str, req, false)).To(Succeed())
-		headerFields := decode(strBuf)
-		Expect(headerFields).To(HaveKeyWithValue(":method", "CONNECT"))
-		Expect(headerFields).To(HaveKeyWithValue(":authority", "quic.clemente.io"))
-		Expect(headerFields).ToNot(HaveKey(":path"))
-		Expect(headerFields).ToNot(HaveKey(":scheme"))
-		Expect(headerFields).ToNot(HaveKey(":protocol"))
-	})
+func TestRequestWriterConnect(t *testing.T) {
+	req, err := http.NewRequest(http.MethodConnect, "https://quic-go.net/", nil)
+	require.NoError(t, err)
+	rw := newRequestWriter()
+	strBuf := &bytes.Buffer{}
+	mockCtrl := gomock.NewController(t)
+	str := mockquic.NewMockStream(mockCtrl)
+	str.EXPECT().Write(gomock.Any()).DoAndReturn(strBuf.Write).AnyTimes()
+	require.NoError(t, rw.WriteRequestHeader(str, req, false))
+	headerFields := decodeRequest(t, strBuf)
+	require.Equal(t, http.MethodConnect, headerFields[":method"])
+	require.Equal(t, "quic-go.net", headerFields[":authority"])
+	require.NotContains(t, headerFields, ":path")
+	require.NotContains(t, headerFields, ":scheme")
+	require.NotContains(t, headerFields, ":protocol")
+}
 
-	It("writes an Extended CONNECT request", func() {
-		req, err := http.NewRequest(http.MethodConnect, "https://quic.clemente.io/foobar", nil)
-		Expect(err).ToNot(HaveOccurred())
-		req.Proto = "webtransport"
-		Expect(rw.WriteRequestHeader(str, req, false)).To(Succeed())
-		headerFields := decode(strBuf)
-		Expect(headerFields).To(HaveKeyWithValue(":authority", "quic.clemente.io"))
-		Expect(headerFields).To(HaveKeyWithValue(":method", "CONNECT"))
-		Expect(headerFields).To(HaveKeyWithValue(":path", "/foobar"))
-		Expect(headerFields).To(HaveKeyWithValue(":scheme", "https"))
-		Expect(headerFields).To(HaveKeyWithValue(":protocol", "webtransport"))
-	})
-})
+func TestRequestWriterExtendedConnect(t *testing.T) {
+	req, err := http.NewRequest(http.MethodConnect, "https://quic-go.net/", nil)
+	require.NoError(t, err)
+	req.Proto = "webtransport"
+	rw := newRequestWriter()
+	strBuf := &bytes.Buffer{}
+	mockCtrl := gomock.NewController(t)
+	str := mockquic.NewMockStream(mockCtrl)
+	str.EXPECT().Write(gomock.Any()).DoAndReturn(strBuf.Write).AnyTimes()
+	require.NoError(t, rw.WriteRequestHeader(str, req, false))
+	headerFields := decodeRequest(t, strBuf)
+	require.Equal(t, "quic-go.net", headerFields[":authority"])
+	require.Equal(t, http.MethodConnect, headerFields[":method"])
+	require.Equal(t, "/", headerFields[":path"])
+	require.Equal(t, "https", headerFields[":scheme"])
+	require.Equal(t, "webtransport", headerFields[":protocol"])
+}
diff --git a/http3/transport_test.go b/http3/transport_test.go
index 2ddc83cb..cc2ae6f8 100644
--- a/http3/transport_test.go
+++ b/http3/transport_test.go
@@ -15,6 +15,7 @@ import (
 	"github.com/quic-go/quic-go/internal/protocol"
 	"github.com/quic-go/quic-go/internal/qerr"
 
+	"github.com/stretchr/testify/assert"
 	"github.com/stretchr/testify/require"
 	"go.uber.org/mock/gomock"
 )
@@ -176,17 +177,16 @@ func TestTransportDialHostname(t *testing.T) {
 func TestTransportDatagrams(t *testing.T) {
 	// if the default quic.Config is used, the transport automatically enables QUIC datagrams
 	t.Run("default quic.Config", func(t *testing.T) {
-		testErr := errors.New("handshake error")
 		tr := &Transport{
 			EnableDatagrams: true,
 			Dial: func(_ context.Context, _ string, _ *tls.Config, quicConf *quic.Config) (quic.EarlyConnection, error) {
 				require.True(t, quicConf.EnableDatagrams)
-				return nil, testErr
+				return nil, assert.AnError
 			},
 		}
 		req := mustNewRequest(http.MethodGet, "https://example.com", nil)
 		_, err := tr.RoundTripOpt(req, RoundTripOpt{})
-		require.ErrorIs(t, err, testErr)
+		require.ErrorIs(t, err, assert.AnError)
 	})
 
 	// if a custom quic.Config is used, the transport just checks that QUIC datagrams are enabled
@@ -391,12 +391,11 @@ func TestTransportConnetionRedialHandshakeError(t *testing.T) {
 	close(handshakeChan)
 	conn.EXPECT().HandshakeComplete().Return(handshakeChan).AnyTimes()
 	var dialCount int
-	testErr := errors.New("handshake error")
 	tr := &Transport{
 		Dial: func(context.Context, string, *tls.Config, *quic.Config) (quic.EarlyConnection, error) {
 			dialCount++
 			if dialCount == 1 {
-				return nil, testErr
+				return nil, assert.AnError
 			}
 			return conn, nil
 		},
@@ -405,7 +404,7 @@ func TestTransportConnetionRedialHandshakeError(t *testing.T) {
 
 	req1 := mustNewRequest("GET", "https://quic-go.net/file1.html", nil)
 	_, err := tr.RoundTrip(req1)
-	require.ErrorIs(t, err, testErr)
+	require.ErrorIs(t, err, assert.AnError)
 	require.Equal(t, 1, dialCount)
 
 	req2 := mustNewRequest("GET", "https://quic-go.net/file2.html", nil)
diff --git a/integrationtests/self/close_test.go b/integrationtests/self/close_test.go
index e8e82487..94ab8e8d 100644
--- a/integrationtests/self/close_test.go
+++ b/integrationtests/self/close_test.go
@@ -3,7 +3,6 @@ package self_test
 import (
 	"context"
 	"crypto/tls"
-	"errors"
 	"net"
 	"sync/atomic"
 	"testing"
@@ -13,6 +12,7 @@ import (
 	quicproxy "github.com/quic-go/quic-go/integrationtests/tools/proxy"
 	"github.com/quic-go/quic-go/internal/protocol"
 
+	"github.com/stretchr/testify/assert"
 	"github.com/stretchr/testify/require"
 )
 
@@ -166,8 +166,7 @@ func TestTransportClose(t *testing.T) {
 		t.Setenv("QUIC_GO_DISABLE_RECEIVE_BUFFER_WARNING", "true")
 
 		bc := newBrokenConn(newUDPConnLocalhost(t))
-		testErr := errors.New("test error")
-		testTransportClose(t, bc, func() { bc.Break(testErr) }, testErr)
+		testTransportClose(t, bc, func() { bc.Break(assert.AnError) }, assert.AnError)
 	})
 }
 
diff --git a/integrationtests/self/http_test.go b/integrationtests/self/http_test.go
index 928fde71..16065dce 100644
--- a/integrationtests/self/http_test.go
+++ b/integrationtests/self/http_test.go
@@ -26,6 +26,7 @@ import (
 	"github.com/quic-go/quic-go/http3"
 	quicproxy "github.com/quic-go/quic-go/integrationtests/tools/proxy"
 
+	"github.com/stretchr/testify/assert"
 	"github.com/stretchr/testify/require"
 )
 
@@ -396,14 +397,13 @@ func TestHTTPReestablishConnectionAfterDialError(t *testing.T) {
 	port := startHTTPServer(t, mux)
 
 	var dialCounter int
-	testErr := errors.New("test error")
 	cl := http.Client{
 		Transport: &http3.Transport{
 			TLSClientConfig: getTLSClientConfig(),
 			Dial: func(ctx context.Context, addr string, tlsConf *tls.Config, conf *quic.Config) (quic.EarlyConnection, error) {
 				dialCounter++
 				if dialCounter == 1 { // make the first dial fail
-					return nil, testErr
+					return nil, assert.AnError
 				}
 				return quic.DialAddrEarly(ctx, addr, tlsConf, conf)
 			},
@@ -412,7 +412,7 @@ func TestHTTPReestablishConnectionAfterDialError(t *testing.T) {
 	defer cl.Transport.(io.Closer).Close()
 
 	_, err := cl.Get(fmt.Sprintf("https://localhost:%d/hello", port))
-	require.ErrorIs(t, err, testErr)
+	require.ErrorIs(t, err, assert.AnError)
 	resp, err := cl.Get(fmt.Sprintf("https://localhost:%d/hello", port))
 	require.NoError(t, err)
 	require.Equal(t, http.StatusOK, resp.StatusCode)
diff --git a/internal/qerr/errors_test.go b/internal/qerr/errors_test.go
index f217a48f..b300f565 100644
--- a/internal/qerr/errors_test.go
+++ b/internal/qerr/errors_test.go
@@ -8,6 +8,7 @@ import (
 
 	"github.com/quic-go/quic-go/internal/protocol"
 
+	"github.com/stretchr/testify/assert"
 	"github.com/stretchr/testify/require"
 )
 
@@ -69,16 +70,15 @@ func TestCryptoError(t *testing.T) {
 	require.True(t, errors.As(err, &myErr))
 	require.Equal(t, myError(1337), myErr)
 
-	testErr := errors.New("test error")
-	err = NewLocalCryptoError(0x42, testErr)
-	require.True(t, errors.Is(err, testErr))
+	err = NewLocalCryptoError(0x42, assert.AnError)
+	require.True(t, errors.Is(err, assert.AnError))
 	require.True(t, errors.Is(
-		NewLocalCryptoError(0x42, testErr),
-		NewLocalCryptoError(0x42, testErr),
+		NewLocalCryptoError(0x42, assert.AnError),
+		NewLocalCryptoError(0x42, assert.AnError),
 	))
 	require.False(t, errors.Is(
-		NewLocalCryptoError(0x42, testErr),
-		NewLocalCryptoError(0x43, testErr),
+		NewLocalCryptoError(0x42, assert.AnError),
+		NewLocalCryptoError(0x43, assert.AnError),
 	))
 }
 
diff --git a/logging/connection_tracer_test.go b/logging/connection_tracer_test.go
index 95229869..b240b567 100644
--- a/logging/connection_tracer_test.go
+++ b/logging/connection_tracer_test.go
@@ -1,10 +1,11 @@
 package logging_test
 
 import (
-	"errors"
 	"testing"
 
 	"github.com/quic-go/quic-go/logging"
+
+	"github.com/stretchr/testify/assert"
 	"github.com/stretchr/testify/require"
 )
 
@@ -14,8 +15,7 @@ func TestConnectionTracerMultiplexing(t *testing.T) {
 	t2 := &logging.ConnectionTracer{ClosedConnection: func(e error) { err2 = e }}
 	tracer := logging.NewMultiplexedConnectionTracer(t1, t2)
 
-	e := errors.New("test err")
-	tracer.ClosedConnection(e)
-	require.Equal(t, e, err1)
-	require.Equal(t, e, err2)
+	tracer.ClosedConnection(assert.AnError)
+	require.Equal(t, assert.AnError, err1)
+	require.Equal(t, assert.AnError, err2)
 }
diff --git a/packet_handler_map_test.go b/packet_handler_map_test.go
index 353e2bff..6d826670 100644
--- a/packet_handler_map_test.go
+++ b/packet_handler_map_test.go
@@ -10,6 +10,7 @@ import (
 	"github.com/quic-go/quic-go/internal/protocol"
 	"github.com/quic-go/quic-go/internal/utils"
 
+	"github.com/stretchr/testify/assert"
 	"github.com/stretchr/testify/require"
 )
 
@@ -146,7 +147,6 @@ func TestPacketHandlerMapReplaceWithRemoteClosed(t *testing.T) {
 
 func TestPacketHandlerMapClose(t *testing.T) {
 	m := newPacketHandlerMap(nil, utils.DefaultLogger)
-	testErr := errors.New("shutdown")
 	const numConns = 10
 	destroyChan := make(chan error, 2*numConns)
 	for i := 0; i < numConns; i++ {
@@ -155,14 +155,14 @@ func TestPacketHandlerMapClose(t *testing.T) {
 		rand.Read(b)
 		m.Add(protocol.ParseConnectionID(b), conn)
 	}
-	m.Close(testErr)
+	m.Close(assert.AnError)
 	// check that Close can be called multiple times
 	m.Close(errors.New("close"))
 
 	for i := 0; i < numConns; i++ {
 		select {
 		case err := <-destroyChan:
-			require.Equal(t, testErr, err)
+			require.ErrorIs(t, err, assert.AnError)
 		default:
 			t.Fatalf("connection not destroyed")
 		}
diff --git a/packet_packer_test.go b/packet_packer_test.go
index c588408f..98f4f6f4 100644
--- a/packet_packer_test.go
+++ b/packet_packer_test.go
@@ -341,7 +341,7 @@ func testPackConnectionCloseCoalesced(t *testing.T, pers protocol.Perspective) {
 	}
 	p, err := tp.packer.PackApplicationClose(&qerr.ApplicationError{
 		ErrorCode:    0x1337,
-		ErrorMessage: "test error",
+		ErrorMessage: "foobar",
 	}, maxPacketSize, protocol.Version1)
 	require.NoError(t, err)
 	switch pers {
@@ -388,7 +388,7 @@ func testPackConnectionCloseCoalesced(t *testing.T, pers protocol.Perspective) {
 	}
 	require.True(t, ccf.IsApplicationError)
 	require.Equal(t, uint64(0x1337), ccf.ErrorCode)
-	require.Equal(t, "test error", ccf.ReasonPhrase)
+	require.Equal(t, "foobar", ccf.ReasonPhrase)
 
 	// the client needs to pad this packet to the max packet size
 	switch pers {
@@ -434,7 +434,7 @@ func TestPackConnectionClose1RTT(t *testing.T) {
 	// expect no framer.PopStreamFrames
 	p, err := tp.packer.PackConnectionClose(&qerr.TransportError{
 		ErrorCode:    qerr.CryptoBufferExceeded,
-		ErrorMessage: "test error",
+		ErrorMessage: "foo",
 	}, protocol.MaxByteCount, protocol.Version1)
 	require.NoError(t, err)
 	require.Empty(t, p.longHdrPackets)
@@ -443,7 +443,7 @@ func TestPackConnectionClose1RTT(t *testing.T) {
 	ccf := p.shortHdrPacket.Frames[0].Frame.(*wire.ConnectionCloseFrame)
 	require.False(t, ccf.IsApplicationError)
 	require.Equal(t, uint64(qerr.CryptoBufferExceeded), ccf.ErrorCode)
-	require.Equal(t, "test error", ccf.ReasonPhrase)
+	require.Equal(t, "foo", ccf.ReasonPhrase)
 }
 
 func TestPack1RTTPacketNothingToSend(t *testing.T) {
diff --git a/receive_stream_test.go b/receive_stream_test.go
index a2ef35ec..2b9f38d2 100644
--- a/receive_stream_test.go
+++ b/receive_stream_test.go
@@ -1,7 +1,6 @@
 package quic
 
 import (
-	"errors"
 	"fmt"
 	"io"
 	"net"
@@ -14,6 +13,7 @@ import (
 	"github.com/quic-go/quic-go/internal/protocol"
 	"github.com/quic-go/quic-go/internal/wire"
 
+	"github.com/stretchr/testify/assert"
 	"github.com/stretchr/testify/require"
 	"go.uber.org/mock/gomock"
 )
@@ -349,11 +349,10 @@ func TestReceiveStreamCloseForShutdown(t *testing.T) {
 	case <-time.After(scaleDuration(5 * time.Millisecond)): // short wait to ensure read is blocked
 	}
 
-	testErr := errors.New("test error")
-	str.closeForShutdown(testErr)
+	str.closeForShutdown(assert.AnError)
 	select {
 	case err := <-errChan:
-		require.ErrorIs(t, err, testErr)
+		require.ErrorIs(t, err, assert.AnError)
 	case <-time.After(time.Second):
 		t.Fatal("read did not return after closeForShutdown")
 	}
@@ -361,19 +360,19 @@ func TestReceiveStreamCloseForShutdown(t *testing.T) {
 	// following calls to Read should return the error
 	n, err := strWithTimeout.Read([]byte{0})
 	require.Zero(t, n)
-	require.ErrorIs(t, err, testErr)
+	require.ErrorIs(t, err, assert.AnError)
 
 	// receiving a RESET_STREAM frame after closeForShutdown does nothing
 	require.NoError(t, str.handleResetStreamFrame(&wire.ResetStreamFrame{StreamID: 42, ErrorCode: 1234, FinalSize: 42}, time.Now()))
 	n, err = strWithTimeout.Read([]byte{0})
 	require.Zero(t, n)
-	require.ErrorIs(t, err, testErr)
+	require.ErrorIs(t, err, assert.AnError)
 
 	// calling CancelRead after closeForShutdown does nothing
 	str.CancelRead(1234)
 	n, err = strWithTimeout.Read([]byte{0})
 	require.Zero(t, n)
-	require.ErrorIs(t, err, testErr)
+	require.ErrorIs(t, err, assert.AnError)
 }
 
 func TestReceiveStreamCancellation(t *testing.T) {
diff --git a/send_queue_test.go b/send_queue_test.go
index 623a8149..776ba391 100644
--- a/send_queue_test.go
+++ b/send_queue_test.go
@@ -1,14 +1,14 @@
 package quic
 
 import (
-	"errors"
 	"net"
 	"testing"
 	"time"
 
 	"github.com/quic-go/quic-go/internal/protocol"
-	"github.com/stretchr/testify/require"
 
+	"github.com/stretchr/testify/assert"
+	"github.com/stretchr/testify/require"
 	"go.uber.org/mock/gomock"
 )
 
@@ -143,7 +143,7 @@ func TestSendQueueWriteError(t *testing.T) {
 	c := NewMockSendConn(mockCtrl)
 	q := newSendQueue(c)
 
-	c.EXPECT().Write(gomock.Any(), gomock.Any(), gomock.Any()).Return(errors.New("test error"))
+	c.EXPECT().Write(gomock.Any(), gomock.Any(), gomock.Any()).Return(assert.AnError)
 	q.Send(getPacketWithContents([]byte("foobar")), 6, protocol.ECNNon)
 
 	errChan := make(chan error, 1)
@@ -151,7 +151,7 @@ func TestSendQueueWriteError(t *testing.T) {
 
 	select {
 	case err := <-errChan:
-		require.EqualError(t, err, "test error")
+		require.ErrorIs(t, err, assert.AnError)
 	case <-time.After(time.Second):
 		t.Fatal("timeout")
 	}
diff --git a/send_stream_test.go b/send_stream_test.go
index 9588691e..3d0af2eb 100644
--- a/send_stream_test.go
+++ b/send_stream_test.go
@@ -17,6 +17,7 @@ import (
 	"github.com/quic-go/quic-go/internal/protocol"
 	"github.com/quic-go/quic-go/internal/wire"
 
+	"github.com/stretchr/testify/assert"
 	"github.com/stretchr/testify/require"
 	"go.uber.org/mock/gomock"
 )
@@ -508,20 +509,19 @@ func TestSendStreamCloseForShutdown(t *testing.T) {
 	case <-time.After(scaleDuration(5 * time.Millisecond)): // short wait to ensure write is blocked
 	}
 
-	testErr := errors.New("test error")
-	str.closeForShutdown(testErr)
+	str.closeForShutdown(assert.AnError)
 	require.True(t, mockCtrl.Satisfied())
 
 	select {
 	case err := <-errChan:
-		require.ErrorIs(t, err, testErr)
+		require.ErrorIs(t, err, assert.AnError)
 	case <-time.After(time.Second):
 		t.Fatal("timeout")
 	}
 
 	// future calls to Write should return the error
 	_, err := strWithTimeout.Write([]byte("foobar"))
-	require.ErrorIs(t, err, testErr)
+	require.ErrorIs(t, err, assert.AnError)
 
 	// closing the stream doesn't do anything
 	require.NoError(t, str.Close())
@@ -534,7 +534,7 @@ func TestSendStreamCloseForShutdown(t *testing.T) {
 	// canceling the stream doesn't do anything
 	str.CancelWrite(1234)
 	_, err = strWithTimeout.Write([]byte("foobar"))
-	require.ErrorIs(t, err, testErr) // error unchanged
+	require.ErrorIs(t, err, assert.AnError) // error unchanged
 }
 
 func TestSendStreamUpdateSendWindow(t *testing.T) {
diff --git a/streams_map_incoming_test.go b/streams_map_incoming_test.go
index 8e2fbcf3..5ec2a523 100644
--- a/streams_map_incoming_test.go
+++ b/streams_map_incoming_test.go
@@ -2,13 +2,14 @@ package quic
 
 import (
 	"context"
-	"errors"
 	"math/rand/v2"
 	"testing"
 	"time"
 
 	"github.com/quic-go/quic-go/internal/protocol"
 	"github.com/quic-go/quic-go/internal/wire"
+
+	"github.com/stretchr/testify/assert"
 	"github.com/stretchr/testify/require"
 )
 
@@ -218,18 +219,17 @@ func TestStreamsMapIncomingClosing(t *testing.T) {
 		errChan <- err
 	}()
 
-	testErr := errors.New("test error")
-	m.CloseWithError(testErr)
+	m.CloseWithError(assert.AnError)
 
 	// accepted streams should be closed
 	for _, str := range streams {
 		require.True(t, str.closed)
-		require.ErrorIs(t, str.closeErr, testErr)
+		require.ErrorIs(t, str.closeErr, assert.AnError)
 	}
 	// AcceptStream should return the error
 	select {
 	case err := <-errChan:
-		require.ErrorIs(t, err, testErr)
+		require.ErrorIs(t, err, assert.AnError)
 	case <-time.After(time.Second):
 		t.Fatal("timeout")
 	}
diff --git a/transport_test.go b/transport_test.go
index 8a3c9c77..98c8bcc4 100644
--- a/transport_test.go
+++ b/transport_test.go
@@ -17,6 +17,7 @@ import (
 	"github.com/quic-go/quic-go/internal/wire"
 	"github.com/quic-go/quic-go/logging"
 
+	"github.com/stretchr/testify/assert"
 	"github.com/stretchr/testify/require"
 	"go.uber.org/mock/gomock"
 )
@@ -427,7 +428,7 @@ func TestTransportNonQUICPackets(t *testing.T) {
 
 type faultySyscallConn struct{ net.PacketConn }
 
-func (c *faultySyscallConn) SyscallConn() (syscall.RawConn, error) { return nil, errors.New("mocked") }
+func (c *faultySyscallConn) SyscallConn() (syscall.RawConn, error) { return nil, assert.AnError }
 
 func TestTransportFaultySyscallConn(t *testing.T) {
 	syscallconn := &faultySyscallConn{PacketConn: newUDPConnLocalhost(t)}
@@ -435,7 +436,7 @@ func TestTransportFaultySyscallConn(t *testing.T) {
 	tr := &Transport{Conn: syscallconn}
 	_, err := tr.Listen(&tls.Config{}, nil)
 	require.Error(t, err)
-	require.ErrorContains(t, err, "mocked")
+	require.ErrorIs(t, err, assert.AnError)
 }
 
 func TestTransportSetTLSConfigServerName(t *testing.T) {
@@ -572,7 +573,7 @@ func TestTransportDialingVersionNegotiation(t *testing.T) {
 
 	conn2 := NewMockQUICConn(mockCtrl)
 	conn2.EXPECT().HandshakeComplete().Return(make(chan struct{}))
-	conn2.EXPECT().run().Return(errors.New("test done"))
+	conn2.EXPECT().run().Return(assert.AnError)
 
 	type connParams struct {
 		pn                   protocol.PacketNumber
@@ -612,7 +613,7 @@ func TestTransportDialingVersionNegotiation(t *testing.T) {
 	defer tr.Close()
 
 	_, err := tr.Dial(context.Background(), nil, &tls.Config{}, nil)
-	require.EqualError(t, err, "test done")
+	require.ErrorIs(t, err, assert.AnError)
 
 	select {
 	case params := <-connChan: