testing-practices

This skill covers comprehensive testing strategies for modern JavaScript/TypeScript applications with Remix and SST.

Safety Notice

This listing is imported from skills.sh public index metadata. Review upstream SKILL.md and repository scripts before running.

Copy this and send it to your AI assistant to learn

Install skill "testing-practices" with this command: npx skills add tejovanthn/rasikalife/tejovanthn-rasikalife-testing-practices

Testing Practices

This skill covers comprehensive testing strategies for modern JavaScript/TypeScript applications with Remix and SST.

Core Philosophy

Good tests are:

  • Fast: Run quickly to encourage frequent execution

  • Isolated: Each test is independent

  • Readable: Clear what's being tested and why

  • Reliable: Same input always produces same output

  • Maintainable: Easy to update when code changes

Testing Pyramid

 /\
/  \  E2E Tests (few)

/
/
/ Integr \ Integration Tests (some) /
__
/
/ Unit \ Unit Tests (many) /
__________\

Unit Tests (70%):

  • Test individual functions

  • Fast, isolated

  • Mock dependencies

Integration Tests (20%):

  • Test multiple components together

  • Test database queries

  • Test API routes

E2E Tests (10%):

  • Test full user workflows

  • Slow but high confidence

  • Critical paths only

Test Frameworks

Setup with Vitest

npm install -D vitest @vitest/ui

// vitest.config.ts import { defineConfig } from "vitest/config";

export default defineConfig({ test: { globals: true, environment: "node", setupFiles: ["./test/setup.ts"], coverage: { provider: "v8", reporter: ["text", "html"], exclude: ["/node_modules/", "/test/"] } } });

Setup for React/Remix

npm install -D @testing-library/react @testing-library/jest-dom @testing-library/user-event

// test/setup.ts import "@testing-library/jest-dom"; import { cleanup } from "@testing-library/react"; import { afterEach } from "vitest";

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

Unit Testing Patterns

Pattern 1: Testing Pure Functions

// src/lib/utils.ts export function calculateTotal(items: CartItem[]): number { return items.reduce((sum, item) => sum + item.price * item.quantity, 0); }

// test/lib/utils.test.ts import { describe, test, expect } from "vitest"; import { calculateTotal } from "../../src/lib/utils";

describe("calculateTotal", () => { test("returns 0 for empty cart", () => { expect(calculateTotal([])).toBe(0); });

test("calculates total for single item", () => { const items = [{ price: 10, quantity: 2 }]; expect(calculateTotal(items)).toBe(20); });

test("calculates total for multiple items", () => { const items = [ { price: 10, quantity: 2 }, { price: 5, quantity: 3 } ]; expect(calculateTotal(items)).toBe(35); });

test("handles decimal prices", () => { const items = [{ price: 9.99, quantity: 2 }]; expect(calculateTotal(items)).toBeCloseTo(19.98); }); });

Pattern 2: Testing with Mocks

// src/lib/email.ts import { SESClient, SendEmailCommand } from "@aws-sdk/client-ses";

export async function sendWelcomeEmail(email: string, name: string) { const ses = new SESClient({});

await ses.send(new SendEmailCommand({ Source: "noreply@example.com", Destination: { ToAddresses: [email] }, Message: { Subject: { Data: "Welcome!" }, Body: { Text: { Data: Hello ${name}! } } } })); }

// test/lib/email.test.ts import { describe, test, expect, vi } from "vitest"; import { sendWelcomeEmail } from "../../src/lib/email"; import { SESClient } from "@aws-sdk/client-ses";

vi.mock("@aws-sdk/client-ses", () => ({ SESClient: vi.fn(), SendEmailCommand: vi.fn() }));

describe("sendWelcomeEmail", () => { test("sends email with correct parameters", async () => { const mockSend = vi.fn().mockResolvedValue({}); (SESClient as any).mockImplementation(() => ({ send: mockSend }));

await sendWelcomeEmail("user@example.com", "John");

expect(mockSend).toHaveBeenCalledWith(
  expect.objectContaining({
    input: expect.objectContaining({
      Destination: { ToAddresses: ["user@example.com"] }
    })
  })
);

}); });

