Repository: getsentry/sentry-go

Issue:
Expose MockTransport in a testhelpers package
### Problem Statement

I'm integrating Sentry into an existing application, and I want to confirm my application sends expected data to Sentry. A client with a mocked transport layer fits the bill.

### Solution Brainstorm

I suggest exposing the `TransportMock`

It's even _duplicated_, for exactly the same reason I have: `TransportMock` is useful for testing a program's interaction with Sentry. The duplicated code has a `FIXME` comment from : https://github.com/getsentry/sentry-go/blob/e56ac3010f8c48bbb32839ff8a7e751f420c361f/otel/helpers_test.go#L116-L122

cc @tonyo — I think a clear docstring explaining the meaning of `TransportMock` should be clear enough to prevent someone accidentally using it.

Happy to open a PR implementing the above and removing the duplication 🙇 

Discussion:
@tonyo: I don't mind :) I'm not with Sentry anymore though, so will delegate to the maintainers.

Solution patch:
diff --git a/mocks.go b/mocks.go
new file mode 100644
index 0000000..e6345ab
--- /dev/null
+++ b/mocks.go
@@ -0,0 +1,47 @@
+package sentry
+
+import (
+	"sync"
+	"time"
+)
+
+// MockScope implements [Scope] for use in tests.
+type MockScope struct {
+	breadcrumb      *Breadcrumb
+	shouldDropEvent bool
+}
+
+func (scope *MockScope) AddBreadcrumb(breadcrumb *Breadcrumb, _ int) {
+	scope.breadcrumb = breadcrumb
+}
+
+func (scope *MockScope) ApplyToEvent(event *Event, _ *EventHint, _ *Client) *Event {
+	if scope.shouldDropEvent {
+		return nil
+	}
+	return event
+}
+
+// MockTransport implements [Transport] for use in tests.
+type MockTransport struct {
+	mu        sync.Mutex
+	events    []*Event
+	lastEvent *Event
+}
+
+func (t *MockTransport) Configure(_ ClientOptions) {}
+func (t *MockTransport) SendEvent(event *Event) {
+	t.mu.Lock()
+	defer t.mu.Unlock()
+	t.events = append(t.events, event)
+	t.lastEvent = event
+}
+func (t *MockTransport) Flush(_ time.Duration) bool {
+	return true
+}
+func (t *MockTransport) Events() []*Event {
+	t.mu.Lock()
+	defer t.mu.Unlock()
+	return t.events
+}
+func (t *MockTransport) Close() {}



Test patch:
diff --git a/client_test.go b/client_test.go
index 60d63e2..2f1ce57 100644
--- a/client_test.go
+++ b/client_test.go
@@ -17,7 +17,7 @@ import (
 )
 
 func TestNewClientAllowsEmptyDSN(t *testing.T) {
-	transport := &TransportMock{}
+	transport := &MockTransport{}
 	client, err := NewClient(ClientOptions{
 		Transport: transport,
 	})
@@ -25,7 +25,7 @@ func TestNewClientAllowsEmptyDSN(t *testing.T) {
 		t.Fatalf("expected no error when creating client without a DNS but got %v", err)
 	}
 
-	client.CaptureException(errors.New("custom error"), nil, &ScopeMock{})
+	client.CaptureException(errors.New("custom error"), nil, &MockScope{})
 	assertEqual(t, transport.lastEvent.Exception[0].Value, "custom error")
 }
 
@@ -41,9 +41,9 @@ func (e customComplexError) AnswerToLife() string {
 	return "42"
 }
 
