All Posts

How to Generate Unit Tests with Cursor

Writing tests is the part of software development that everyone agrees is important and nobody wants to do manually. For GTM Engineers building integrations, webhook handlers, and data pipelines, untested code isn't just a code quality issue--it's a business risk.

How to Generate Unit Tests with Cursor

Published on
February 26, 2026

Overview

Writing tests is the part of software development that everyone agrees is important and nobody wants to do manually. For GTM Engineers building integrations, webhook handlers, and data pipelines, untested code isn't just a code quality issue--it's a business risk. A broken CRM sync or a silently failing enrichment pipeline can torpedo an entire outbound campaign before anyone notices.

Cursor's AI capabilities make unit test generation dramatically faster, but speed without strategy produces brittle, meaningless tests. This guide covers the practical patterns for generating tests in Cursor that actually catch bugs--from framework-specific prompts and mocking strategies to edge case coverage and CI/CD integration. Whether you're testing webhook handlers, data transformation functions, or complex multi-tool orchestration logic, these workflows will help you ship with confidence.

Why AI-Assisted Test Generation Matters for GTM Code

GTM engineering code is uniquely difficult to test well. You're dealing with external APIs that return inconsistent data, webhook payloads that change without notice, and data transformations where a single malformed field can cascade into downstream failures. Manual test writing for this kind of code is tedious because you need to capture dozens of realistic data shapes from tools like Clay, Salesforce, and Outreach.

Cursor changes the economics here. Instead of spending 30 minutes crafting a test fixture that represents a Clay enrichment webhook payload, you can describe the shape and let Cursor generate it. Instead of manually writing assertions for every field mapping permutation, you prompt Cursor with the mapping logic and it produces comprehensive test coverage.

The key insight: AI test generation isn't about eliminating the thinking, it's about eliminating the typing. You still need to decide what to test. Cursor handles the boilerplate.

The Real Benefit

Teams that use AI-assisted test generation consistently report writing tests at 3-5x the speed of manual authoring. The bigger win is that they actually write tests for code that would otherwise ship untested--edge cases, error paths, and integration boundaries that feel too tedious to cover by hand.

Setting Up Cursor for Effective Test Generation

Before generating a single test, configure Cursor to understand your testing conventions. This upfront work prevents the most common frustration: AI-generated tests that use the wrong framework, wrong assertion style, or wrong project structure.

Configure .cursorrules for Testing

Your .cursorrules file should include testing-specific directives. Add these to your existing rules:

Rule CategoryExample ContentImpact
Test framework"Use pytest with pytest-asyncio for all tests. Use @pytest.fixture for shared setup."Cursor generates framework-correct syntax
Test structure"Tests live in /tests mirroring /src structure. Test files start with test_. Use arrange-act-assert pattern."Generated tests land in the right location
Mocking conventions"Mock external API calls using unittest.mock.patch. Never make real HTTP requests in tests."Prevents accidental API calls during testing
Assertion style"Use plain assert statements, not self.assertEqual. Include descriptive assertion messages for failures."Consistent assertion patterns across the codebase
Coverage targets"Every public function needs at least: happy path, error case, and edge case tests."Sets minimum quality bar for generated tests

Index Your Test Fixtures

If you already have sample API responses, webhook payloads, or test data files, make sure they're in your project where Cursor can index them. Create a /tests/fixtures directory with realistic data samples from your GTM tools:

  • Clay webhook payloads (both successful and malformed)
  • CRM API responses for different record types
  • Enrichment provider response shapes
  • Sequencer enrollment confirmation payloads

When Cursor can reference these fixtures, it generates tests with realistic data instead of placeholder strings like "test_email@example.com".

Test Generation Prompts That Actually Work

The quality of AI-generated tests depends entirely on your prompt quality. Here are the patterns that produce useful, maintainable tests rather than superficial coverage.

The Comprehensive Function Test Prompt

When you need full test coverage for an existing function, use this structure:

Weak prompt: "Write tests for the process_webhook function"

Strong prompt: "Write pytest tests for the process_webhook function in /src/webhooks/clay.py. This function receives Clay enrichment payloads, validates required fields (company_name, domain, enrichment_score), normalizes the domain, and returns a list of lead dicts. Cover: (1) happy path with complete data, (2) missing required fields, (3) malformed domain values, (4) empty payload, (5) payload with extra unexpected fields. Use the sample payload in /tests/fixtures/clay_webhook.json as the base data shape."