Pattern 3: Testing Async Functions

// src/lib/users.ts export async function getUser(id: string): Promise<User | null> { const result = await db.query({ TableName: Resource.Database.name, KeyConditionExpression: "pk = :pk", ExpressionAttributeValues: { ":pk": USER#${id} } });

return result.Items?.[0] as User | null; }

// test/lib/users.test.ts import { describe, test, expect, beforeEach, vi } from "vitest"; import { getUser } from "../../src/lib/users";

// Mock the database vi.mock("../../src/lib/db", () => ({ db: { query: vi.fn() } }));

import { db } from "../../src/lib/db";

describe("getUser", () => { beforeEach(() => { vi.clearAllMocks(); });

test("returns user when found", async () => { const mockUser = { pk: "USER#123", name: "John" }; (db.query as any).mockResolvedValue({ Items: [mockUser] });

const user = await getUser("123");
expect(user).toEqual(mockUser);

});

test("returns null when not found", async () => { (db.query as any).mockResolvedValue({ Items: [] });

const user = await getUser("123");
expect(user).toBeNull();

});

test("throws on database error", async () => { (db.query as any).mockRejectedValue(new Error("DB Error"));

await expect(getUser("123")).rejects.toThrow("DB Error");

}); });

Testing Remix Routes

Pattern 1: Testing Loaders

// app/routes/posts.$id.tsx export async function loader({ params }: LoaderFunctionArgs) { const post = await getPost(params.id); if (!post) { throw new Response("Not Found", { status: 404 }); } return json({ post }); }

// test/routes/posts.$id.test.ts import { describe, test, expect, vi } from "vitest"; import { loader } from "../../app/routes/posts.$id";

vi.mock("../../src/lib/posts", () => ({ getPost: vi.fn() }));

import { getPost } from "../../src/lib/posts";

describe("posts.$id loader", () => { test("returns post data when found", async () => { const mockPost = { id: "123", title: "Test Post" }; (getPost as any).mockResolvedValue(mockPost);

const response = await loader({
  params: { id: "123" },
  request: new Request("http://localhost/posts/123")
} as any);

const data = await response.json();
expect(data.post).toEqual(mockPost);

});

test("throws 404 when post not found", async () => { (getPost as any).mockResolvedValue(null);

await expect(
  loader({
    params: { id: "123" },
    request: new Request("http://localhost/posts/123")
  } as any)
).rejects.toThrow("Not Found");

}); });

Pattern 2: Testing Actions

// app/routes/posts.new.tsx export async function action({ request }: ActionFunctionArgs) { const formData = await request.formData(); const title = formData.get("title"); const content = formData.get("content");

if (!title || !content) { return json({ errors: { title: "Required", content: "Required" } }); }

const post = await createPost({ title, content }); return redirect(/posts/${post.id}); }

// test/routes/posts.new.test.ts import { describe, test, expect, vi } from "vitest"; import { action } from "../../app/routes/posts.new";

vi.mock("../../src/lib/posts", () => ({ createPost: vi.fn() }));

import { createPost } from "../../src/lib/posts";

describe("posts.new action", () => { test("creates post and redirects on success", async () => { const mockPost = { id: "123", title: "Test", content: "Content" }; (createPost as any).mockResolvedValue(mockPost);

const formData = new FormData();
formData.append("title", "Test");
formData.append("content", "Content");

const response = await action({
  request: new Request("http://localhost/posts/new", {
    method: "POST",
    body: formData
  })
} as any);

expect(response.status).toBe(302);
expect(response.headers.get("Location")).toBe("/posts/123");

});

test("returns errors for invalid data", async () => { const formData = new FormData(); formData.append("title", ""); formData.append("content", "");

const response = await action({
  request: new Request("http://localhost/posts/new", {
    method: "POST",
    body: formData
  })
} as any);

const data = await response.json();
expect(data.errors).toEqual({
  title: "Required",
  content: "Required"
});

}); });