-func setupClientTest() (*Client, *ScopeMock, *TransportMock) {
-	scope := &ScopeMock{}
-	transport := &TransportMock{}
+func setupClientTest() (*Client, *MockScope, *MockTransport) {
+	scope := &MockScope{}
+	transport := &MockTransport{}
 	client, _ := NewClient(ClientOptions{
 		Dsn:       "http://whatever@example.com/1337",
 		Transport: transport,
@@ -533,7 +533,7 @@ func TestBeforeSendGetAccessToEventHint(t *testing.T) {
 }
 
 func TestBeforeSendTransactionCanDropTransaction(t *testing.T) {
-	transport := &TransportMock{}
+	transport := &MockTransport{}
 	ctx := NewTestContext(ClientOptions{
 		EnableTracing:    true,
 		TracesSampleRate: 1.0,
@@ -559,7 +559,7 @@ func TestBeforeSendTransactionCanDropTransaction(t *testing.T) {
 }
 
 func TestBeforeSendTransactionIsCalled(t *testing.T) {
-	transport := &TransportMock{}
+	transport := &MockTransport{}
 	ctx := NewTestContext(ClientOptions{
 		EnableTracing:    true,
 		TracesSampleRate: 1.0,
@@ -621,8 +621,8 @@ func TestIgnoreErrors(t *testing.T) {
 
 	for name, tt := range tests {
 		t.Run(name, func(t *testing.T) {
-			scope := &ScopeMock{}
-			transport := &TransportMock{}
+			scope := &MockScope{}
+			transport := &MockTransport{}
 			client, err := NewClient(ClientOptions{
 				Transport:    transport,
 				IgnoreErrors: tt.ignoreErrors,
@@ -676,7 +676,7 @@ func TestIgnoreTransactions(t *testing.T) {
 
 	for name, tt := range tests {
 		t.Run(name, func(t *testing.T) {
-			transport := &TransportMock{}
+			transport := &MockTransport{}
 			ctx := NewTestContext(ClientOptions{
 				EnableTracing:      true,
 				TracesSampleRate:   1.0,
@@ -755,7 +755,7 @@ func TestSampleRate(t *testing.T) {
 func BenchmarkProcessEvent(b *testing.B) {
 	c, err := NewClient(ClientOptions{
 		SampleRate: 0.25,
-		Transport:  &TransportMock{},
+		Transport:  &MockTransport{},
 	})
 	if err != nil {
 		b.Fatal(err)
@@ -856,7 +856,7 @@ func TestCustomMaxSpansProperty(t *testing.T) {
 
 	properClient, _ := NewClient(ClientOptions{
 		MaxSpans:  3000,
-		Transport: &TransportMock{},
+		Transport: &MockTransport{},
 	})
 
 	assertEqual(t, properClient.Options().MaxSpans, 3000)
diff --git a/hub_test.go b/hub_test.go
index 93381d2..97d0127 100644
--- a/hub_test.go
+++ b/hub_test.go
@@ -15,7 +15,7 @@ import (
 const testDsn = "http://whatever@example.com/1337"
 
 func setupHubTest() (*Hub, *Client, *Scope) {
-	client, _ := NewClient(ClientOptions{Dsn: testDsn, Transport: &TransportMock{}})
+	client, _ := NewClient(ClientOptions{Dsn: testDsn, Transport: &MockTransport{}})
 	scope := NewScope()
 	hub := NewHub(client, scope)
 	return hub, client, scope
@@ -97,7 +97,7 @@ func TestPopScopeCannotLeaveStackEmpty(t *testing.T) {
 func TestBindClient(t *testing.T) {
 	hub, client, _ := setupHubTest()
 	hub.PushScope()
-	newClient, _ := NewClient(ClientOptions{Dsn: testDsn, Transport: &TransportMock{}})
+	newClient, _ := NewClient(ClientOptions{Dsn: testDsn, Transport: &MockTransport{}})
 	hub.BindClient(newClient)
 
 	if (*hub.stack)[0].client == (*hub.stack)[1].client {
@@ -125,7 +125,7 @@ func TestWithScopeBindClient(t *testing.T) {
 	hub, client, _ := setupHubTest()
 
 	hub.WithScope(func(scope *Scope) {
-		newClient, _ := NewClient(ClientOptions{Dsn: testDsn, Transport: &TransportMock{}})
+		newClient, _ := NewClient(ClientOptions{Dsn: testDsn, Transport: &MockTransport{}})
 		hub.BindClient(newClient)
 		if hub.stackTop().client != newClient {
 			t.Error("should use newly bound client")
@@ -436,7 +436,7 @@ func TestConcurrentHubClone(t *testing.T) {
 	const goroutineCount = 3
 
 	hub, client, _ := setupHubTest()
-	transport := &TransportMock{}
+	transport := &MockTransport{}
 	client.Transport = transport
 
 	var wg sync.WaitGroup
diff --git a/integrations_test.go b/integrations_test.go
index c43cea5..138daf9 100644
--- a/integrations_test.go
+++ b/integrations_test.go
@@ -399,7 +399,7 @@ func TestExtractModules(t *testing.T) {
 }
 
 func TestEnvironmentIntegrationDoesNotOverrideExistingContexts(t *testing.T) {
-	transport := &TransportMock{}
+	transport := &MockTransport{}
 	client, err := NewClient(ClientOptions{
 		Transport: transport,
 		Integrations: func([]Integration) []Integration {
@@ -451,7 +451,7 @@ func TestGlobalTagsIntegration(t *testing.T) {
 	defer os.Unsetenv("SENTRY_TAGS_bar")
 	defer os.Unsetenv("SENTRY_TAGS_baz")
 
-	transport := &TransportMock{}
+	transport := &MockTransport{}
 	client, err := NewClient(ClientOptions{
 		Transport: transport,
 		Tags: map[string]string{
diff --git a/mocks_test.go b/mocks_test.go
deleted file mode 100644
index 5cc127e..0000000
--- a/mocks_test.go
+++ /dev/null
@@ -1,46 +0,0 @@
-package sentry
-
-import (
-	"sync"
-	"time"
-)
-
-type ScopeMock struct {
-	breadcrumb      *Breadcrumb
-	shouldDropEvent bool
-}
-
-func (scope *ScopeMock) AddBreadcrumb(breadcrumb *Breadcrumb, _ int) {
-	scope.breadcrumb = breadcrumb
-}
-
-func (scope *ScopeMock) ApplyToEvent(event *Event, _ *EventHint, _ *Client) *Event {
-	if scope.shouldDropEvent {
-		return nil
-	}
-	return event
-}
-
-type TransportMock struct {
-	mu        sync.Mutex
-	events    []*Event
-	lastEvent *Event
-}
-
-func (t *TransportMock) Configure(_ ClientOptions) {}
-func (t *TransportMock) SendEvent(event *Event) {
-	t.mu.Lock()
-	defer t.mu.Unlock()
-	t.events = append(t.events, event)
-	t.lastEvent = event
-}
-func (t *TransportMock) Flush(_ time.Duration) bool {
-	return true
-}
-func (t *TransportMock) Events() []*Event {
-	t.mu.Lock()
-	defer t.mu.Unlock()
-	return t.events
-}
-func (t *TransportMock) Close() {}
-
diff --git a/otel/event_processor_test.go b/otel/event_processor_test.go
index 43df64a..9cde8f5 100644
--- a/otel/event_processor_test.go
+++ b/otel/event_processor_test.go
@@ -34,7 +34,7 @@ func TestLinkTraceContextToErrorEventSetsContext(t *testing.T) {
 				hub.Scope(),
 			)
 
-			transport := client.Transport.(*TransportMock)
+			transport := client.Transport.(*sentry.MockTransport)
 			events := transport.Events()
 			assertEqual(t, len(events), 1)
 			err := events[0]
diff --git a/otel/helpers_test.go b/otel/helpers_test.go
index 227f1a8..09f648c 100644
--- a/otel/helpers_test.go
+++ b/otel/helpers_test.go
@@ -3,9 +3,7 @@ package sentryotel
 import (
 	"encoding/hex"
 	"sort"
-	"sync"
 	"testing"
-	"time"
 
 	"github.com/getsentry/sentry-go"
 	"github.com/getsentry/sentry-go/internal/otel/baggage"
@@ -112,37 +110,3 @@ func otelSpanIDFromHex(s string) trace.SpanID {
 	}
 	return spanID
 }
-
-// FIXME(anton): TransportMock is copied from mocks_test.go
-// I don't see an easy way right now to reuse this struct in "sentry" and
-// "sentryotel" packages: it naturally depends on "sentry", but tests in "sentry"
-// package also depend on it, so if we move it to a new package, we'll get an
-// import cycle.
-// Alternatively, it could be made public on "sentry" package, but it doesn't
-// feel right.
-
-type TransportMock struct {
-	mu        sync.Mutex
-	events    []*sentry.Event
-	lastEvent *sentry.Event
-}
-
-func (t *TransportMock) Configure(options sentry.ClientOptions) {}
-func (t *TransportMock) SendEvent(event *sentry.Event) {
-	t.mu.Lock()
-	defer t.mu.Unlock()
-	t.events = append(t.events, event)
-	t.lastEvent = event
-}
-func (t *TransportMock) Flush(timeout time.Duration) bool {
-	return true
-}
-func (t *TransportMock) Events() []*sentry.Event {
-	t.mu.Lock()
-	defer t.mu.Unlock()
-	return t.events
-}
-
-func (t *TransportMock) Close()  {}
-
-//
diff --git a/otel/span_processor_test.go b/otel/span_processor_test.go
index 03c9796..9d6013f 100644
--- a/otel/span_processor_test.go
+++ b/otel/span_processor_test.go
@@ -40,15 +40,15 @@ func emptyContextWithSentry() context.Context {
 		Release:          "1.2.3",
 		EnableTracing:    true,
 		TracesSampleRate: 1.0,
-		Transport:        &TransportMock{},
+		Transport:        &sentry.MockTransport{},
 	})
 	hub := sentry.NewHub(client, sentry.NewScope())
 	return sentry.SetHubOnContext(context.Background(), hub)
 }
 
-func getSentryTransportFromContext(ctx context.Context) *TransportMock {
+func getSentryTransportFromContext(ctx context.Context) *sentry.MockTransport {
 	hub := sentry.GetHubFromContext(ctx)
-	transport, ok := hub.Client().Transport.(*TransportMock)
+	transport, ok := hub.Client().Transport.(*sentry.MockTransport)
 	if !ok {
 		log.Fatal(
 			"Cannot get mock transport from context",
diff --git a/tracing_test.go b/tracing_test.go
index 519b6f3..257251a 100644
--- a/tracing_test.go
+++ b/tracing_test.go
@@ -90,7 +90,7 @@ func testMarshalJSONOmitEmptyParentSpanID(t *testing.T, v interface{}) {
 }
 
 func TestStartSpan(t *testing.T) {
-	transport := &TransportMock{}
+	transport := &MockTransport{}
 	ctx := NewTestContext(ClientOptions{
 		EnableTracing: true,
 		Transport:     transport,
@@ -172,7 +172,7 @@ func TestStartSpan(t *testing.T) {
 }
 
 func TestStartChild(t *testing.T) {
-	transport := &TransportMock{}
+	transport := &MockTransport{}
 	ctx := NewTestContext(ClientOptions{
 		EnableTracing:    true,
 		TracesSampleRate: 1.0,
@@ -239,7 +239,7 @@ func TestStartChild(t *testing.T) {
 }
 
 func TestStartTransaction(t *testing.T) {
-	transport := &TransportMock{}
+	transport := &MockTransport{}
 	ctx := NewTestContext(ClientOptions{
 		EnableTracing: true,
 		Transport:     transport,
@@ -398,7 +398,7 @@ type testContextValue struct{}
 
 func NewTestContext(options ClientOptions) context.Context {
 	if options.Transport == nil {
-		options.Transport = &TransportMock{}
+		options.Transport = &MockTransport{}
 	}
 	client, err := NewClient(options)
 	if err != nil {
@@ -637,7 +637,7 @@ func TestSpanFromContext(_ *testing.T) {
 }
 
 func TestDoubleSampling(t *testing.T) {
-	transport := &TransportMock{}
+	transport := &MockTransport{}
 	ctx := NewTestContext(ClientOptions{
 		// A SampleRate set to 0.0 will be transformed to 1.0,
 		// hence we're using math.SmallestNonzeroFloat64.
@@ -975,7 +975,7 @@ func TestAdjustingTransactionSourceBeforeSending(t *testing.T) {
 	for _, tt := range tests {
 		tt := tt
 		t.Run(tt.name, func(t *testing.T) {
-			transport := &TransportMock{}
+			transport := &MockTransport{}
 			ctx := NewTestContext(ClientOptions{
 				EnableTracing:    true,
 				TracesSampleRate: 1.0,
@@ -1022,7 +1022,7 @@ func TestSpanFinishConcurrentlyWithoutRaces(_ *testing.T) {
 
 func TestSpanScopeManagement(t *testing.T) {
 	// Initialize a test hub and client
-	transport := &TransportMock{}
+	transport := &MockTransport{}
 	client, err := NewClient(ClientOptions{
 		EnableTracing:    true,
 		TracesSampleRate: 1.0,