# Course: Testing Sanity Studio
https://www.sanity.io/learn/course/testing-sanity-studio

Learn to balance test coverage with development velocity while protecting the critical operations that power your Content Operating System. Establish automated testing strategies, document business requirements in executable code, and enable confident iteration. From validation logic to React components, develop an intentional testing strategy that ensures reliability and enables you to ship Studio changes with confidence.

---

## Navigation

## Contents

1. [Why Testing Matters for Studio Development](https://www.sanity.io/learn/course/testing-sanity-studio/why-testing-matters-for-studio-development) · [markdown](https://www.sanity.io/learn/course/testing-sanity-studio/why-testing-matters-for-studio-development.md)
2. [Setting Up Your Testing Environment](https://www.sanity.io/learn/course/testing-sanity-studio/setting-up-your-testing-environment) · [markdown](https://www.sanity.io/learn/course/testing-sanity-studio/setting-up-your-testing-environment.md)
3. [Testing Validation and Access Control](https://www.sanity.io/learn/course/testing-sanity-studio/testing-validation-and-access-control) · [markdown](https://www.sanity.io/learn/course/testing-sanity-studio/testing-validation-and-access-control.md)
4. [Testing Stateful Studio Logic](https://www.sanity.io/learn/course/testing-sanity-studio/testing-stateful-studio-logic) · [markdown](https://www.sanity.io/learn/course/testing-sanity-studio/testing-stateful-studio-logic.md)
5. [Testing Studio React Components](https://www.sanity.io/learn/course/testing-sanity-studio/testing-studio-react-component) · [markdown](https://www.sanity.io/learn/course/testing-sanity-studio/testing-studio-react-component.md)
6. [Continuous Integration and Test Strategy](https://www.sanity.io/learn/course/testing-sanity-studio/continuous-integration-and-test-strategy) · [markdown](https://www.sanity.io/learn/course/testing-sanity-studio/continuous-integration-and-test-strategy.md)
7. [Tests as Content Operating System Infrastructure](https://www.sanity.io/learn/course/testing-sanity-studio/tests-as-content-operating-system-infrastructure) · [markdown](https://www.sanity.io/learn/course/testing-sanity-studio/tests-as-content-operating-system-infrastructure.md)

---

## Lesson 1: Why Testing Matters for Studio Development
https://www.sanity.io/learn/course/testing-sanity-studio/why-testing-matters-for-studio-development

Testing isn't just quality assurance—it's strategic infrastructure that protects your business logic and enables rapid iteration. Understand how tests document requirements in executable code, provide confidence before changes reach content editors, and free your team to move faster. Learn what makes Studio customizations worth testing and how to think about testing as an investment in long-term velocity.

This course takes an incremental approach to writing tests, starting with validation and access control logic, then progressing to more complex React components that interact with Studio's APIs. This building-block approach helps you establish a solid foundation of test coverage that grows with your Studio's complexity.



## What you'll build



By the end of this course, you'll have a complete test suite for a real Sanity Studio that manages events, artists, and venues. You'll test:



- **Validation and access control logic** - Permission checks and business rules

- **Validation logic** - Custom schema validation rules

- **React components** - Custom input components with Sanity hooks

- **Integration workflows** - CI/CD pipelines that run tests automatically


Upon completing this course, you'll be able to:



- Set up Vitest in a Sanity Studio project

- Write tests for validation and access control functions

- Test async validation functions with mocked clients

- Create reusable test fixtures for consistent mocking

- Test React components that use Sanity UI and Studio APIs

- Implement automated testing in GitHub Actions

- Make strategic decisions about what to test and when


## The strategic case for testing



Testing is an investment. It takes time to write tests, configure testing tools, and maintain test suites. But this upfront investment pays dividends:



**For solo developers:**



- Catch bugs before they reach production

- Refactor with confidence

- Document your intent for future you


**For teams:**



- Enable multiple developers to work simultaneously without breaking each other's code

- Onboard new team members faster with executable documentation

- Review pull requests more efficiently when tests verify behavior


**For AI-assisted development:**



- Tests provide clear specifications of expected behavior

- AI agents can understand your business logic through test descriptions

- Automated refactoring becomes safer when tests guard against regressions


When code becomes commodity through AI assistance, your specifications—documented in tests—become your competitive advantage.



---

## Lesson 2: Setting Up Your Testing Environment
https://www.sanity.io/learn/course/testing-sanity-studio/setting-up-your-testing-environment

Configure Vitest as your testing framework and integrate it into your Studio development workflow. Set up test environments for both monorepo and single-app configurations, understand how testing fits into your build process, and write your first test. Learn the fundamentals of test structure, assertions, and organizing test files alongside the code they verify.

In this lesson, you'll set up a testing environment using Vitest and understand why testing is a strategic investment for your Sanity Studio.



## Why test your Studio?



With Sanity's code-first architecture, your Studio is configured through the code you write—custom inputs, validation functions, preview configuration, formatting helpers, and other business logic. Testing this code before it reaches Studio gives you confidence that your changes won't break the editing experience.



When you write tests for your Studio code, you're not checking that it runs—you're encoding your team's business requirements and design decisions. Tests become documentation of how your Studio should work, written in code that can verify itself.



Consider a concert venue booking system. If you write a validation rule that certain event types must have a venue, a test ensures this rule works correctly:



- When a concert event has no venue, validation fails with a helpful message

- When a livestream event has no venue, validation passes (it doesn't need one)

- When someone modifies the validation logic later, the test catches breaking changes


This is powerful when developing new features: write tests that describe the expected behavior first, then implement the code that makes them pass. Tests become executable specifications that document your business logic.



## Testing integrates into your workflow



Testing fits into your development workflow at multiple points:



- **During local development** - Watch mode provides instant feedback as you write code

- **In pull requests** - Automated CI runs validate changes before code review

- **Before deployment** - Tests ensure your changes won't disrupt content editors


By implementing a testing strategy, you can iterate on your Studio with confidence, knowing that your custom inputs, schema helpers, and validation functions are covered by tests.



## Setting up Vitest



[Vitest](https://vitest.dev) is a modern testing framework designed for TypeScript projects. It provides a fast, developer-friendly experience with instant feedback through watch mode.



### Monorepo configuration



This repository is a monorepo with multiple apps (`apps/studio`, `apps/tickets`, `apps/web`). Vitest's workspace feature lets you run tests across all apps from the root, or target individual apps.



First, install Vitest at the root level:



```sh
pnpm add -D vitest -w
```

The `-w` flag installs to the workspace root, making Vitest available to all packages.



Create a `vitest.config.ts` file at the repository root:



```typescript:vitest.config.ts
import {defineConfig}from 'vitest/config'

export default defineConfig({
  test: {
    // Automatically discover test configs in all apps
    projects: ['apps/*'],
  },
})
```

This tells Vitest to look for test configurations in each app directory. Each app can have its own specialized config.



> [!NOTE]
> Working with a standalone Studio app instead of in a monorepo? You can skip the workspace configuration and use `defineConfig()` directly in your Studio's `vitest.config.ts`.



### Studio app test configuration



Now create `apps/studio/vitest.config.ts`:



```typescript:apps/studio/vitest.config.ts
import {defineProject} from 'vitest/config'

export default defineProject({
  test: {
    name: 'studio',
    include: ['**/*.test.ts'],
    environment: 'node',
  },
})
```

Notice we use `defineProject()` instead of `defineConfig()`. This provides better type checking for workspace projects. The `name: 'studio'` is required—Vitest needs unique names for each project.



Add test scripts in the root `package.json`:



```json:package.json
{
  "scripts": {
    "test": "vitest",
  }
}
```

Now you can run tests from the root (all apps) or run individual app tests.



## Your first test



Let's write a simple test to verify your setup works. Create a test file at `apps/studio/example.test.ts`:



```typescript:apps/studio/example.test.ts
import {describe, it, expect} from 'vitest'

describe('Vitest setup', () => {
  it('runs basic assertions', () => {
    expect(2 + 2).toBe(4)
  })

  it('handles string comparisons', () => {
    const greeting = 'Hello, Sanity'
    expect(greeting).toContain('Sanity')
  })

  it('validates arrays', () => {
    const events = ['concert', 'livestream', 'exhibition']
    expect(events).toHaveLength(3)
    expect(events).toContain('concert')
  })
})
```

This test file demonstrates the basic structure:



- **`describe`** - Groups related tests together (think of it as a container)

- **`it`** - Defines an individual test case (read it as "it should...")

- **`expect`** - Makes assertions about values (this is where actual testing happens)


Run the tests. Since we're in a monorepo, you have multiple options:



```sh
# From the repository root - runs all tests in all apps
pnpm test

# From the root - run only Studio tests
pnpm test --project=studio
```

For now, the simplest approach is to run from the root with `pnpm test`.



- [ ] Create the `apps/studio/example.test.ts` file with the code above and run `pnpm test` to verify your setup works.


You should see output indicating all three tests passed. Vitest enters watch mode, waiting for file changes to rerun tests automatically.



### The arrange-act-assert pattern



Each test follows a three-step pattern:



1. **Arrange** - Set up the data and conditions

2. **Act** - Execute the code being tested

3. **Assert** - Verify the result matches expectations

> [!TIP]
> The arrange-act-assert pattern works great for [Test-Driven Development (TDD)](https://en.wikipedia.org/wiki/Test-driven_development). Write the test first (it fails), implement the code (it passes), then refactor with confidence.



Here's an example with the event domain:



```typescript
describe('Event type classification', () => {
  it('identifies concerts as venue-required events', () => {
    // Arrange
    const eventType = 'concert'
    const venueRequiredTypes = ['concert', 'exhibition']

    // Act
    const requiresVenue = venueRequiredTypes.includes(eventType)

    // Assert
    expect(requiresVenue).toBe(true)
  })
})
```

This pattern keeps tests readable and maintainable. When a test fails, you can quickly identify which stage failed.



## What to test (and what not to test)



Not everything needs a test. Focus on code that contains business logic or could break in surprising ways:



**Write tests for:**



- Validation functions that enforce business rules

- Helper functions that transform or format data

- Custom input components with complex interactions

- Preview configurations that shape how content appears


**Don't test:**



- Simple field definitions with no logic

- Third-party library code (assume it's tested)

- Trivial getters and setters with no transformation


For this events Studio, high-value tests would cover:



- Validation: "Concert events must have a venue, livestream events don't"

- Date logic: "Doors open time calculates correctly from event date"

- URL validation: "Ticket URLs must be valid HTTPS URLs"


## Watch mode: your testing companion



Leave `pnpm test` running while you develop. Vitest watches your files and automatically reruns affected tests when you save changes.



Try modifying your test file—change an assertion to make it fail:



```typescript
expect(2 + 2).toBe(5)
```

Vitest immediately detects the change and shows you the failure. Change it back to `4` and the tests pass again. This instant feedback loop helps you catch errors early and iterate quickly.



- [ ] With watch mode running, modify the test to make it fail, then fix it. Observe how Vitest automatically reruns tests on file changes.


## Next steps



You now have a working test environment and understand why testing is a strategic investment for Studio development. You've set up Vitest in a monorepo configuration with workspace projects, learned the basic structure of tests with `describe`, `it`, and `expect`, and discovered how watch mode provides instant feedback as you develop. You also know what types of code are worth testing—validation functions, helper utilities, and custom components with business logic—versus what to skip, like simple field definitions with no logic. In the next lesson, you'll test validation logic and access control rules from the events Studio, building confidence with real-world examples.



- [ ] Think about one validation function or helper in your own Studio that would benefit from testing. You'll write tests for it as you progress through this course.


In the next lesson, you'll test validation logic and access control rules from the events Studio, building confidence with real-world examples.  




---

## Lesson 3: Testing Validation and Access Control
https://www.sanity.io/learn/course/testing-sanity-studio/testing-validation-and-access-control

Start with the simplest testing scenario: functions with no external dependencies or side effects. Test access control logic that determines who can edit fields, validation functions that enforce business rules, and utility functions that transform data. These isolated functions are straightforward to test and often contain critical business logic that protects content quality across your organization.

In this lesson, you'll test validation logic and access control rules from the events Studio—functions with no external dependencies that are straightforward to test.



## Why start with isolated functions?



A "pure" function is predictable and isolated:



- **Same inputs always produce same outputs** - No randomness or hidden state

- **No side effects** - Doesn't modify external state, make API calls, or change files

- **Easy to test** - Pass inputs, verify outputs, done


Functions with no external dependencies are the easiest place to start testing because they require no mocks, no setup, and no teardown. The function is completely self-contained.



## Testing validation and access control



The Studio includes access control logic that determines who can edit certain fields. The slug field has a rule: anyone can set the initial slug, but only administrators can change it once set:



```typescript:apps/studio/helpers.ts
import type {CurrentUser} from 'sanity'

/**
 * Determines if the current user can edit a slug field
 * Only administrators can edit existing slugs
 */
export function canEditSlug(user?: Omit<CurrentUser, 'role'> | null): boolean {
  return user?.roles.some((role) => role.name === 'administrator') ?? false
}

```

> [!TIP]
> Learn more about Sanity's role-based access control in  [Users, roles and using roles](https://www.sanity.io/learn/course/introduction-to-users-and-roles)



This function is pure: given a user object, it returns whether they have admin privileges. No API calls, no state changes, no external dependencies.



To ensure this pure helper function matches our business logic, we should test the following scenarios:



1. **Admin user** - Has administrator role (should return `true`)

2. **Regular user** - Has non-admin role like editor (should return `false`)

3. **Multiple roles** - User with both editor and admin roles (should return `true`)

4. **Null user** - No user logged in (should return `false`)

5. **Empty roles** - User exists but has no roles assigned (should return `false`)


```typescript:apps/studio/helpers.test.ts
import {describe, it, expect} from 'vitest'
import type {CurrentUser} from 'sanity'

import {canEditSlug} from './helpers'

describe('canEditSlug', () => {
  it('allows administrators to edit slugs', () => {
    const adminUser: Omit<CurrentUser, 'role'> = {
      id: 'admin-user',
      name: 'Admin User',
      email: 'admin@example.com',
      roles: [{name: 'administrator', title: 'Administrator'}],
    }

    expect(canEditSlug(adminUser)).toBe(true)
  })

  it('prevents non-admin users from editing slugs', () => {
    const regularUser: Omit<CurrentUser, 'role'> = {
      id: 'regular-user',
      name: 'Regular User',
      email: 'user@example.com',
      roles: [{name: 'editor', title: 'Editor'}],
    }

    expect(canEditSlug(regularUser)).toBe(false)
  })

  it('handles users with multiple roles', () => {
    const multiRoleUser: Omit<CurrentUser, 'role'> = {
      id: 'multirole-user',
      name: 'Multi-role User',
      email: 'multi@example.com',
      roles: [
        {name: 'editor', title: 'Editor'},
        {name: 'administrator', title: 'Administrator'},
      ],
    }

    expect(canEditSlug(multiRoleUser)).toBe(true)
  })

  it('prevents access when user is `null`', () => {
    expect(canEditSlug(null)).toBe(false)
  })

  it('prevents access when user has no roles', () => {
    const userWithoutRoles: Omit<CurrentUser, 'role'> = {
      id: 'no-roles',
      name: 'No Roles',
      email: 'noroles@example.com',
      roles: [],
    }

    expect(canEditSlug(userWithoutRoles)).toBe(false)
  })
})

```

Run the tests from the root with `pnpm test`. All tests should pass.



This approach ensures the permission check works correctly for all possible user states, protecting your content from unauthorized edits.



## Testing validation and access control



Another example might be the IANA tags that you might use to localize your content:



```typescript:apps/studio/validation.ts
import type {StringRule} from 'sanity'

/**
 * IANA language tag pattern (BCP 47)
 * @see https://en.wikipedia.org/wiki/IETF_language_tag
 *
 * Supports formats like:
 * - en (2-letter language code)
 * - en-US (language + region)
 * - zh-Hant-TW (language + script + region)
 * - en-US-x-private (with private use extensions)
 */
export const LANGUAGE_TAG_PATTERN =
  /^[a-z]{2,3}(?:-[A-Z][a-z]{3})?(?:-(?:[A-Z]{2}|\d{3}))?(?:-[a-zA-Z0-9]{5,8}|-[0-9][a-zA-Z0-9]{3})*$/

/**
 * Validation function for IANA language tags (BCP 47 format)
 *
 * @example
 * ```ts
 * defineField({
 *   name: 'language',
 *   type: 'string',
 *   validation: validateLanguageTag
 * })
 * ```
 */
export const validateLanguageTag = (rule: StringRule): StringRule =>
  rule.regex(LANGUAGE_TAG_PATTERN, {
    name: 'IANA language tag',
  })

```

> [!TIP]
> This example uses [TSDoc](https://typedoc.org/)-style comments to annotate and provide at-a-glance, inline documentation when hovering over definitions.



Our test suite for this validation might look something like:



```typescript:apps/studio/validation.test.ts
import {describe, it, expect} from 'vitest'
import {LANGUAGE_TAG_PATTERN} from './validation'

describe('LANGUAGE_TAG_PATTERN', () => {
  it('matches valid language tags (real-world examples)', () => {
    // Simple language codes (most common)
    expect(LANGUAGE_TAG_PATTERN.test('en')).toBe(true) // English
    expect(LANGUAGE_TAG_PATTERN.test('fr')).toBe(true) // French
    expect(LANGUAGE_TAG_PATTERN.test('ja')).toBe(true) // Japanese
    expect(LANGUAGE_TAG_PATTERN.test('nan')).toBe(true) // Min Nan Chinese (3-letter ISO 639-3)

    // Language + Region (localization)
    expect(LANGUAGE_TAG_PATTERN.test('en-US')).toBe(true) // US English
    expect(LANGUAGE_TAG_PATTERN.test('en-GB')).toBe(true) // British English
    expect(LANGUAGE_TAG_PATTERN.test('fr-CA')).toBe(true) // Canadian French
    expect(LANGUAGE_TAG_PATTERN.test('es-419')).toBe(true) // Latin American Spanish (UN M.49 numeric code)
  })

  it('rejects common mistakes', () => {
    // Case errors (most common mistake)
    expect(LANGUAGE_TAG_PATTERN.test('EN')).toBe(false) // Language must be lowercase
    expect(LANGUAGE_TAG_PATTERN.test('en-us')).toBe(false) // Region must be uppercase (en-US)
    expect(LANGUAGE_TAG_PATTERN.test('zh-hant')).toBe(false) // Script must be Title Case (zh-Hant)
    expect(LANGUAGE_TAG_PATTERN.test('zh-HANT')).toBe(false) // Script cannot be all caps

    // Wrong separator
    expect(LANGUAGE_TAG_PATTERN.test('en_US')).toBe(false) // Must use hyphen, not underscore
    expect(LANGUAGE_TAG_PATTERN.test('en.US')).toBe(false) // Must use hyphen, not period

    // Using full names instead of codes
    expect(LANGUAGE_TAG_PATTERN.test('english')).toBe(false) // Must use ISO code 'en', not full name
    expect(LANGUAGE_TAG_PATTERN.test('English')).toBe(false)

    // Wrong region code length
    expect(LANGUAGE_TAG_PATTERN.test('en-USA')).toBe(false) // Region must be 2 letters, not 3
    expect(LANGUAGE_TAG_PATTERN.test('en-U')).toBe(false) // Region cannot be 1 letter

    // Confusing region with script
    expect(LANGUAGE_TAG_PATTERN.test('zh-CN')).toBe(true) // Valid but ambiguous - prefer zh-Hans
    expect(LANGUAGE_TAG_PATTERN.test('zh-TW')).toBe(true) // Valid but ambiguous - prefer zh-Hant
  })
})

```

These functions exhibit a key trait: they're completely isolated. No API calls, no database queries, no React hooks. This makes them fast to test and easy to verify. Pure function tests require no special setup—no providers, no mocks, no configuration. This simplicity makes them an excellent starting point for your testing strategy.



More importantly though, these pure functions often form the foundation of critical business rules in your Sanity Studio. Validation functions ensure data integrity, formatting utilities maintain consistency, and helper functions encapsulate important domain logic. By thoroughly testing these functions, you're safeguarding the core rules that protect your content quality.



## Next steps



You've tested validation and access control logic—permission logic with edge cases like admin checks and null handling and IANA tag validation.



Next you'll test functions that need more context—validation rules that query Sanity's Content Lake to enforce business logic. You'll learn to mock Sanity client, create reusable fixtures, and build a testing harness for async validation logic.



---

## Lesson 4: Testing Stateful Studio Logic
https://www.sanity.io/learn/course/testing-sanity-studio/testing-stateful-studio-logic

Test validation functions that query your Content Lake to verify business rules across documents. Learn to mock the Sanity client to create controlled test scenarios, build reusable test fixtures that simplify setup, and verify async validation logic that prevents invalid content states. Understand how to test functions that depend on external data without requiring a populated dataset.

In this lesson, you'll test validation functions that need context—rules that query your Content Lake to check conditions across multiple documents.



The validation logic you tested in the previous lesson worked in isolation: it took a user as input and returned a boolean to determine access control. But some business rules require checking other documents in your dataset.



Consider validation that depends on dataset state:



- *"Only one event can be featured at a time"* (needs to check if others are featured)

- *"Artist cannot have overlapping performances"* (needs to check other event dates)


These validations need to query Content Lake. To test them, you'll mock the Sanity client and validation context, creating reusable fixtures that keep your tests clean and focused on business logic.



## Testing stateful validation functions



Event companies need to promote one event above others—the "featured" event appears on the homepage, gets social media promotion, and drives ticket sales. Only one event can be featured at a time.



This business rule needs enforcement at the data layer. If two events are featured simultaneously, the homepage breaks and marketing campaigns become confused.



```typescript:apps/studio/validation.ts
import {
  DEFAULT_STUDIO_CLIENT_OPTIONS,
  getPublishedId,
  type BooleanRule,
  type ValidationBuilder,
  type ValidationContext,
} from 'sanity'

/**
 * Checks if setting this event as featured would result in a single featured event
 * Business logic function that queries the dataset for other featured events
 */
export async function isSingleFeaturedEvent(
  value: boolean | undefined,
  context: ValidationContext,
): Promise<boolean> {
  // If not setting to featured, no need to check
  if (!value) return true

  const {getClient, document} = context

  if (!document) {
    throw new Error('Document context required for validation')
  }

  const client = getClient(DEFAULT_STUDIO_CLIENT_OPTIONS)
  const documentId = getPublishedId(document._id)

  // Query for other featured events (excluding this document's versions)
  const existingFeatured = await client.fetch<boolean>(
    `defined(*[_type == "event" && featured == true && !sanity::versionOf($documentId)][0]._id)`,
    {documentId},
    {tag: 'validation.single-featured-event', perspective: 'raw'},
  )

  // Return true if no other featured event exists
  return !existingFeatured
}

/**
 * Validation builder for the featured field
 * Ensures only one event can be featured at a time
 *
 * @example
 * ```ts
 * defineField({
 *   name: 'featured',
 *   type: 'boolean',
 *   validation: validateSingleFeaturedEvent
 * })
 * ```
 */
export const validateSingleFeaturedEvent: ValidationBuilder<BooleanRule, boolean> = (rule) =>
  rule.custom(async (value, context) => {
    if (await isSingleFeaturedEvent(value, context)) {
      return true
    }

    return 'Only one event can be featured at a time'
  })

```

There is a clean separation between the testable business logic function (`isSingleFeaturedEvent`), which returns a boolean indicating validity, and the validation builder that wraps it with the error message.



## Understanding mocking



When testing functions with external dependencies, you need a controlled environment where you can verify behavior without relying on external systems. **Mocking** creates this test "harness" by replacing real dependencies with controlled test doubles that you configure precisely for each test scenario.



A mock is a fake implementation that mimics the behavior of a real object. You control what the mock returns, letting you simulate different scenarios without needing the real dependency. Mocks also track how they're called—which methods were invoked, with what arguments, and how many times—letting you verify your code interacts with dependencies correctly.



For validation functions that query Sanity's Content Lake, you'll mock the Sanity client's `fetch()` method. Instead of running actual database queries, the mock returns predefined values you specify. This lets you test scenarios like "no featured events exist" or "another event is already featured" without populating a real dataset. The tests run in milliseconds instead of seconds, and always produce the same results regardless of what data exists in your actual Content Lake.



> [!NOTE]
> Mocks let you test business logic in isolation. You're verifying your code's behavior, not testing that Sanity's client works (we do that for you).



## Creating test fixtures



Testing our various stateful functions requires setup—mock clients, mock contexts, test data. Rather than recreate this setup in every test, you'll use **fixtures**: reusable building blocks that encapsulate common test setup patterns.



A fixture is a function that creates consistent test data or dependencies. Instead of writing the same mock setup repeatedly, you call a fixture function that handles the details. This keeps tests focused on what's unique (the scenario being tested) rather than boilerplate (how to create a mock client).



First let's create a client fixture will be reused across all validation tests, ensuring consistency and reducing repetitive code:



```typescript:apps/studio/__tests__/fixtures/client.ts
import {test as base, vi, type Mock} from 'vitest'
import type {SanityClient} from 'sanity'

type MockSanityClient = SanityClient & {
  fetch: Mock
}

/**
 * Helper function to create a mock Sanity client
 * Use this when you need a client outside of the test fixture
 *
 * @example
 * ```tsx
 * const mockClient = createMockClient()
 * mockClient.fetch.mockResolvedValue({...})
 * vi.mocked(useClient).mockReturnValue(mockClient)
 * ```
 */
export function createMockClient(): MockSanityClient {
  return {
    fetch: vi.fn(),
  } as unknown as MockSanityClient
}

/**
 * Mock Sanity client fixture
 *
 * Provides a mocked Sanity client for testing components that use useClient().
 * The client has a mocked fetch() method that can be configured per-test.
 *
 * @example
 * ```tsx
 * import {test, expect} from '@/__tests__/fixtures/client'
 *
 * test('fetches data', async ({mockClient}) => {
 *   mockClient.fetch.mockResolvedValue({_id: '123', title: 'Test'})
 *
 *   // Your test code here
 * })
 * ```
 */
export const test = base.extend<{
  mockClient: MockSanityClient
}>({
  // eslint-disable-next-line no-empty-pattern
  async mockClient({}, use) {
    // eslint-disable-next-line react-hooks/rules-of-hooks
    await use(createMockClient())
  },
})

```

> [!NOTE]
> The `mockClient` fixture will be reused across all validation tests in this course and beyond. Investing in good fixtures pays off quickly.



This fixture extends Vitest's base `test` function with a `mockClient` property. Each test automatically gets a fresh mock client, preventing tests from interfering with each other. The fixture pattern keeps test setup minimal while ensuring consistency.



Now let's test our stateful validation function using our `mockClient` fixture as well as some locally defined ones:



```typescript:apps/studio/validations.test.ts
import {describe, expect} from 'vitest'
import type {ValidationContext, ID} from 'sanity'

import {isSingleFeaturedEvent} from '../helpers'
import {it} from './__tests__/fixtures/client'

describe('isSingleFeaturedEvent', () => {
  // Local helper - creates mock event document
  const createMockEventDocument = (id: ID) => ({
    _id: id,
    _type: 'event',
    _createdAt: '2025-01-01T00:00:00Z',
    _updatedAt: '2025-01-01T00:00:00Z',
    _rev: 'mock-rev',
  })

  // Local fixture - creates validation context for featured event tests
  const createValidationContext = ({documentId, client}: {documentId: string; client: any}) =>
    ({
      getClient: () => client,
      document: createMockEventDocument(documentId),
      path: ['featured'],
    }) as unknown as ValidationContext

  it('returns `true` when no other featured event exists', async ({mockClient}) => {
    mockClient.fetch.mockResolvedValue(false) // No existing featured event

    const context = createValidationContext({documentId: 'event-1', client: mockClient})

    expect(await isSingleFeaturedEvent(true, context)).toBe(true)
  })

  it('returns `false` when another event is already featured', async ({ mockClient }) => {
    mockClient.fetch.mockResolvedValue(true) // Another event is featured

    const context = createValidationContext({documentId: 'event-2', client: mockClient})

    expect(await isSingleFeaturedEvent(true, context)).toBe(false)
  })

  it('returns true when unsetting featured (no query needed)', async ({ mockClient }) => {
    const context = createValidationContext({documentId: 'event-3', client: mockClient})

    expect(await isSingleFeaturedEvent(false, context)).toBe(true)
    // Should not query when value is false
    expect(mockClient.fetch).not.toHaveBeenCalled()
  })

  it('queries with correct parameters and excludes document versions', async ({ mockClient }) => {
    mockClient.fetch.mockResolvedValue(false)

    const documentId = getDraftId('event-4')
    const context = createValidationContext({documentId, client: mockClient})

    await isSingleFeaturedEvent(true, context)

    expect(mockClient.fetch).toHaveBeenCalledWith(
      expect.any(String),
      expect.objectContaining({documentId: getPublishedId(documentId)}), // Published ID, not draft
      expect.objectContaining({tag: 'validation.single-featured-event', perspective: 'raw'}),
    )
  })
})
```

> [!NOTE]
> Validation functions that query your Content Lake are async by nature. All your test functions will need to be asynchronous and use `await` when calling these validators.



### Understanding the test strategy



The local helper functions (`createMockEventDocument`, `createContext`) keep test setup close to the tests that use them. While `createMockClient()` is imported from fixtures (reusable across all tests), the validation context helper is specific to featured event validation—it knows about the `event` type and `featured` path.



This pattern balances reusability with specificity:



- **Global fixtures** - Broadly useful (mock clients)

- **Local helpers** - Test-suite specific (event documents, featured field context)


These four tests verify the business logic returns correct booleans:



1. **No existing featured event** → Returns `true` (can set featured)

2. **Existing featured event** → Returns `false` (cannot set featured)

3. **Unsetting featured** → Returns `true` without querying (performance)

4. **Correct query** → Verifies GROQ uses published IDs and tags


By testing the business logic function, we verify the core decision-making. The validation builder just wraps this with an error message—that's simple enough to trust without testing.



## Why this validation matters



Validation functions that query your dataset might have more moving parts than pure functions—async operations, client queries, document ID handling—but they're equally critical to test. The complexity makes them more fragile and the business impact makes them more important:



**Without this test:**



- Refactor the query → accidentally allow multiple featured events

- Change the document ID logic → validation blocks the wrong documents

- Remove the early return → unnecessary queries slow down the editor


**With this test:**



- Query changes break tests immediately

- Document ID handling is verified

- Performance optimizations are protected


This is the kind of business logic that justifies test investment. A broken featured event selector means confused marketing, broken homepage, and lost ticket sales.



## Next steps



You've learned to test validation functions that query your Content Lake to enforce business rules. By creating a reusable mock client fixture and test-specific local helpers for validation contexts, you've built a testing harness that keeps tests focused on business logic rather than setup boilerplate. You now know how to test async validation with controlled mock return values, verify that queries use correct parameters, and protect performance optimizations with assertions that functions don't query unnecessarily. These patterns work for any validation rule that accesses document state or queries your Content Lake to check conditions across multiple documents.



In the next lesson, you'll test custom input components that render UI, use Sanity hooks, and handle user interactions. You'll learn to set up a browser-like test environment and simulate real user behavior.  




---

## Lesson 5: Testing Studio React Components
https://www.sanity.io/learn/course/testing-sanity-studio/testing-studio-react-component

Test custom input components that render UI, use Studio hooks, and handle user interactions. Configure a browser-like test environment with React Testing Library, create provider fixtures that supply Studio context, and verify component behavior through simulated user actions. Learn what aspects of components are worth testing and how to balance thorough coverage with maintainable tests.

The validation logic and access control functions you've tested so far run in Node.js with no UI. Custom input components are different—they render DOM elements, depend on Sanity UI's theming context, use Studio hooks like `useFormValue` and `useClient`, and respond to user interactions. Testing these components requires a browser-like environment, provider setup for Sanity UI, and tools to simulate user behavior like clicks and typing.



Testing components verifies not just that your code runs, but that content editors can successfully interact with your custom inputs. A permission check might pass its pure function test but fail when integrated into a component's `readOnly` callback. A date calculation might work in isolation but render incorrectly when formatted for display. Component tests catch these integration issues by exercising the full user interaction flow.



## Setting up the component testing environment



Install React Testing Library and `jsdom`:



```sh
pnpm add -D @testing-library/react @testing-library/user-event jsdom @testing-library/jest-dom
```

These packages provide:



- **`jsdom`** - A browser environment implementation for Node.js

- **@testing-library/react** - Utilities for rendering and querying React components

- **@testing-library/user-event** - Functions that simulate realistic user interactions

- **@testing-library/jest-dom** - Additional matchers for DOM testing

> [!WARNING]
> `jsdom` simulates a browser environment in Node.js. It's not a real browser, so some browser-specific features (like layout calculations) won't work. For most Studio components, `jsdom` is sufficient.



## Configuring multiple test environments



Your test suite now has different needs: pure functions run in Node, React components need a browser environment.



Update `apps/studio/vitest.config.ts` to define **test-type projects** (not to be confused with workspace projects):



```typescript:apps/studio/vitest.config.ts
import {defineProject} from 'vitest/config'

export default defineProject({
  test: {
    name: 'studio',
    // Define test-type projects within the Studio workspace project
    projects: [
      {
        test: {
          name: 'unit',
          environment: 'node',
          include: ['**/*.test.ts'],
        },
      },
      {
        test: {
          name: 'component',
          environment: 'jsdom',
          include: ['**/*.test.tsx'],
          setupFiles: ['./__tests__/setup.ts'],
        },
      },
    ],
  },
})
```

> [!WARNING]
> **Important:** Vitest requires unique project names across your entire workspace. If you add other workspace projects, you may need to adjust the naming convention.



And we'll need to create the setup file for the component test suite:



```typescript:apps/studio/__tests__/setup.ts
import {cleanup} from '@testing-library/react'
import {afterEach} from 'vitest'
import '@testing-library/jest-dom/vitest'

afterEach(() => {
  cleanup()
})
```

> [!NOTE]
> Component tests are slower than unit tests because they render real DOM elements. This is why we separate them into different test projects with the `.test.tsx` extension.



This configuration creates **nested projects**:



1. **Workspace project** - `studio` (defined at root `vitest.config.ts` with `projects: ['apps/*']`)

2. **Test-type projects** - `unit` and `component` (defined within Studio's config)


The `component` project:



- Uses `jsdom` to provide DOM APIs that React needs

- Only runs files with `.test.tsx` extension (TypeScript with JSX)

- Runs a setup file before each test


## Creating provider fixtures



React components in Sanity Studio don't work in isolation—they expect certain contexts to exist. Sanity UI components read theme values from a `ThemeProvider` to render correctly (colors, spacing, typography). Without this provider, components throw errors or render incorrectly. Similarly, components that trigger toast notifications need a `ToastProvider`, and dialogs need a `LayerProvider` for z-index management.



In production, your Studio wraps everything with these providers automatically. In tests, you need to recreate this harness manually. Rather than wrap each component individually in every test, create a reusable fixture that provides all required contexts:



```tsx:apps/studio/__tests__/fixtures/providers.tsx
import {LayerProvider, ThemeProvider, ToastProvider} from '@sanity/ui'
import {defaultTheme} from 'sanity'
import {render, type RenderOptions} from '@testing-library/react'
import type {ReactElement} from 'react'

export function TestProviders({children}: {children: React.ReactNode}) {
  return (
    <ThemeProvider theme={defaultTheme}>
      <ToastProvider>
        <LayerProvider>{children}</LayerProvider>
      </ToastProvider>
    </ThemeProvider>
  )
}

export function renderWithProviders(
  ui: ReactElement,
  options?: Omit<RenderOptions, 'wrapper'>
) {
  return render(ui, {wrapper: TestProviders, ...options})
}
```

> [!NOTE]
> Start with these three providers for most Sanity UI components. Add more providers (like `FormBuilderProvider`) only when your components require them. Keep provider setup minimal.



The `TestProviders` component recreates the essential Studio provider hierarchy for these component tests:



- **`ThemeProvider`** - Provides theme values (colors, spacing, typography)

- **`ToastProvider`** - Enables toast notifications (some inputs use these)

- **`LayerProvider`** - Manages z-index stacking for dialogs and popovers


These are the providers the `DoorsOpenInput` component needs. Other custom components might require additional providers—like `FormBuilderProvider` for components that use form-level hooks, or custom context providers for specialized features. Add providers to `TestProviders` as your test suite requires them.



The `renderWithProviders()` helper wraps React Testing Library's `render()` function, automatically including these providers for every component test. This fixture becomes your test harness for Sanity UI components—call it instead of `render()` and components work as they would in production.



## Testing the `DoorsOpenInput` component



The `DoorsOpenInput` component shows when doors open based on the event date and a minutes-before value. It uses:



- `useFormValue` hook to read the event date

- `useClient` hook to access the Sanity client

- Custom logic to calculate and display the doors open time


## Testing the `DoorsOpenInput` component



The `DoorsOpenInput` component displays when doors open for an event, calculated from the event date and a minutes-before value:



```tsx:apps/studio/schemaTypes/components/DoorsOpenInput.tsx
import {getPublishedId, type NumberInputProps, useClient, useFormValue} from 'sanity'
import {Stack, Text} from '@sanity/ui'
import {useEffect} from 'react'

// ..

export function DoorsOpenInput(props: NumberInputProps) {
  const date = useFormValue(['date']) as string | undefined
  const client = useClient({apiVersion: '2025-05-08'})
  const documentId = useFormValue(['_id']) as string | undefined

  // Query for document versions (demonstration purposes)
  useEffect(() => {
    if (!documentId) return
    // ... query logic
  }, [client, documentId])

  return (
    <Stack space={3}>
      {props.renderDefault(props)}
      {typeof props.value === 'number' && date ? (
        <Text size={1}>
          Doors open{' '}
          {subtractMinutesFromDate(date, props.value).toLocaleDateString(undefined, {
            month: 'long',
            day: 'numeric',
            year: 'numeric',
            hour: 'numeric',
            minute: 'numeric',
          })}
        </Text>
      ) : null}
    </Stack>
  )
}
```

The component uses Sanity hooks (`useFormValue`, `useClient`) to access form data and the Sanity client. Testing it requires mocking these hooks to control what values the component receives:



```tsx:apps/studio/schemaTypes/components/DoorsOpenInput.test.tsx
import {describe, it, expect, vi, beforeEach} from 'vitest'
import {screen} from '@testing-library/react'
import {useFormValue, useClient} from 'sanity'

import {renderWithProviders} from '../../../__tests__/fixtures/providers'
import {DoorsOpenInput} from './DoorsOpenInput'

// Mock Sanity hooks
vi.mock('sanity', async () => {
  const actual = await vi.importActual('sanity')
  return {
    ...actual,
    useFormValue: vi.fn(),
    useClient: vi.fn(),
  }
})

describe('DoorsOpenInput', () => {
  const mockProps = {
    value: 60,
    onChange: vi.fn(),
    renderDefault: vi.fn((props) => <input type="number" {...props} />),
  } as any

  beforeEach(() => {
    vi.clearAllMocks()
  })

  it('shows doors open time when date and value exist', () => {
    vi.mocked(useFormValue).mockImplementation((path) => {
      if (path[0] === 'date') return '2025-06-15T20:00:00Z'
      if (path[0] === '_id') return 'event-1'
      return undefined
    })
    vi.mocked(useClient).mockReturnValue({} as any)

    renderWithProviders(<DoorsOpenInput {...mockProps} />)

    expect(screen.getByText(/Doors open/i)).toBeInTheDocument()
    expect(screen.getByText(/June/i)).toBeInTheDocument()
  })

  it('hides doors open time when date is missing', () => {
    vi.mocked(useFormValue).mockImplementation((path) => {
      if (path[0] === 'date') return undefined
      if (path[0] === '_id') return 'event-2'
      return undefined
    })
    vi.mocked(useClient).mockReturnValue({} as any)

    renderWithProviders(<DoorsOpenInput {...mockProps} />)

    expect(screen.queryByText(/Doors open/i)).not.toBeInTheDocument()
  })

  it('hides doors open time when value is missing', () => {
    vi.mocked(useFormValue).mockImplementation((path) => {
      if (path[0] === 'date') return '2025-06-15T20:00:00Z'
      if (path[0] === '_id') return 'event-3'
      return undefined
    })
    vi.mocked(useClient).mockReturnValue({} as any)

    const propsWithoutValue = {...mockProps, value: undefined}
    renderWithProviders(<DoorsOpenInput {...propsWithoutValue} />)

    expect(screen.queryByText(/Doors open/i)).not.toBeInTheDocument()
  })
})
```

> [!NOTE]
> Module mocks apply to the entire test file. If you need different mock behavior per test, use `mockImplementation()` in each test's setup rather than at the file level.



This test mocks the entire `sanity` module to control what `useFormValue` and `useClient` return:



1. **`vi.mock('sanity')`** - Intercepts all imports from the `sanity` package

2. **`vi.importActual()`** - Preserves original exports (types, utilities)

3. **`useFormValue: vi.fn()`** - Replaces hook with mock function

4. **`mockImplementation()`** - Controls what the hook returns based on the path argument

5. **`beforeEach()`** - Clears mocks between tests for isolation


The `mockImplementation()` function lets you return different values based on which form field is being accessed. When the component calls `useFormValue(['date'])`, the mock checks if `path[0] === 'date'` and returns the appropriate value. This pattern works for any component that reads multiple form fields through `useFormValue`.



## What to test in components



Focus on behavior that matters to content editors and enforced data integrity:



**Test:**



- Components render with expected content

- User interactions trigger correct callbacks

- Interactive components mutate documents accurately

- Accessibility attributes are present


**Don't test:**



- Implementation details (state variable names, hooks used)

- Sanity internals (we ensure test coverage for our own code)

- CSS styles and exact positioning

- Third-party library behavior (they should be testing it)


Custom inputs that modify documents deserve thorough testing. A bug in a read-only component might confuse an editor temporarily, but a bug in a component that writes data to the published document rather than the current draft would be a headache. Test these rigorously to ensure they write the correct values to the intended fields in the intended document.



- [ ] Add a test that verifies the component calls `onChange` when the user modifies the input value. Use `userEvent.type()` to simulate typing.


## Next steps



You've learned to test React components in Sanity Studio:



- Setting up `jsdom` for browser-like testing

- Creating provider fixtures for Studio context

- Rendering components with React Testing Library

- Simulating user interactions with `userEvent`

- Mocking Sanity hooks like `useFormValue` and `useClient`

- Using accessible queries to find elements


In the next lesson, you'll integrate tests into GitHub Actions, implement coverage reporting, and develop a testing strategy that scales with your Studio.



---

## Lesson 6: Continuous Integration and Test Strategy
https://www.sanity.io/learn/course/testing-sanity-studio/continuous-integration-and-test-strategy

Move tests from local development into CI pipelines that verify changes before they reach production. Configure automated test runs on pull requests, report test results directly in GitHub, and develop a strategic framework for deciding what to test. Learn to prioritize test coverage based on business impact, complexity, and change frequency—balancing protection with development velocity.

## From local development to production



You've been running tests locally with `pnpm test` in watch mode. This provides instant feedback while developing. But tests become more valuable when integrated into your workflow at key points:



- **Pull requests** - Automated checks prevent broken code from being merged

- **Before deployment** - Tests catch issues before they reach content editors

- **Scheduled runs** - Detect drift from dependencies or external changes


Running tests as part of your continuous integration (CI) ensures every code change is validated automatically, regardless of who wrote it or what they tested locally.



> [!TIP]
> [Architecture & DevOps](https://www.sanity.io/learn/course/architecture-and-devops) covers setting up schema validation, linting, and preview deployments for your pull requests.



## Reporting test output in pull requests



When tests fail in CI, Vitest reports the failure(s) in your pull request. In Github, for example, failing pull requests will show:



- ❌ Red X next to the commit

- Detailed logs showing which tests failed

- Line numbers and error messages

- Option to re-run failed tests


This prevents merging broken code and makes code review more efficient. Reviewers can focus on logic and design, trusting that tests verify correctness.



## What is important to test?



Not all code is equally important to test. Prioritize based on:



### High priority - Always test



**Validation functions** - Protect data integrity



**Data transformation** - Shape content for display



**Critical business logic** - Features that could break revenue/experience



### Medium priority - Test when complex



**Custom input components** - When they have non-trivial logic



**Schema structure helpers** - When they involve logic



### Low priority - Usually skip



**Simple schema definitions** - No logic to test



**Thin wrappers** - Just pass through to libraries



**UI-only components** - Styling with no behavior



> [!NOTE]
> Testing strategy is about making intentional trade-offs. Perfect coverage isn't the goal—protecting critical business logic while maintaining development velocity is.



## Test organization patterns



As your test suite grows, maintain structure:



`apps/studio/  
├── schemaTypes/  
│   ├── validation/  
│   │   ├── eventValidation.ts  
│   │   └── eventValidation.test.ts  
│   ├── components/  
│   │   ├── DoorsOpenInput.tsx  
│   │   └── DoorsOpenInput.test.tsx  
│   └── eventType.ts  
└── __tests__/  
    ├── fixtures/  
    │   ├── validation.ts  
    │   ├── client.ts  
    │   └── providers.tsx  
    └── setup.ts`  




Key principles:



- **Co-locate tests** with the code they test

- **Share fixtures** in a central location (`__tests__/fixtures`)

- **Name tests** after the file being tested (`DoorsOpenInput.test.tsx`)


## Test maintenance best practices



Tests require maintenance like production code. Follow these practices:



### Keep tests simple



```typescript
// ❌ Complex test with multiple concerns
it('handles everything', async () => {
  const result1 = await validateVenue(venue1, context1)
  const result2 = await validateVenue(venue2, context2)
  const result3 = await validateVenue(venue3, context3)
  expect(result1).toBe(true)
  expect(result2).toBe(false)
  expect(result3).toBe(true)
})

// ✅ Focused tests, one concept each
it('allows venue for in-person events', async () => {
  const result = await validateVenue(venue, inPersonContext)
  expect(result).toBe(true)
})

it('rejects venue for virtual events', async () => {
  const result = await validateVenue(venue, virtualContext)
  expect(result).toBe('Only in-person events can have a venue')
})
```

### Use descriptive test names



```typescript
// ❌ Vague
it('works', () => {})
it('test1', () => {})

// ✅ Clear intent
it('allows venue for in-person events', () => {})
it('rejects venue for virtual events', () => {})
it('calculates doors open time 60 minutes before event', () => {})
```

### Extract test helpers



```typescript
// ❌ Repeated setup in every test
it('test 1', () => {
  const context = {document: {_id: '1', _type: 'event', eventType: 'in-person'}}
  // ... test
})

it('test 2', () => {
  const context = {document: {_id: '2', _type: 'event', eventType: 'virtual'}}
  // ... test
})

// ✅ Reusable fixture
function createEventContext(eventType: string) {
  return createMockValidationContext({
    _id: `event-${eventType}`,
    _type: 'event',
    eventType,
  })
}

it('allows venue for in-person events', () => {
  const context = createEventContext('in-person')
  // ... test
})
```

## Start small, grow strategically



Building a test suite is an investment. Start small and expand strategically:



### Phase 1: Test critical validation



Begin with functions that protect data integrity:



- Required field validation

- Business rule enforcement

- Data consistency checks


**Goal**: Prevent content editors from creating invalid documents



### Phase 2: Test complex helpers



Add tests for helper functions with non-trivial logic:



- Date/time calculations

- Formatting utilities

- Data transformations


**Goal**: Catch bugs in commonly-used utilities



### Phase 3: Test custom components



Test custom inputs with complex behavior:



- Components with conditional rendering

- Components with user interactions

- Components that query the dataset


**Goal**: Ensure editor UI works correctly



### Phase 4: Integrate CI



Add GitHub Actions to run tests automatically:



- On every pull request

- Before merging to main

- Before deployments


**Goal**: Prevent untested code from reaching production



- [ ] Review your current Studio code. Categorize your validation functions, helpers, and components into high/medium/low priority based on business impact and complexity.


## Maintaining test quality



As your suite grows, periodically review test quality:



### Red-green-refactor cycle



1. **Red** - Write a failing test

2. **Green** - Make it pass with minimal code

3. **Refactor** - Improve both test and production code

> [!NOTE]
> [Test-driven development (TDD)](https://en.wikipedia.org/wiki/Test-driven_development) naturally produces better-designed code. Writing tests first forces you to think about interfaces and edge cases before implementation.



This discipline prevents over-engineering and keeps tests focused.



### Delete obsolete tests



When you remove features or refactor code, delete tests that no longer serve a purpose. Dead code in tests is still maintenance burden.



## Key takeaways



**Strategic investment**



- Tests pay dividends through confident refactoring and faster debugging

- Start with high-value tests (validation, critical logic)

- Grow your suite incrementally as complexity increases


**Technical implementation**



- Pure functions are easiest to test

- Mock external dependencies (clients, contexts)

- Test user-facing behavior, not implementation details


**Workflow integration**



- Watch mode for instant local feedback

- CI runs for automated validation

- Coverage reports to find gaps


**Sustainable testing**



- Keep tests simple and focused

- Co-locate tests with code

- Delete obsolete tests

- Prioritize readability over cleverness


---

## Lesson 7: Tests as Content Operating System Infrastructure
https://www.sanity.io/learn/course/testing-sanity-studio/tests-as-content-operating-system-infrastructure

Tests aren't overhead—they're strategic infrastructure that enables confident scaling of your Content Operating System. Comprehensive test coverage protects critical operations as your Studio grows, documents business requirements for future developers, and creates the safety net that allows your team to iterate rapidly. Learn to view testing as a foundational investment that compounds in value over time.

You've built a comprehensive testing strategy, progressing from validation logic and access control to stateful functions to React components. In Sanity's Content Operating System, your Studio isn't just an editing interface—it's programmable infrastructure that encodes business rules, workflow automation, and content governance. Tests are specifications for this infrastructure.



Just as the Content Operating System treats content as structured, reusable data rather than unstructured files, testing treats business requirements as executable specifications rather than documentation that drifts from reality. Your tests document how validation should work, what helpers should return, and how custom inputs should behave—in code that verifies itself. This aligns with Sanity's philosophy of strategic preparation: when code becomes commodity through AI assistance, your specifications become your competitive advantage.



Tests enable AI agents to understand your business logic, allowing them to safely refactor, extend, and maintain your Studio while respecting the rules that protect your content quality. Your test suite becomes a strategic asset that compounds in value as complexity grows, enabling you to move fast always through preparation rather than scrambling.



Start with high-value tests—validation functions that protect data integrity, helper utilities with business logic, custom components that mutate documents. Grow incrementally as complexity increases. Keep tests simple and focused, co-located with the code they verify. Mock external dependencies like Sanity clients and validation contexts. Separate business logic from validation builders for reusability. Use watch mode locally for instant feedback, run comprehensive suites in CI before merge, and leverage coverage reports to find gaps in critical paths.



Add tests for new features before they reach production. Test components that write data rigorously to prevent corruption. Build reusable fixtures for your domain. Review tests in code review. Delete obsolete tests as features change. Refactor tests when they become harder to maintain than production code.



Your Studio is now protected by executable specifications that grow with your business needs. Content editors work confidently, knowing custom logic protects their content. Developers refactor fearlessly, knowing tests catch regressions. Your tests are infrastructure for the Content Operating System, ensuring reliable content operations at scale.



---

## Related Resources

- [All courses and lessons](https://www.sanity.io/learn/sitemap.md)
- [Complete content for LLMs](https://www.sanity.io/learn/llms-full.txt)