The Edge Case Expansion Prompt

When you have basic tests but need to cover edge cases:

"Look at the existing tests in test_lead_processor.py and the implementation in lead_processor.py. Generate additional test cases covering: unicode characters in company names, extremely long field values, numeric strings where strings are expected, null vs empty string vs missing key differences, and concurrent processing scenarios."

The Error Path Prompt

Error handling is where most GTM code fails in production. Prompt specifically for it:

"Generate tests for error scenarios in the salesforce_upsert function. Cover: API timeout after 30s, 429 rate limit response, 400 validation error with field-level details, 401 expired token, network connection failure, and duplicate record conflict (DUPLICATE_VALUE error code). Each test should verify that the function raises the appropriate custom exception and logs the error details."

Pro Tip: Reference Real Errors

If you have production error logs, paste a sanitized example into your prompt. "Here's a real error we saw last week: [error]. Write a test that would have caught this." This produces tests grounded in actual failure modes, not theoretical ones.

The Regression Test Prompt

After fixing a bug, prevent it from returning:

"We had a bug where leads with '+' characters in email addresses (like user+tag@company.com) were being rejected by validation. The fix is in commit abc123. Write a regression test that specifically covers plus-addressed emails, subaddressed emails, and other valid-but-unusual email formats to ensure this class of bug doesn't recur."

Framework-Specific Test Generation

Different test frameworks have different idioms. Here's how to get Cursor to generate idiomatic tests for the most common frameworks in GTM engineering stacks.

Pytest (Python)

Pytest is the standard for Python-based GTM automation. The key is guiding Cursor toward pytest fixtures and parametrize decorators rather than class-based tests.

Prompt pattern: "Generate pytest tests using fixtures for shared setup. Use @pytest.mark.parametrize for testing multiple input variations. Use tmp_path for any file operations. Mark async tests with @pytest.mark.asyncio."

Pytest FeatureWhen to Prompt For ItExample Use Case
@pytest.fixtureShared test data or setup/teardownDatabase connections, API client instances
@pytest.mark.parametrizeSame logic, multiple inputsTesting field validation across data types
@pytest.mark.asyncioAsync webhook handlersFastAPI endpoint tests
conftest.pyCross-file shared fixturesMock API clients used by multiple test files
monkeypatchEnvironment variable mockingTesting different config environments

Jest (TypeScript/JavaScript)

For Node.js-based GTM tools, Jest is the default. Cursor generates solid Jest tests when you specify the module system and mocking approach.

Prompt pattern: "Generate Jest tests using ES module imports. Use jest.mock() for external dependencies. Use beforeEach/afterEach for setup and cleanup. Use describe blocks to group related tests. Include type annotations for TypeScript test files."

Vitest

Teams using Vite-based tooling should specify Vitest explicitly, since Cursor may default to Jest syntax:

Prompt pattern: "Generate Vitest tests. Use vi.mock() instead of jest.mock(). Use vi.fn() for spy functions. Import test utilities from 'vitest' not '@jest/globals'."

Framework Comparison for GTM Use Cases

Use CaseRecommended FrameworkWhy
Python webhook handlers (FastAPI)Pytest + httpxAsync support, clean fixtures, ASGI test client
Node.js API integrationsJest or VitestNative module mocking, snapshot testing
Data transformation scriptsPytest with parametrizeEasily test dozens of input/output pairs
CLI tools and scriptsPytest with capsys/capfdCaptures stdout/stderr for output validation
React-based internal toolsVitest + Testing LibraryComponent testing with fast execution

Mocking Strategies for Integration Code

GTM code lives at the boundary between your system and external APIs. Effective mocking is the difference between tests that run in 2 seconds and tests that make 50 HTTP requests to production APIs. More importantly, it's the difference between tests that work reliably and tests that flake because an external service was temporarily slow.

API Response Mocking

The most common mocking need in GTM code is simulating API responses. Tell Cursor exactly what to mock and what the mock should return:

"Mock the Clay API client's get_table_rows method to return a fixture response. Create separate mocks for: (1) successful response with 10 rows, (2) empty table response, (3) 401 unauthorized response, (4) 500 server error, (5) timeout after 30 seconds. Each mock should be a separate test case."

