https://funnelstory.ai/blog/engineering/scaling-go-testing-with-contract-and-scenario-mocks Awarded as the top customer success solution by Winning by Design - 2025Learn More FunnelStory logo Solutions BY USE CASE Cut Surprise Churn Cut Surprise Churn by > 90% AI Search & Reasoning Reasoning across your Enterprise Data Triple Your CS Productivity Improve CSM to account ratio by 3X Needle Movers Time to see the road ahead Customers Pricing Resources Blog Read Product-led stories hot off the press ROI Calculator AI investment calculator Documentation The answer's you're looking for are just a click away Prompt Library Discover powerful prompts to use Renari Webinars Discover past webinars slack Join our Slack community Get the answers you are looking for in our community Sign In Start Free Trial Start Free Trial Sign In Start Free Trial Start Free Trial In this article * The Problem: Mock Drift vs. Integration Complexity * The Contract Test (Real Infrastructure) * The Database Contract (Postgres) * The API Contract (Salesforce) * You Still Need E2E Tests * Designing for Testability * Bad: Coupled Dependencies * Good: Interface-Based Design * The Scenario Test (Mocked Logic) * Logic Filtering * Error Handling * The Toolkit * Summary Scaling Go Testing with Contract and Scenario Mocks Mocks aren't the enemy; bad tests are. We scale Go testing using "Tactical Pairs": Contract Tests for truth, Scenario Mocks for logic. Preetam Jinka By Preetam Jinka Co-founder and Chief Architect Dec 17, 2025 6 min read Browsing the /r/golang subreddit, you might get the impression that "mock" is a dirty word. I was recently reading a thread on testing strategies and was surprised by the intensity of the anti-mock sentiment. The arguments were visceral: Avoid mocks like the plague. Mocks are like payday loans. Convenient. You say "expect this method to be called with args X and Y, return Z". Seems cheap. But the loan shark quickly starts charging interest. At FunnelStory, we see it differently. We've learned to love mocks--not as a shortcut, but as the only way to meaningfully build and maintain a complex application as a small team (with no dedicated QA). We manage a complex distributed system that orchestrates LLMs, syncs CRM data, and handles long-running workflows. In this environment, relying solely on "real" integration tests left us with blind spots. We couldn't reliably test what happens when an API sends a 429 Rate Limit, or when a workflow needs to pause for exactly 24 hours. More importantly, we realized that the majority of our edge cases depend on specific combinations of integrations that exist in a customer's workspace--states that are impossible to reproduce or recreate in our own environments. This reality drove us to define a new goal: 100% meaningful coverage. We don't just want green checkmarks for the "happy paths" that run when the database is online. We want to cover the "chaos paths" that cause production outages. While we are still refactoring our legacy code to fully meet this standard, this disciplined style of mocking has become indispensable to our scale. Here is the "Tactical Pair" strategy we adopted to make mocks safe, reliable, and effective. The Problem: Mock Drift vs. Integration Complexity When testing systems, you face two opposing risks: 1. Mock Drift: Mocks are static, but reality evolves. If your database schema changes but your mock doesn't, your test suite stays green while production breaks. The mock becomes a lie. 2. The Integration Trap: Testing logic that depends on external volatility (like API rate limits or long-running workflows) against real dependencies is slow, flaky, and non-deterministic. The solution isn't to pick a side; it's to sequence them. You should never mock a dependency until you have verified its behavior with a real, isolated Contract Test. The Contract Test (Real Infrastructure) A Contract Test verifies the Data Layer against real infrastructure. It does not test business logic; it tests the Round Trip. The Database Contract (Postgres) By writing data and immediately reading it back, we prove that the code speaks the correct SQL dialect and that the struct tags match the schema. // internal/store/user_repo_test.go // RUNS AGAINST: Real Postgres Container func TestUserRepo_RoundTrip(t *testing.T) { db := testdb.New(t) repo := NewPostgresUserRepo(db) // 1. Write Data (Prove the schema accepts it) err := repo.UpsertIdentity(ctx, &Identity{UserID: "u1", Token: "xoxb-123"}) require.NoError(t, err) // 2. Read Data (Prove we map it back correctly) identity, err := repo.GetIdentity(ctx, "u1") assert.NoError(t, err) assert.Equal(t, "xoxb-123", identity.Token) } The API Contract (Salesforce) For external APIs, we can't always hit the real endpoint (due to rate limits or auth). Instead, we verify our client can parse Real JSON Responses. We use httpmock with a fixture captured from a real Salesforce call. // internal/integrations/crm/sfdc_test.go // RUNS AGAINST: HttpMock with Real JSON Fixture func TestSFDC_FindAccountByDomain_ParsesResponse(t *testing.T) { // 1. Load a real, messy JSON response captured from Salesforce // This proves our struct tags match the actual API field names fixture, _ := os.ReadFile("testdata/sfdc_account_response.json") httpmock.RegisterResponder("GET", "https://na1.salesforce.com/services/data/v53.0/query", httpmock.NewBytesResponder(200, fixture), ) client := NewSFDCClient("valid-token") // 2. Execute the Client Method account, err := client.FindAccountByDomain(ctx, "acme.com") // 3. Verify Parsing assert.NoError(t, err) assert.Equal(t, "001xx000003Dgsd", account.ID) assert.Equal(t, "Acme Corp", account.Name) } Once these tests pass, we trust that GetIdentity speaks SQL and FindAccountByDomain speaks Salesforce JSON. We can now mock these contracts in higher-level tests without fear of drift. You Still Need E2E Tests It is important to note that Contract Tests are not a replacement for End-to-End (E2E) tests. A Contract Test proves that "If the API returns X, my code does Y." It does not prove that the API still returns X. If Salesforce changes their API schema tomorrow (which they ideally shouldn't, without a version change), our Contract Test will pass (because it uses a cached fixture), but production will fail. We still rely on a lightweight layer of true E2E smoke tests (running on a schedule, not on every PR) to verify that our fixtures are still valid and that authentication flows haven't changed. Once these Contract tests pass, however, we trust the boundary. We can now mock these interfaces in our complex logic tests without fear of drift. Designing for Testability You cannot simply add mocks to coupled code. The application must define dependencies via interfaces rather than concrete structs. Consider a worker that fetches a calendar event, checks a CRM, and sends a Slack notification. Bad: Coupled Dependencies type MeetingWorker struct { db *sql.DB // Concrete SQL connection nylasClient *nylas.Client // Concrete HTTP client } Good: Interface-Based Design By defining contracts, we separate infrastructure (SQL, HTTP) from business value (filtering, alerting). // internal/worker/meeting_prep.go // The Contracts type CalendarClient interface { GetUpcomingEvents(ctx context.Context, token string) ([]Event, error) } type CRMClient interface { FindAccountByDomain(ctx context.Context, domain string) (*Account, error) } type SlackNotifier interface { PostMessage(ctx context.Context, userID string, msg string) error } // The Logic type Runner struct { calendar CalendarClient crm CRMClient slack SlackNotifier } func NewRunner(cal CalendarClient, crm CRMClient, slack SlackNotifier) *Runner { return &Runner{ calendar: cal, crm: crm, slack: slack } } The Scenario Test (Mocked Logic) With trusted contracts in place, we can use mocks to test "Impossible" scenarios--edge cases that are difficult or destructive to reproduce in a real environment. Logic Filtering Testing that nothing happens is notoriously flaky in integration tests. Mocks make this deterministic. func TestProcess_Scenario_InternalMeeting(t *testing.T) { // Simulate: CRM returns no match mockCRM.On("FindAccountByDomain", ctx, "funnelstory.ai").Return(nil, nil) // Expectation: DO NOT call Slack mockSlack.AssertNotCalled(t, "PostMessage") runner.ProcessMeeting(ctx, "user_1") } Error Handling We can simulate specific downstream failures, such as a Rate Limit error, to ensure the worker re-queues the job correctly. func TestProcess_Scenario_SlackRateLimit(t *testing.T) { // Simulate: CRM Match mockCRM.On("FindAccountByDomain", ctx, "acme.com").Return(&Account{ID: "001"}, nil) // Simulate: Slack returns a specific error mockSlack.On("PostMessage", ctx, "user_1", mock.Anything). Return(errors.New("slack: rate limit_exceeded")) err := runner.ProcessMeeting(ctx, "user_1") // Verify the error bubbles up for the Job Queue to handle assert.ErrorContains(t, err, "rate limit") } The Toolkit To make this practical, we rely on a few battle-tested Go packages: * https://pkg.go.dev/github.com/stretchr/testify/mock: The industry standard for assertions and mocking (testify/mock). It allows us to generate mocks for our interfaces easily. * https://pkg.go.dev/github.com/jarcoal/httpmock: Essential for testing our API client contracts. It lets us intercept HTTP requests at the transport layer to verify URL structure and simulate JSON responses without hitting the network. * https://pkg.go.dev/github.com/DATA-DOG/go-sqlmock: Used sparingly. We prefer real Postgres containers for Contract Tests, but sqlmock is occasionally useful for testing complex transaction logic or driver-specific edge cases where a container is overkill. Summary This approach uses Contract Tests to prove the code interacts with the database and external APIs correctly, and Scenario Tests to prove the code handles business logic and errors correctly. This separation allows for high confidence in the system's behavior without the overhead of maintaining a full end-to-end test suite for every edge case. [ ]Subscribe funnelstory logo Home * * Copyright (c) 2025 FunnelStory. All Rights Reserved. Explore * Customers * Pricing Solutions * Cut Surprise Churn * AI Search & Reasoning * Triple Your CS Productivity * Needle Movers Company * About Us * Events * Press Releases * Join the Team Resources * Prompt Library * Blog * ROI Calculator * Documentation * Release Notes Legal * Terms of Service * Privacy Policy * Support * Trust Report * All systems operational * Mail us [email protected] LinkedIn tracking pixel image