Testing React Components

Pattern 1: Simple Component Tests

// app/components/PostCard.tsx export function PostCard({ post }: { post: Post }) { return ( <article> <h2>{post.title}</h2> <p>{post.content}</p> <time>{post.createdAt}</time> </article> ); }

// test/components/PostCard.test.tsx import { describe, test, expect } from "vitest"; import { render, screen } from "@testing-library/react"; import { PostCard } from "../../app/components/PostCard";

describe("PostCard", () => { test("renders post information", () => { const post = { id: "123", title: "Test Post", content: "This is content", createdAt: "2025-01-02" };

render(&#x3C;PostCard post={post} />);

expect(screen.getByText("Test Post")).toBeInTheDocument();
expect(screen.getByText("This is content")).toBeInTheDocument();
expect(screen.getByText("2025-01-02")).toBeInTheDocument();

}); });

Pattern 2: Interactive Components

// app/components/Counter.tsx import { useState } from "react";

export function Counter() { const [count, setCount] = useState(0);

return ( <div> <p>Count: {count}</p> <button onClick={() => setCount(count + 1)}>Increment</button> <button onClick={() => setCount(0)}>Reset</button> </div> ); }

// test/components/Counter.test.tsx import { describe, test, expect } from "vitest"; import { render, screen } from "@testing-library/react"; import userEvent from "@testing-library/user-event"; import { Counter } from "../../app/components/Counter";

describe("Counter", () => { test("increments count", async () => { const user = userEvent.setup(); render(<Counter />);

expect(screen.getByText("Count: 0")).toBeInTheDocument();

await user.click(screen.getByText("Increment"));
expect(screen.getByText("Count: 1")).toBeInTheDocument();

await user.click(screen.getByText("Increment"));
expect(screen.getByText("Count: 2")).toBeInTheDocument();

});

test("resets count", async () => { const user = userEvent.setup(); render(<Counter />);

await user.click(screen.getByText("Increment"));
await user.click(screen.getByText("Increment"));
expect(screen.getByText("Count: 2")).toBeInTheDocument();

await user.click(screen.getByText("Reset"));
expect(screen.getByText("Count: 0")).toBeInTheDocument();

}); });

Testing SST Functions

Pattern 1: Testing Lambda Handlers

// src/api/posts.ts import { APIGatewayProxyEventV2, APIGatewayProxyResultV2 } from "aws-lambda"; import { getPosts } from "../lib/posts";

export async function handler( event: APIGatewayProxyEventV2 ): Promise<APIGatewayProxyResultV2> { const posts = await getPosts();

return { statusCode: 200, body: JSON.stringify({ posts }) }; }

// test/api/posts.test.ts import { describe, test, expect, vi } from "vitest"; import { handler } from "../../src/api/posts";

vi.mock("../../src/lib/posts", () => ({ getPosts: vi.fn() }));

import { getPosts } from "../../src/lib/posts";

describe("posts handler", () => { test("returns posts", async () => { const mockPosts = [{ id: "1", title: "Post 1" }]; (getPosts as any).mockResolvedValue(mockPosts);

const result = await handler({} as any);

expect(result.statusCode).toBe(200);
expect(JSON.parse(result.body!)).toEqual({ posts: mockPosts });

});

test("handles errors", async () => { (getPosts as any).mockRejectedValue(new Error("DB Error"));

const result = await handler({} as any);

expect(result.statusCode).toBe(500);

}); });

Integration Testing

Pattern 1: Testing with Real Database

// test/integration/posts.test.ts import { describe, test, expect, beforeAll, afterAll } from "vitest"; import { DynamoDBClient } from "@aws-sdk/client-dynamodb"; import { DynamoDBDocumentClient } from "@aws-sdk/lib-dynamodb"; import { createPost, getPost } from "../../src/lib/posts";

// Use local DynamoDB for tests const client = DynamoDBDocumentClient.from( new DynamoDBClient({ endpoint: "http://localhost:8000" }) );

describe("Post operations", () => { beforeAll(async () => { // Create test table await createTestTable(); });

afterAll(async () => { // Clean up test table await deleteTestTable(); });

test("creates and retrieves post", async () => { const postData = { title: "Test Post", content: "This is a test", authorId: "user123" };

const created = await createPost(postData);
expect(created.id).toBeDefined();
expect(created.title).toBe(postData.title);

const retrieved = await getPost(created.id);
expect(retrieved).toEqual(created);

}); });

E2E Testing with Playwright

// e2e/auth.spec.ts import { test, expect } from "@playwright/test";

test("user can sign up and log in", async ({ page }) => { // Sign up await page.goto("/signup"); await page.fill('input[name="email"]', "test@example.com"); await page.fill('input[name="password"]', "password123"); await page.fill('input[name="name"]', "Test User"); await page.click('button[type="submit"]');

// Should redirect to dashboard await expect(page).toHaveURL("/dashboard"); await expect(page.locator("text=Welcome, Test User")).toBeVisible();

// Log out await page.click('button:has-text("Log out")'); await expect(page).toHaveURL("/");

// Log in await page.goto("/login"); await page.fill('input[name="email"]', "test@example.com"); await page.fill('input[name="password"]', "password123"); await page.click('button[type="submit"]');

// Should be logged in await expect(page).toHaveURL("/dashboard"); });

Test Best Practices

  1. Use Descriptive Test Names

✅ Do:

test("returns 404 when post not found", () => {}); test("creates user and sends welcome email", () => {});

❌ Don't:

test("test1", () => {}); test("works", () => {});

  1. Follow AAA Pattern

test("updates post title", async () => { // Arrange const post = await createPost({ title: "Old Title" });

// Act await updatePost(post.id, { title: "New Title" });

// Assert const updated = await getPost(post.id); expect(updated.title).toBe("New Title"); });

  1. Test One Thing Per Test

✅ Do:

test("validates email format", () => {}); test("validates email uniqueness", () => {});

❌ Don't:

test("validates email", () => { // Tests 5 different things });

  1. Mock External Dependencies

// Mock AWS services vi.mock("@aws-sdk/client-s3");

// Mock environment variables vi.stubEnv("API_KEY", "test-key");

// Mock time vi.useFakeTimers(); vi.setSystemTime(new Date("2025-01-02"));

  1. Clean Up After Tests

import { afterEach, beforeEach } from "vitest";

beforeEach(() => { // Set up test data });

afterEach(() => { // Clean up vi.clearAllMocks(); vi.restoreAllMocks(); });

Coverage Goals

Aim for:

  • Unit tests: 80%+ coverage

  • Integration tests: Key workflows

  • E2E tests: Critical user paths

Run tests with coverage

npm test -- --coverage

Set minimum coverage

vitest.config.ts: coverage: { lines: 80, functions: 80, branches: 75, statements: 80 }

CI/CD Integration

.github/workflows/test.yml

name: Test

on: [push, pull_request]

jobs: test: runs-on: ubuntu-latest steps: - uses: actions/checkout@v3 - uses: actions/setup-node@v3 - run: npm install - run: npm test - run: npm run test:e2e

Further Reading

Source Transparency

This detail page is rendered from real SKILL.md content. Trust labels are metadata-based hints, not a safety guarantee.

Related Skills

Related by shared tags or category signals.

General

frontend-design

No summary provided by upstream source.

Repository SourceNeeds Review
General

email-templates

No summary provided by upstream source.

Repository SourceNeeds Review
General

marketing-copy

No summary provided by upstream source.

Repository SourceNeeds Review
General

conform

No summary provided by upstream source.

Repository SourceNeeds Review