Layered Mocking for Multi-Step Workflows

GTM workflows often chain multiple API calls. When testing a function that enriches a lead, scores it, and creates a CRM record, you need coordinated mocks:

"This function calls clay_client.enrich(), then scoring_service.score(), then salesforce.create_lead() in sequence. Create mocks for all three. Test: (1) all succeed, (2) enrichment fails and the function stops, (3) enrichment succeeds but scoring fails, (4) enrichment and scoring succeed but CRM creation fails. Verify that downstream calls aren't made when upstream calls fail."

Common Mistake

Cursor sometimes generates mocks that are too permissive--they accept any input and return a fixed response. Always prompt for input validation in your mocks: "The mock should verify it receives the expected arguments and raise an error if called with unexpected inputs." This catches bugs where your code passes wrong data between steps.

Database and State Mocking

For code that interacts with databases, decide between mocking the database layer or using test databases:

ApproachWhen to UsePrompt Guidance
Mock the ORM/clientUnit tests, fast feedback"Mock the SQLAlchemy session. Patch session.query to return fixture data."
SQLite in-memoryIntegration tests, schema validation"Use an in-memory SQLite database with the same schema. Set up with fixtures."
Docker test databaseFull integration tests"Assume a Postgres test database is available at TEST_DATABASE_URL."

Time and Date Mocking

GTM code frequently depends on timestamps--lead scoring windows, sequence timing, SLA calculations. Always mock time in tests:

"Use freezegun to freeze time at 2026-02-25T10:00:00Z. Test that leads created more than 24 hours ago get flagged as stale. Test that the SLA timer calculates correctly across timezone boundaries."

Systematic Edge Case Coverage

Edge cases are where GTM integrations break in production. The data coming from external systems is messy in predictable ways, and Cursor is excellent at generating tests for these patterns once you tell it what to look for.

The GTM Edge Case Checklist

Use this checklist when prompting Cursor to generate edge case tests for any data processing function:

CategoryEdge Cases to TestWhy It Matters
Missing datanull, undefined, empty string, missing key entirelyDifferent systems represent "no value" differently
String encodingUnicode characters, emoji in company names, accented charactersInternational prospects break naive string handling
Numeric boundariesZero, negative values, extremely large numbers, NaN, InfinityEnrichment scores and deal values hit these
Email formatsPlus addressing, subdomains, new TLDs, case sensitivityEmail validation is a common failure point
URL formatsWith/without protocol, trailing slashes, query params, punycode domainsDomain normalization across enrichment sources
Array handlingEmpty array, single element, duplicates, very large arraysBatch processing and list operations
ConcurrencyDuplicate webhook deliveries, out-of-order eventsWebhooks can fire multiple times

Prompt for Boundary Testing

"Generate boundary value tests for the lead_scoring function. The score_threshold parameter accepts 0-100. Test: exactly 0, exactly 100, -1, 101, 0.5 (float), None, and string '50'. The batch_size parameter accepts 1-1000. Test: 1, 1000, 0, 1001, and -1. Verify appropriate errors are raised for invalid inputs."

Property-Based Testing

For data transformation functions, prompt Cursor to generate property-based tests using Hypothesis (Python) or fast-check (JavaScript):

"Generate Hypothesis property-based tests for the normalize_domain function. Properties to verify: (1) output is always lowercase, (2) output never contains protocol prefixes, (3) output never has trailing slashes, (4) applying normalize twice gives the same result as applying it once (idempotent). Use the @given decorator with text() and url() strategies."

Property-based testing is especially powerful for field mapping and data normalization functions where the input space is enormous.

Test Maintenance Workflows

Generating tests is the easy part. Keeping them useful as your codebase evolves is the real challenge. Here are the Cursor workflows that keep your test suite healthy.

Updating Tests After Refactoring

When you refactor a function's signature or behavior, use Cursor to update the corresponding tests:

"I've refactored the enrich_lead function to accept a LeadRequest dataclass instead of separate keyword arguments. Update all tests in test_enrich_lead.py to use the new signature. Keep the same test scenarios but update the setup to construct LeadRequest objects."

Adding Tests for Bug Fixes

Every bug fix should come with a test. Use this workflow:

