Você não pode selecionar mais de 25 tópicos Os tópicos devem começar com uma letra ou um número, podem incluir traços ('-') e podem ter até 35 caracteres.

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500
  1. # Data Factories and API-First Setup
  2. ## Principle
  3. Prefer factory functions that accept overrides and return complete objects (`createUser(overrides)`). Seed test state through APIs, tasks, or direct DB helpers before visiting the UI—never via slow UI interactions. UI is for validation only, not setup.
  4. ## Rationale
  5. Static fixtures (JSON files, hardcoded objects) create brittle tests that:
  6. - Fail when schemas evolve (missing new required fields)
  7. - Cause collisions in parallel execution (same user IDs)
  8. - Hide test intent (what matters for _this_ test?)
  9. Dynamic factories with overrides provide:
  10. - **Parallel safety**: UUIDs and timestamps prevent collisions
  11. - **Schema evolution**: Defaults adapt to schema changes automatically
  12. - **Explicit intent**: Overrides show what matters for each test
  13. - **Speed**: API setup is 10-50x faster than UI
  14. ## Pattern Examples
  15. ### Example 1: Factory Function with Overrides
  16. **Context**: When creating test data, build factory functions with sensible defaults and explicit overrides. Use `faker` for dynamic values that prevent collisions.
  17. **Implementation**:
  18. ```typescript
  19. // test-utils/factories/user-factory.ts
  20. import { faker } from '@faker-js/faker';
  21. type User = {
  22. id: string;
  23. email: string;
  24. name: string;
  25. role: 'user' | 'admin' | 'moderator';
  26. createdAt: Date;
  27. isActive: boolean;
  28. };
  29. export const createUser = (overrides: Partial<User> = {}): User => ({
  30. id: faker.string.uuid(),
  31. email: faker.internet.email(),
  32. name: faker.person.fullName(),
  33. role: 'user',
  34. createdAt: new Date(),
  35. isActive: true,
  36. ...overrides,
  37. });
  38. // test-utils/factories/product-factory.ts
  39. type Product = {
  40. id: string;
  41. name: string;
  42. price: number;
  43. stock: number;
  44. category: string;
  45. };
  46. export const createProduct = (overrides: Partial<Product> = {}): Product => ({
  47. id: faker.string.uuid(),
  48. name: faker.commerce.productName(),
  49. price: parseFloat(faker.commerce.price()),
  50. stock: faker.number.int({ min: 0, max: 100 }),
  51. category: faker.commerce.department(),
  52. ...overrides,
  53. });
  54. // Usage in tests:
  55. test('admin can delete users', async ({ page, apiRequest }) => {
  56. // Default user
  57. const user = createUser();
  58. // Admin user (explicit override shows intent)
  59. const admin = createUser({ role: 'admin' });
  60. // Seed via API (fast!)
  61. await apiRequest({ method: 'POST', url: '/api/users', data: user });
  62. await apiRequest({ method: 'POST', url: '/api/users', data: admin });
  63. // Now test UI behavior
  64. await page.goto('/admin/users');
  65. await page.click(`[data-testid="delete-user-${user.id}"]`);
  66. await expect(page.getByText(`User ${user.name} deleted`)).toBeVisible();
  67. });
  68. ```
  69. **Key Points**:
  70. - `Partial<User>` allows overriding any field without breaking type safety
  71. - Faker generates unique values—no collisions in parallel tests
  72. - Override shows test intent: `createUser({ role: 'admin' })` is explicit
  73. - Factory lives in `test-utils/factories/` for easy reuse
  74. ### Example 2: Nested Factory Pattern
  75. **Context**: When testing relationships (orders with users and products), nest factories to create complete object graphs. Control relationship data explicitly.
  76. **Implementation**:
  77. ```typescript
  78. // test-utils/factories/order-factory.ts
  79. import { createUser } from './user-factory';
  80. import { createProduct } from './product-factory';
  81. type OrderItem = {
  82. product: Product;
  83. quantity: number;
  84. price: number;
  85. };
  86. type Order = {
  87. id: string;
  88. user: User;
  89. items: OrderItem[];
  90. total: number;
  91. status: 'pending' | 'paid' | 'shipped' | 'delivered';
  92. createdAt: Date;
  93. };
  94. export const createOrderItem = (overrides: Partial<OrderItem> = {}): OrderItem => {
  95. const product = overrides.product || createProduct();
  96. const quantity = overrides.quantity || faker.number.int({ min: 1, max: 5 });
  97. return {
  98. product,
  99. quantity,
  100. price: product.price * quantity,
  101. ...overrides,
  102. };
  103. };
  104. export const createOrder = (overrides: Partial<Order> = {}): Order => {
  105. const items = overrides.items || [createOrderItem(), createOrderItem()];
  106. const total = items.reduce((sum, item) => sum + item.price, 0);
  107. return {
  108. id: faker.string.uuid(),
  109. user: overrides.user || createUser(),
  110. items,
  111. total,
  112. status: 'pending',
  113. createdAt: new Date(),
  114. ...overrides,
  115. };
  116. };
  117. // Usage in tests:
  118. test('user can view order details', async ({ page, apiRequest }) => {
  119. const user = createUser({ email: 'test@example.com' });
  120. const product1 = createProduct({ name: 'Widget A', price: 10.0 });
  121. const product2 = createProduct({ name: 'Widget B', price: 15.0 });
  122. // Explicit relationships
  123. const order = createOrder({
  124. user,
  125. items: [
  126. createOrderItem({ product: product1, quantity: 2 }), // $20
  127. createOrderItem({ product: product2, quantity: 1 }), // $15
  128. ],
  129. });
  130. // Seed via API
  131. await apiRequest({ method: 'POST', url: '/api/users', data: user });
  132. await apiRequest({ method: 'POST', url: '/api/products', data: product1 });
  133. await apiRequest({ method: 'POST', url: '/api/products', data: product2 });
  134. await apiRequest({ method: 'POST', url: '/api/orders', data: order });
  135. // Test UI
  136. await page.goto(`/orders/${order.id}`);
  137. await expect(page.getByText('Widget A x 2')).toBeVisible();
  138. await expect(page.getByText('Widget B x 1')).toBeVisible();
  139. await expect(page.getByText('Total: $35.00')).toBeVisible();
  140. });
  141. ```
  142. **Key Points**:
  143. - Nested factories handle relationships (order → user, order → products)
  144. - Overrides cascade: provide custom user/products or use defaults
  145. - Calculated fields (total) derived automatically from nested data
  146. - Explicit relationships make test data clear and maintainable
  147. ### Example 3: Factory with API Seeding
  148. **Context**: When tests need data setup, always use API calls or database tasks—never UI navigation. Wrap factory usage with seeding utilities for clean test setup.
  149. **Implementation**:
  150. ```typescript
  151. // playwright/support/helpers/seed-helpers.ts
  152. import { APIRequestContext } from '@playwright/test';
  153. import { User, createUser } from '../../test-utils/factories/user-factory';
  154. import { Product, createProduct } from '../../test-utils/factories/product-factory';
  155. export async function seedUser(request: APIRequestContext, overrides: Partial<User> = {}): Promise<User> {
  156. const user = createUser(overrides);
  157. const response = await request.post('/api/users', {
  158. data: user,
  159. });
  160. if (!response.ok()) {
  161. throw new Error(`Failed to seed user: ${response.status()}`);
  162. }
  163. return user;
  164. }
  165. export async function seedProduct(request: APIRequestContext, overrides: Partial<Product> = {}): Promise<Product> {
  166. const product = createProduct(overrides);
  167. const response = await request.post('/api/products', {
  168. data: product,
  169. });
  170. if (!response.ok()) {
  171. throw new Error(`Failed to seed product: ${response.status()}`);
  172. }
  173. return product;
  174. }
  175. // Playwright globalSetup for shared data
  176. // playwright/support/global-setup.ts
  177. import { chromium, FullConfig } from '@playwright/test';
  178. import { seedUser } from './helpers/seed-helpers';
  179. async function globalSetup(config: FullConfig) {
  180. const browser = await chromium.launch();
  181. const page = await browser.newPage();
  182. const context = page.context();
  183. // Seed admin user for all tests
  184. const admin = await seedUser(context.request, {
  185. email: 'admin@example.com',
  186. role: 'admin',
  187. });
  188. // Save auth state for reuse
  189. await context.storageState({ path: 'playwright/.auth/admin.json' });
  190. await browser.close();
  191. }
  192. export default globalSetup;
  193. // Cypress equivalent with cy.task
  194. // cypress/support/tasks.ts
  195. export const seedDatabase = async (entity: string, data: unknown) => {
  196. // Direct database insert or API call
  197. if (entity === 'users') {
  198. await db.users.create(data);
  199. }
  200. return null;
  201. };
  202. // Usage in Cypress tests:
  203. beforeEach(() => {
  204. const user = createUser({ email: 'test@example.com' });
  205. cy.task('db:seed', { entity: 'users', data: user });
  206. });
  207. ```
  208. **Key Points**:
  209. - API seeding is 10-50x faster than UI-based setup
  210. - `globalSetup` seeds shared data once (e.g., admin user)
  211. - Per-test seeding uses `seedUser()` helpers for isolation
  212. - Cypress `cy.task` allows direct database access for speed
  213. ### Example 4: Anti-Pattern - Hardcoded Test Data
  214. **Problem**:
  215. ```typescript
  216. // ❌ BAD: Hardcoded test data
  217. test('user can login', async ({ page }) => {
  218. await page.goto('/login');
  219. await page.fill('[data-testid="email"]', 'test@test.com'); // Hardcoded
  220. await page.fill('[data-testid="password"]', 'password123'); // Hardcoded
  221. await page.click('[data-testid="submit"]');
  222. // What if this user already exists? Test fails in parallel runs.
  223. // What if schema adds required fields? Test breaks.
  224. });
  225. // ❌ BAD: Static JSON fixtures
  226. // fixtures/users.json
  227. {
  228. "users": [
  229. { "id": 1, "email": "user1@test.com", "name": "User 1" },
  230. { "id": 2, "email": "user2@test.com", "name": "User 2" }
  231. ]
  232. }
  233. test('admin can delete user', async ({ page }) => {
  234. const users = require('../fixtures/users.json');
  235. // Brittle: IDs collide in parallel, schema drift breaks tests
  236. });
  237. ```
  238. **Why It Fails**:
  239. - **Parallel collisions**: Hardcoded IDs (`id: 1`, `email: 'test@test.com'`) cause failures when tests run concurrently
  240. - **Schema drift**: Adding required fields (`phoneNumber`, `address`) breaks all tests using fixtures
  241. - **Hidden intent**: Does this test need `email: 'test@test.com'` specifically, or any email?
  242. - **Slow setup**: UI-based data creation is 10-50x slower than API
  243. **Better Approach**: Use factories
  244. ```typescript
  245. // ✅ GOOD: Factory-based data
  246. test('user can login', async ({ page, apiRequest }) => {
  247. const user = createUser({ email: 'unique@example.com', password: 'secure123' });
  248. // Seed via API (fast, parallel-safe)
  249. await apiRequest({ method: 'POST', url: '/api/users', data: user });
  250. // Test UI
  251. await page.goto('/login');
  252. await page.fill('[data-testid="email"]', user.email);
  253. await page.fill('[data-testid="password"]', user.password);
  254. await page.click('[data-testid="submit"]');
  255. await expect(page).toHaveURL('/dashboard');
  256. });
  257. // ✅ GOOD: Factories adapt to schema changes automatically
  258. // When `phoneNumber` becomes required, update factory once:
  259. export const createUser = (overrides: Partial<User> = {}): User => ({
  260. id: faker.string.uuid(),
  261. email: faker.internet.email(),
  262. name: faker.person.fullName(),
  263. phoneNumber: faker.phone.number(), // NEW field, all tests get it automatically
  264. role: 'user',
  265. ...overrides,
  266. });
  267. ```
  268. **Key Points**:
  269. - Factories generate unique, parallel-safe data
  270. - Schema evolution handled in one place (factory), not every test
  271. - Test intent explicit via overrides
  272. - API seeding is fast and reliable
  273. ### Example 5: Factory Composition
  274. **Context**: When building specialized factories, compose simpler factories instead of duplicating logic. Layer overrides for specific test scenarios.
  275. **Implementation**:
  276. ```typescript
  277. // test-utils/factories/user-factory.ts (base)
  278. export const createUser = (overrides: Partial<User> = {}): User => ({
  279. id: faker.string.uuid(),
  280. email: faker.internet.email(),
  281. name: faker.person.fullName(),
  282. role: 'user',
  283. createdAt: new Date(),
  284. isActive: true,
  285. ...overrides,
  286. });
  287. // Compose specialized factories
  288. export const createAdminUser = (overrides: Partial<User> = {}): User => createUser({ role: 'admin', ...overrides });
  289. export const createModeratorUser = (overrides: Partial<User> = {}): User => createUser({ role: 'moderator', ...overrides });
  290. export const createInactiveUser = (overrides: Partial<User> = {}): User => createUser({ isActive: false, ...overrides });
  291. // Account-level factories with feature flags
  292. type Account = {
  293. id: string;
  294. owner: User;
  295. plan: 'free' | 'pro' | 'enterprise';
  296. features: string[];
  297. maxUsers: number;
  298. };
  299. export const createAccount = (overrides: Partial<Account> = {}): Account => ({
  300. id: faker.string.uuid(),
  301. owner: overrides.owner || createUser(),
  302. plan: 'free',
  303. features: [],
  304. maxUsers: 1,
  305. ...overrides,
  306. });
  307. export const createProAccount = (overrides: Partial<Account> = {}): Account =>
  308. createAccount({
  309. plan: 'pro',
  310. features: ['advanced-analytics', 'priority-support'],
  311. maxUsers: 10,
  312. ...overrides,
  313. });
  314. export const createEnterpriseAccount = (overrides: Partial<Account> = {}): Account =>
  315. createAccount({
  316. plan: 'enterprise',
  317. features: ['advanced-analytics', 'priority-support', 'sso', 'audit-logs'],
  318. maxUsers: 100,
  319. ...overrides,
  320. });
  321. // Usage in tests:
  322. test('pro accounts can access analytics', async ({ page, apiRequest }) => {
  323. const admin = createAdminUser({ email: 'admin@company.com' });
  324. const account = createProAccount({ owner: admin });
  325. await apiRequest({ method: 'POST', url: '/api/users', data: admin });
  326. await apiRequest({ method: 'POST', url: '/api/accounts', data: account });
  327. await page.goto('/analytics');
  328. await expect(page.getByText('Advanced Analytics')).toBeVisible();
  329. });
  330. test('free accounts cannot access analytics', async ({ page, apiRequest }) => {
  331. const user = createUser({ email: 'user@company.com' });
  332. const account = createAccount({ owner: user }); // Defaults to free plan
  333. await apiRequest({ method: 'POST', url: '/api/users', data: user });
  334. await apiRequest({ method: 'POST', url: '/api/accounts', data: account });
  335. await page.goto('/analytics');
  336. await expect(page.getByText('Upgrade to Pro')).toBeVisible();
  337. });
  338. ```
  339. **Key Points**:
  340. - Compose specialized factories from base factories (`createAdminUser` → `createUser`)
  341. - Defaults cascade: `createProAccount` sets plan + features automatically
  342. - Still allow overrides: `createProAccount({ maxUsers: 50 })` works
  343. - Test intent clear: `createProAccount()` vs `createAccount({ plan: 'pro', features: [...] })`
  344. ## Integration Points
  345. - **Used in workflows**: `*atdd` (test generation), `*automate` (test expansion), `*framework` (factory setup)
  346. - **Related fragments**:
  347. - `fixture-architecture.md` - Pure functions and fixtures for factory integration
  348. - `network-first.md` - API-first setup patterns
  349. - `test-quality.md` - Parallel-safe, deterministic test design
  350. ## Cleanup Strategy
  351. Ensure factories work with cleanup patterns:
  352. ```typescript
  353. // Track created IDs for cleanup
  354. const createdUsers: string[] = [];
  355. afterEach(async ({ apiRequest }) => {
  356. // Clean up all users created during test
  357. for (const userId of createdUsers) {
  358. await apiRequest({ method: 'DELETE', url: `/api/users/${userId}` });
  359. }
  360. createdUsers.length = 0;
  361. });
  362. test('user registration flow', async ({ page, apiRequest }) => {
  363. const user = createUser();
  364. createdUsers.push(user.id);
  365. await apiRequest({ method: 'POST', url: '/api/users', data: user });
  366. // ... test logic
  367. });
  368. ```
  369. ## Feature Flag Integration
  370. When working with feature flags, layer them into factories:
  371. ```typescript
  372. export const createUserWithFlags = (
  373. overrides: Partial<User> = {},
  374. flags: Record<string, boolean> = {},
  375. ): User & { flags: Record<string, boolean> } => ({
  376. ...createUser(overrides),
  377. flags: {
  378. 'new-dashboard': false,
  379. 'beta-features': false,
  380. ...flags,
  381. },
  382. });
  383. // Usage:
  384. const user = createUserWithFlags(
  385. { email: 'test@example.com' },
  386. {
  387. 'new-dashboard': true,
  388. 'beta-features': true,
  389. },
  390. );
  391. ```
  392. _Source: Murat Testing Philosophy (lines 94-120), API-first testing patterns, faker.js documentation._