{"id":"18514c6d-7792-4dce-b354-fcafd4898fec","shortId":"srGjL9","kind":"skill","title":"typescript-testing","tagline":"TypeScript testing patterns with Vitest and MSW. Use when writing unit tests, mocking APIs, creating typed mocks for dependency injection, or using snapshot testing.","description":"# TypeScript Testing Patterns\n\nComprehensive testing patterns using Vitest for unit testing, MSW for API mocking, and snapshot testing for complex object validation.\n\n## Quick Start\n\n### Installation\n\n```bash\n# Core testing dependencies\nbun add -d vitest @vitest/ui\n\n# MSW for API mocking\nbun add -d msw\n\n# Optional: coverage reporting\nbun add -d @vitest/coverage-v8\n```\n\n### Basic Test Structure\n\n```typescript\nimport { describe, it, expect, vi, beforeEach } from 'vitest'\n\ndescribe('UserService', () => {\n  beforeEach(() => {\n    vi.clearAllMocks()\n  })\n\n  it('creates user with valid data', async () => {\n    const result = await userService.create({ name: 'Alice' })\n    expect(result).toMatchObject({ name: 'Alice' })\n  })\n})\n```\n\n## Typed Mock Objects\n\nCreate type-safe mocks for dependency injection using `vi.mocked()`:\n\n```typescript\nimport { vi } from 'vitest'\nimport type { UserRepository } from './user-repository'\n\n// Mock the entire module\nvi.mock('./user-repository')\n\n// Get typed mock instance\nconst mockUserRepo = vi.mocked<UserRepository>({\n  findById: vi.fn(),\n  save: vi.fn(),\n  delete: vi.fn(),\n})\n\n// Type-safe mock return values\nmockUserRepo.findById.mockResolvedValue({\n  id: '123',\n  name: 'Alice',\n  email: 'alice@example.com',\n})\n\n// Assertions with full type safety\nexpect(mockUserRepo.findById).toHaveBeenCalledWith('123')\n```\n\n### Mock Return Values\n\n```typescript\n// Single return value\nmockRepo.findById.mockResolvedValue(user)\n\n// Multiple calls, different returns\nmockRepo.findById\n  .mockResolvedValueOnce(null)\n  .mockResolvedValueOnce(user)\n\n// Conditional logic\nmockRepo.findById.mockImplementation(async (id) => {\n  if (id === '123') return user\n  return null\n})\n\n// Throw errors\nmockRepo.save.mockRejectedValue(new Error('Database error'))\n```\n\n### Spy on Real Implementations\n\n```typescript\nimport { vi } from 'vitest'\nimport { emailService } from './email-service'\n\n// Spy on method without replacing it\nvi.spyOn(emailService, 'send')\n\n// Call real implementation\nawait emailService.send({ to: 'alice@example.com', subject: 'Test' })\n\n// Assert it was called\nexpect(emailService.send).toHaveBeenCalledOnce()\n\n// Temporarily override return value\nemailService.send.mockResolvedValueOnce({ messageId: 'test-123' })\n```\n\nSee [references/mocking.md](./references/mocking.md) for comprehensive mocking patterns including module mocks, class mocks, stateful handlers, and dependency injection.\n\n## API Mocking with MSW\n\nMock HTTP requests at the network level for integration tests:\n\n```typescript\nimport { http, HttpResponse } from 'msw'\nimport { setupServer } from 'msw/node'\nimport { afterAll, afterEach, beforeAll } from 'vitest'\n\n// Define handlers\nconst handlers = [\n  http.get('/api/users/:id', ({ params }) => {\n    return HttpResponse.json({\n      id: params.id,\n      name: 'Alice',\n      email: 'alice@example.com',\n    })\n  }),\n\n  http.post('/api/users', async ({ request }) => {\n    const body = await request.json()\n    return HttpResponse.json({ id: '123', ...body }, { status: 201 })\n  }),\n]\n\n// Setup server\nconst server = setupServer(...handlers)\n\nbeforeAll(() => server.listen())\nafterEach(() => server.resetHandlers())\nafterAll(() => server.close())\n```\n\nSee [references/msw.md](./references/msw.md) for advanced MSW patterns including error simulation, delays, and per-test overrides.\n\n## Snapshot Testing\n\nValidate complex objects and output without manual assertions:\n\n```typescript\nimport { it, expect } from 'vitest'\n\nit('generates correct user profile', () => {\n  const profile = generateUserProfile(user)\n\n  // Snapshot entire object\n  expect(profile).toMatchSnapshot()\n})\n\n// Handle dynamic values (dates, IDs)\nit('creates order with timestamp', () => {\n  const order = createOrder(items)\n\n  expect(order).toMatchSnapshot({\n    id: expect.any(String),\n    createdAt: expect.any(Date),\n  })\n})\n\n// Inline snapshots for small objects\nit('formats error message', () => {\n  const error = formatError(new Error('Failed'))\n  expect(error).toMatchInlineSnapshot(`\n    {\n      \"message\": \"Failed\",\n      \"code\": \"UNKNOWN_ERROR\",\n    }\n  `)\n})\n```\n\nSee [references/snapshot-testing.md](./references/snapshot-testing.md) for snapshot maintenance, custom serializers, and best practices.\n\n## Testing Async Code\n\n```typescript\n// Promises\nit('loads user data', async () => {\n  const user = await userService.findById('123')\n  expect(user).toBeDefined()\n})\n\n// Callbacks\nit('calls callback on completion', () => {\n  return new Promise<void>((resolve) => {\n    processData(data, (result) => {\n      expect(result).toBe(expected)\n      resolve()\n    })\n  })\n})\n\n// Timers\nit('retries after delay', async () => {\n  vi.useFakeTimers()\n\n  const promise = retryOperation()\n  vi.advanceTimersByTime(1000)\n\n  const result = await promise\n  expect(result).toBe('success')\n\n  vi.useRealTimers()\n})\n```\n\n## Test Organization\n\n```typescript\n// Group related tests\ndescribe('UserService', () => {\n  describe('create', () => {\n    it('succeeds with valid data', () => {})\n    it('throws on duplicate email', () => {})\n  })\n\n  describe('update', () => {\n    it('updates existing user', () => {})\n    it('throws on not found', () => {})\n  })\n})\n\n// Shared setup\ndescribe('authenticated requests', () => {\n  beforeEach(() => {\n    mockAuth.isAuthenticated.mockReturnValue(true)\n  })\n\n  it('allows user creation', () => {})\n  it('allows user deletion', () => {})\n})\n```\n\n## Guidelines\n\n1. **Test behavior, not implementation** - Focus on what the code does, not how it does it\n2. **One assertion per test when possible** - Makes failures clearer and tests more focused\n3. **Use typed mocks** - `vi.mocked<T>()` provides type safety for mock setup and assertions\n4. **Mock at boundaries** - Mock external dependencies (APIs, databases), not internal functions\n5. **Use MSW for API tests** - Mock at the network level for realistic integration tests\n6. **Snapshots for complex output** - Use snapshots for large objects, explicit assertions for critical values\n7. **Property matchers for dynamic values** - Handle dates, IDs, timestamps with `expect.any()`\n8. **Clear test names** - Describe the scenario and expected outcome: \"creates user with valid data\"\n9. **Setup/teardown for state** - Use `beforeEach`/`afterEach` to ensure test isolation\n10. **Async/await over callbacks** - Prefer `async`/`await` for cleaner async test code\n11. **Fake timers for time-based code** - Use `vi.useFakeTimers()` to control time in tests\n12. **Test error cases** - Verify error handling with `expect().rejects.toThrow()`\n\n## Related Skills\n\n- **build-tools** - Vitest configuration, test scripts, coverage setup\n- **api-design** - Testing REST API contracts and error responses\n- **fastify** - Testing Fastify routes and plugins\n- **drizzle-orm** - Testing database queries (use MSW or in-memory DB)\n- **dynamodb-toolbox** - Testing DynamoDB entities and queries\n\n## Common Patterns\n\n### Testing with Dependency Injection\n\n```typescript\nclass UserService {\n  constructor(\n    private repo: UserRepository,\n    private email: EmailService,\n  ) {}\n\n  async create(data: CreateUserInput) {\n    const user = await this.repo.save(data)\n    await this.email.sendWelcome(user.email)\n    return user\n  }\n}\n\n// Test with mocks\nit('sends welcome email on create', async () => {\n  const mockRepo = vi.mocked<UserRepository>({\n    save: vi.fn().mockResolvedValue(savedUser),\n  })\n  const mockEmail = vi.mocked<EmailService>({\n    sendWelcome: vi.fn().mockResolvedValue(undefined),\n  })\n\n  const service = new UserService(mockRepo, mockEmail)\n  await service.create(userData)\n\n  expect(mockEmail.sendWelcome).toHaveBeenCalledWith('alice@example.com')\n})\n```\n\n### Testing Error Boundaries\n\n```typescript\nit('handles repository errors gracefully', async () => {\n  mockRepo.save.mockRejectedValue(new Error('Database error'))\n\n  await expect(\n    service.create(userData)\n  ).rejects.toThrow('Failed to create user')\n\n  // Verify cleanup or rollback occurred\n  expect(mockEmail.sendWelcome).not.toHaveBeenCalled()\n})\n```\n\n### Testing Race Conditions\n\n```typescript\nit('handles concurrent requests correctly', async () => {\n  const promises = [\n    service.processOrder(order1),\n    service.processOrder(order2),\n    service.processOrder(order3),\n  ]\n\n  const results = await Promise.all(promises)\n\n  expect(results).toHaveLength(3)\n  expect(new Set(results.map(r => r.id)).size).toBe(3)\n})\n```\n\n## Debugging Tests\n\n```bash\n# Run single test file\nvitest run path/to/test.spec.ts\n\n# Run tests matching pattern\nvitest run -t \"creates user\"\n\n# Watch mode for TDD\nvitest watch\n\n# UI mode for debugging\nvitest --ui\n\n# Coverage report\nvitest run --coverage\n```","tags":["typescript","testing","atelier","martinffx","agent-skills","agentic-coding","anthropic","claude-code","claude-skills","code-review","codex","codex-skill"],"capabilities":["skill","source-martinffx","skill-typescript-testing","topic-agent-skills","topic-agentic-coding","topic-anthropic","topic-claude-code","topic-claude-skills","topic-code-review","topic-codex","topic-codex-skill","topic-opencode","topic-prompt-engineering","topic-sdd","topic-spec-driven-development"],"categories":["atelier"],"synonyms":[],"warnings":[],"endpointUrl":"https://skills.sh/martinffx/atelier/typescript-testing","protocol":"skill","transport":"skills-sh","auth":{"type":"none","details":{"cli":"npx skills add martinffx/atelier","source_repo":"https://github.com/martinffx/atelier","install_from":"skills.sh"}},"qualityScore":"0.461","qualityRationale":"deterministic score 0.46 from registry signals: · indexed on github topic:agent-skills · 23 github stars · SKILL.md body (8,657 chars)","verified":false,"liveness":"unknown","lastLivenessCheck":null,"agentReviews":{"count":0,"score_avg":null,"cost_usd_avg":null,"success_rate":null,"latency_p50_ms":null,"narrative_summary":null,"summary_updated_at":null},"enrichmentModel":"deterministic:skill-github:v1","enrichmentVersion":1,"enrichedAt":"2026-05-18T19:05:25.450Z","embedding":null,"createdAt":"2026-05-10T07:03:13.870Z","updatedAt":"2026-05-18T19:05:25.450Z","lastSeenAt":"2026-05-18T19:05:25.450Z","tsv":"'-123':257 '/api/users':310,322 '/email-service':224 '/references/mocking.md':260 '/references/msw.md':350 '/references/snapshot-testing.md':443 '/user-repository':133,139 '1':557 '10':680 '1000':499 '11':692 '12':707 '123':161,174,200,332,466 '2':573 '201':335 '3':587,890,899 '4':600 '5':612 '6':627 '7':642 '8':654 '9':669 'add':58,67,74 'advanc':352 'afteral':300,346 'aftereach':301,344,675 'alic':105,110,163,318 'alice@example.com':165,240,320,831 'allow':549,553 'api':17,41,64,275,607,616,729,733 'api-design':728 'assert':166,243,373,575,599,638 'async':99,196,323,453,461,493,685,689,781,804,841,873 'async/await':681 'authent':543 'await':102,237,327,464,502,686,787,790,825,847,884 'base':698 'bash':53,902 'basic':77 'beforeal':302,342 'beforeeach':86,91,545,674 'behavior':559 'best':450 'bodi':326,333 'boundari':603,834 'build':720 'build-tool':719 'bun':57,66,73 'call':185,234,246,472 'callback':470,473,683 'case':710 'class':268,772 'cleaner':688 'cleanup':857 'clear':655 'clearer':582 'code':438,454,566,691,699 'common':765 'complet':475 'complex':47,367,630 'comprehens':31,262 'concurr':870 'condit':193,866 'configur':723 'const':100,144,307,325,338,385,405,427,462,495,500,785,805,812,819,874,882 'constructor':774 'contract':734 'control':703 'core':54 'correct':382,872 'coverag':71,726,931,935 'creat':18,94,114,401,518,664,782,803,854,917 'createdat':415 'createord':407 'createuserinput':784 'creation':551 'critic':640 'custom':447 'd':59,68,75 'data':98,460,481,523,668,783,789 'databas':210,608,748,845 'date':398,417,649 'db':756 'debug':900,928 'defin':305 'delay':358,492 'delet':151,555 'depend':22,56,120,273,606,769 'describ':82,89,515,517,529,542,658 'design':730 'differ':186 'drizzl':745 'drizzle-orm':744 'duplic':527 'dynam':396,646 'dynamodb':758,761 'dynamodb-toolbox':757 'email':164,319,528,779,801 'emailservic':222,232,780 'emailservice.send':238,248 'emailservice.send.mockresolvedvalueonce':254 'ensur':677 'entir':136,390 'entiti':762 'error':206,209,211,356,425,428,431,434,440,709,712,736,833,839,844,846 'exist':533 'expect':84,106,171,247,377,392,409,433,467,483,486,504,662,715,828,848,861,887,891 'expect.any':413,416,653 'explicit':637 'extern':605 'fail':432,437,852 'failur':581 'fake':693 'fastifi':738,740 'file':906 'findbyid':147 'focus':562,586 'format':424 'formaterror':429 'found':539 'full':168 'function':611 'generat':381 'generateuserprofil':387 'get':140 'grace':840 'group':512 'guidelin':556 'handl':395,648,713,837,869 'handler':271,306,308,341 'http':280,291 'http.get':309 'http.post':321 'httprespons':292 'httpresponse.json':314,330 'id':160,197,199,311,315,331,399,412,650 'implement':215,236,561 'import':81,125,129,217,221,290,295,299,375 'in-memori':753 'includ':265,355 'inject':23,121,274,770 'inlin':418 'instal':52 'instanc':143 'integr':287,625 'intern':610 'isol':679 'item':408 'larg':635 'level':285,622 'load':458 'logic':194 'mainten':446 'make':580 'manual':372 'match':912 'matcher':644 'memori':755 'messag':426,436 'messageid':255 'method':227 'mock':16,20,42,65,112,118,134,142,156,175,263,267,269,276,279,590,596,601,604,618,797 'mockauth.isauthenticated.mockreturnvalue':546 'mockemail':813,824 'mockemail.sendwelcome':829,862 'mockrepo':806,823 'mockrepo.findbyid':188 'mockrepo.findbyid.mockimplementation':195 'mockrepo.findbyid.mockresolvedvalue':182 'mockrepo.save.mockrejectedvalue':207,842 'mockresolvedvalu':810,817 'mockresolvedvalueonc':189,191 'mockuserrepo':145 'mockuserrepo.findbyid':172 'mockuserrepo.findbyid.mockresolvedvalue':159 'mode':920,926 'modul':137,266 'msw':10,39,62,69,278,294,353,614,751 'msw/node':298 'multipl':184 'name':104,109,162,317,657 'network':284,621 'new':208,430,477,821,843,892 'not.tohavebeencalled':863 'null':190,204 'object':48,113,368,391,422,636 'occur':860 'one':574 'option':70 'order':402,406,410 'order1':877 'order2':879 'order3':881 'organ':510 'orm':746 'outcom':663 'output':370,631 'overrid':251,363 'param':312 'params.id':316 'path/to/test.spec.ts':909 'pattern':6,30,33,264,354,766,913 'per':361,576 'per-test':360 'plugin':743 'possibl':579 'practic':451 'prefer':684 'privat':775,778 'processdata':480 'profil':384,386,393 'promis':456,478,496,503,875,886 'promise.all':885 'properti':643 'provid':592 'queri':749,764 'quick':50 'r':895 'r.id':896 'race':865 'real':214,235 'realist':624 'references/mocking.md':259 'references/msw.md':349 'references/snapshot-testing.md':442 'rejects.tothrow':716,851 'relat':513,717 'replac':229 'repo':776 'report':72,932 'repositori':838 'request':281,324,544,871 'request.json':328 'resolv':479,487 'respons':737 'rest':732 'result':101,107,482,484,501,505,883,888 'results.map':894 'retri':490 'retryoper':497 'return':157,176,180,187,201,203,252,313,329,476,793 'rollback':859 'rout':741 'run':903,908,910,915,934 'safe':117,155 'safeti':170,594 'save':149,808 'savedus':811 'scenario':660 'script':725 'see':258,348,441 'send':233,799 'sendwelcom':815 'serial':448 'server':337,339 'server.close':347 'server.listen':343 'server.resethandlers':345 'servic':820 'service.create':826,849 'service.processorder':876,878,880 'set':893 'setup':336,541,597,727 'setup/teardown':670 'setupserv':296,340 'share':540 'simul':357 'singl':179,904 'size':897 'skill':718 'skill-typescript-testing' 'small':421 'snapshot':26,44,364,389,419,445,628,633 'source-martinffx' 'spi':212,225 'start':51 'state':270,672 'status':334 'string':414 'structur':79 'subject':241 'succeed':520 'success':507 'tdd':922 'temporarili':250 'test':3,5,15,27,29,32,38,45,55,78,242,256,288,362,365,452,509,514,558,577,584,617,626,656,678,690,706,708,724,731,739,747,760,767,795,832,864,901,905,911 'this.email.sendwelcome':791 'this.repo.save':788 'throw':205,525,536 'time':697,704 'time-bas':696 'timer':488,694 'timestamp':404,651 'tobe':485,506,898 'tobedefin':469 'tohavebeencalledonc':249 'tohavebeencalledwith':173,830 'tohavelength':889 'tomatchinlinesnapshot':435 'tomatchobject':108 'tomatchsnapshot':394,411 'tool':721 'toolbox':759 'topic-agent-skills' 'topic-agentic-coding' 'topic-anthropic' 'topic-claude-code' 'topic-claude-skills' 'topic-code-review' 'topic-codex' 'topic-codex-skill' 'topic-opencode' 'topic-prompt-engineering' 'topic-sdd' 'topic-spec-driven-development' 'true':547 'type':19,111,116,130,141,154,169,589,593 'type-saf':115,153 'typescript':2,4,28,80,124,178,216,289,374,455,511,771,835,867 'typescript-test':1 'ui':925,930 'undefin':818 'unit':14,37 'unknown':439 'updat':530,532 'use':11,25,34,122,588,613,632,673,700,750 'user':95,183,192,202,383,388,459,463,468,534,550,554,665,786,794,855,918 'user.email':792 'userdata':827,850 'userrepositori':131,777 'userservic':90,516,773,822 'userservice.create':103 'userservice.findbyid':465 'valid':49,97,366,522,667 'valu':158,177,181,253,397,641,647 'verifi':711,856 'vi':85,126,218 'vi.advancetimersbytime':498 'vi.clearallmocks':92 'vi.fn':148,150,152,809,816 'vi.mock':138 'vi.mocked':123,146,591,807,814 'vi.spyon':231 'vi.usefaketimers':494,701 'vi.userealtimers':508 'vitest':8,35,60,88,128,220,304,379,722,907,914,923,929,933 'vitest/coverage-v8':76 'vitest/ui':61 'watch':919,924 'welcom':800 'without':228,371 'write':13","prices":[{"id":"1447dc36-456f-4649-b4de-891e9c94b2e5","listingId":"18514c6d-7792-4dce-b354-fcafd4898fec","amountUsd":"0","unit":"free","nativeCurrency":null,"nativeAmount":null,"chain":null,"payTo":null,"paymentMethod":"skill-free","isPrimary":true,"details":{"org":"martinffx","category":"atelier","install_from":"skills.sh"},"createdAt":"2026-05-10T07:03:13.870Z"}],"sources":[{"listingId":"18514c6d-7792-4dce-b354-fcafd4898fec","source":"github","sourceId":"martinffx/atelier/typescript-testing","sourceUrl":"https://github.com/martinffx/atelier/tree/main/skills/typescript-testing","isPrimary":false,"firstSeenAt":"2026-05-10T07:03:13.870Z","lastSeenAt":"2026-05-18T19:05:25.450Z"}],"details":{"listingId":"18514c6d-7792-4dce-b354-fcafd4898fec","quickStartSnippet":null,"exampleRequest":null,"exampleResponse":null,"schema":null,"openapiUrl":null,"agentsTxtUrl":null,"citations":[],"useCases":[],"bestFor":[],"notFor":[],"kindDetails":{"org":"martinffx","slug":"typescript-testing","github":{"repo":"martinffx/atelier","stars":23,"topics":["agent-skills","agentic-coding","anthropic","claude-code","claude-skills","code-review","codex","codex-skill","opencode","prompt-engineering","sdd","spec-driven-development"],"license":"mit","html_url":"https://github.com/martinffx/atelier","pushed_at":"2026-05-18T06:56:45Z","description":"An atelier for Opencode, Claude Code, and other coding agents: spec-driven workflows, deep thinking, and code quality.","skill_md_sha":"fce60b716763cb90306a6317dab427087c59439c","skill_md_path":"skills/typescript-testing/SKILL.md","default_branch":"main","skill_tree_url":"https://github.com/martinffx/atelier/tree/main/skills/typescript-testing"},"layout":"multi","source":"github","category":"atelier","frontmatter":{"name":"typescript-testing","description":"TypeScript testing patterns with Vitest and MSW. Use when writing unit tests, mocking APIs, creating typed mocks for dependency injection, or using snapshot testing."},"skills_sh_url":"https://skills.sh/martinffx/atelier/typescript-testing"},"updatedAt":"2026-05-18T19:05:25.450Z"}}