1
Reproduce the bug and capture the exact input that caused it
2
Prompt Cursor: "Write a failing test using this input: [paste the data]. It should fail with the current code because [describe the bug]."
3
Verify the test fails before your fix
4
Apply the fix and verify the test passes
5
Prompt Cursor: "Now generate 3-4 related test cases that cover similar variations of this bug."

Cleaning Up Redundant Tests

As coverage grows, test suites accumulate redundancy. Periodically ask Cursor to audit:

"Review the tests in test_crm_sync.py. Identify any tests that cover identical behavior (same code path, same assertions). Suggest which tests to consolidate and which provide unique coverage. Don't delete anything--just provide a summary."

Updating Test Fixtures

When an external API changes its response format, update fixtures systematically:

"The Clay API now returns 'company_domain' instead of 'domain' in enrichment responses. Update all fixtures in /tests/fixtures/clay_*.json and all tests that reference the 'domain' field. Show me every file that needs to change."

Version Your Fixtures

Keep old API response fixtures alongside new ones (e.g., clay_webhook_v1.json, clay_webhook_v2.json). Test that your code handles both versions during migration periods. External APIs rarely cut over cleanly, and you may receive both formats for days or weeks. This is critical for maintaining reliability in production.

Integrating Tests with CI/CD

Tests that only run on your local machine aren't protecting anything. Here's how to use Cursor to set up CI/CD integration that enforces test quality across your team.

Generating CI Configuration

Prompt Cursor to generate your CI config alongside your tests:

"Generate a GitHub Actions workflow that: (1) runs pytest with coverage on every PR, (2) fails if coverage drops below 80%, (3) runs tests in parallel using pytest-xdist, (4) caches pip dependencies, (5) uploads coverage reports as artifacts. Our Python version is 3.11 and we use Poetry for dependency management."

Pre-Commit Hooks for Test Quality

Generate pre-commit hooks that enforce testing discipline:

HookWhat It EnforcesHow to Prompt
Test file checkEvery new .py file in /src has a corresponding test file"Write a pre-commit hook that checks for matching test files"
Coverage gateChanged files maintain minimum coverage"Write a script that runs coverage only on changed files and fails below 75%"
Test namingTests follow naming conventions"Check that all test functions start with test_ and include the function name they test"
No skipped tests@pytest.mark.skip doesn't accumulate"Warn if more than 3 tests are marked as skipped"

Test Reporting and Visibility

Use Cursor to generate test reporting integrations:

"Generate a pytest conftest.py plugin that: (1) writes JUnit XML for CI parsing, (2) generates an HTML report with failure details, (3) logs slow tests (over 2s) as warnings, (4) tracks test execution time trends in a JSON file."

For teams building monitoring and alerting pipelines, extending this to push test metrics to your observability stack provides early warning when test quality degrades.

Common Pitfalls with AI-Generated Tests

Cursor generates plausible-looking tests, but plausible isn't the same as useful. Watch for these patterns.

Tautological Tests

The most insidious problem: tests that pass by definition. Cursor sometimes generates tests where the expected value is computed using the same logic as the implementation. The test passes, coverage increases, but nothing is actually verified.

Red flag: If your test's expected value is calculated rather than hardcoded, scrutinize it. The expected output for a data transformation should be a literal value you've independently verified, not a function call.

Over-Mocking

When everything is mocked, you're testing your mocks, not your code. Be wary of tests that mock the function under test's own dependencies so aggressively that the test only verifies mock wiring.

Fix: After Cursor generates mocked tests, ask: "Which of these tests would still pass if I deleted the function implementation and replaced it with a hardcoded return value?" Those tests need to be rewritten.

Happy Path Bias

AI models are trained on code that mostly works. Left unprompted, Cursor generates tests for success cases. You need to explicitly ask for failure testing:

"I notice the generated tests only cover successful scenarios. Add tests for: network failures, malformed responses, authentication errors, rate limiting, and partial failures in batch operations."

Snapshot Overuse

Snapshot testing (Jest's toMatchSnapshot) is tempting for complex data structures, but AI-generated snapshots are especially dangerous. The snapshot captures whatever Cursor generated, not necessarily the correct output. Review every snapshot manually before committing.

The Review Rule

Treat AI-generated tests with the same rigor as AI-generated production code. Read every assertion. Verify every expected value. Run the test, then deliberately break the code to confirm the test actually fails. A test that doesn't fail when code breaks is worse than no test at all--it provides false confidence.

Beyond Individual Test Files

The patterns in this guide work well for individual engineers testing individual functions. But GTM teams don't operate in isolation. Your webhook handler connects to a scoring function that feeds a CRM sync that triggers a sequencer enrollment. A test that validates one piece without understanding the whole chain catches local bugs but misses systemic ones.

The challenge compounds when multiple engineers work on different parts of the pipeline. One person changes the output format of an enrichment function, another person's CRM sync tests still pass because they mock the enrichment layer, and nobody catches the incompatibility until it hits production. Shared fixtures help, but they drift. Code review catches some issues, but reviewers can't hold the full context of every integration point in their head.

What teams actually need is a shared context layer that understands the relationships between systems--what data shapes flow between Clay, your scoring logic, your CRM, and your sequencer. When a field name changes upstream, every downstream consumer should know about it immediately, not when a production error fires.

This is what context platforms like Octave are built to solve. Instead of relying on each engineer to maintain accurate mocks of adjacent systems, Octave maintains a unified context graph that represents the actual state of your GTM data across tools. When you're testing a CRM sync function, the context about what enrichment data actually looks like isn't a stale fixture file--it's a live, synchronized representation. For teams running complex enrichment-to-qualification-to-sequence pipelines, this eliminates the category of integration bugs that unit tests alone can't catch.

FAQ

How much test coverage should I target for GTM integration code?

Aim for 80% coverage on your core data processing and integration logic. Don't chase 100% on configuration files or simple wrappers. The critical areas are: data transformation functions, error handling paths, and anything that writes to external systems. Use coverage reports to identify untested error paths rather than obsessing over the overall number.

Should I use Cursor to generate integration tests or just unit tests?

Both, but with different prompting strategies. For unit tests, Cursor excels because the scope is contained. For integration tests, provide more context: describe the full data flow, specify which external systems are mocked vs. real, and include the setup/teardown requirements. Integration test generation requires more review since the interactions between mocked systems can hide subtle bugs.

How do I handle flaky tests that Cursor generates?

Flaky tests usually stem from three causes: timing dependencies, shared state, and non-deterministic ordering. When a Cursor-generated test flakes, prompt: "This test fails intermittently. Identify potential sources of non-determinism: timing, shared state, random values, or ordering assumptions. Rewrite to eliminate the flakiness." Usually the fix involves adding explicit time mocking, isolating test state, or sorting results before comparison.

Can Cursor generate tests for code I didn't write?

Yes, and this is one of its strongest use cases. When inheriting untested code, point Cursor at the file and ask: "Analyze this function and generate tests based on its observable behavior. Include tests for any implicit assumptions you can identify." This is especially useful for converting ad-hoc scripts into tested, reusable workflows.

How do I test async webhook handlers effectively?

For FastAPI handlers, use httpx.AsyncClient as a test client. Prompt Cursor: "Generate async tests for this FastAPI webhook endpoint using httpx.AsyncClient. Mock the background task processing but test the HTTP layer end-to-end: request validation, response codes, and error responses." For Express handlers, use supertest. The key is testing the HTTP contract separately from the business logic.

What's the best way to test data pipelines with multiple transformation steps?

Test each transformation function in isolation with unit tests, then write a smaller set of integration tests for the full pipeline. Prompt Cursor: "Generate unit tests for each function in this pipeline: extract, transform_company, transform_contact, load_to_crm. Then generate 3 integration tests that run the full pipeline with representative data, testing the data shape at each handoff point." This catches both local bugs and interface mismatches.

Conclusion

AI-assisted test generation with Cursor isn't a shortcut around writing good tests--it's a force multiplier for engineers who know what good tests look like. The prompting patterns, framework-specific strategies, and mocking approaches in this guide give you the vocabulary to get useful output from Cursor instead of superficial coverage that provides false confidence.

Start with the fundamentals: configure your .cursorrules for testing, build up a library of realistic fixtures, and develop the habit of prompting for error paths and edge cases specifically. As those patterns become muscle memory, extend into CI/CD integration and systematic test maintenance workflows that keep your suite valuable as your codebase grows.

The teams that ship reliable GTM automation aren't the ones with the most tests--they're the ones with the right tests, covering the failure modes that actually occur in production. Cursor helps you write those tests faster. Your job is making sure they're the tests worth writing.

FAQ

Frequently Asked Questions

Still have questions? Get connected to our support team.