home / skills / gilbertopsantosjr / fullstacknextjs / gs-create-e2e-tests

gs-create-e2e-tests skill

/skills/gs-create-e2e-tests

This skill helps you implement end-to-end testing with Clean Architecture using Vitest and DynamoDB Local across entity, use-case, repository, and E2E layers.

npx playbooks add skill gilbertopsantosjr/fullstacknextjs --skill gs-create-e2e-tests

Review the files below or copy the command above to add this skill to your agents.

Files (1)
SKILL.md
12.4 KB
---
name: gs-create-e2e-tests
description: Creates tests following Clean Architecture test layers. Entity tests (pure unit), Use Case tests (mock repositories), Repository tests (DynamoDB Local), E2E tests (full flow). Uses Vitest and DI Container mocking.
---

# Testing with Clean Architecture

## Test Layer Hierarchy

```
┌─────────────────────────────────────────┐
│ E2E Tests (Action → Use Case → DB)      │ Full flow
├─────────────────────────────────────────┤
│ Repository Tests (DynamoDB Local)       │ Infrastructure
├─────────────────────────────────────────┤
│ Use Case Tests (Mock Repositories)      │ Application
├─────────────────────────────────────────┤
│ Entity Tests (Pure Unit)                │ Domain
└─────────────────────────────────────────┘
```

## File Location Pattern

```
src/backend/domain/<feature>/
├── entities/
│   ├── <entity>.ts
│   └── <entity>.test.ts         # Entity unit tests

src/backend/application/<feature>/
├── use-cases/
│   ├── <use-case>.ts
│   └── <use-case>.test.ts       # Use Case tests (mock repos)

src/backend/infrastructure/<feature>/
├── repositories/
│   ├── <repo>-impl.ts
│   └── <repo>-impl.test.ts      # Repository integration tests

src/features/<feature>/
├── actions/
│   ├── <action>.ts
│   └── <action>.test.ts         # E2E tests (full flow)
```

## Technology Stack

| Component | Technology |
|-----------|------------|
| Test Runner | Vitest |
| Database | DynamoDB Local |
| Mocking | Vitest mocks + DI Container |
| Test Data | @faker-js/faker |

## 1. Entity Tests (Domain Layer)

Pure unit tests - no mocks, no I/O.

```typescript
// src/backend/domain/category/entities/category.test.ts
import { describe, it, expect } from 'vitest'
import { Category } from './category'
import { DomainException } from '@/backend/domain/shared/exceptions'

describe('Category Entity', () => {
  describe('create', () => {
    it('creates valid category', () => {
      const category = Category.create({
        id: 'cat_123',
        name: 'Electronics',
        userId: 'user_456',
      })

      expect(category.id).toBe('cat_123')
      expect(category.name).toBe('Electronics')
      expect(category.status).toBe('active')
    })

    it('throws on empty name', () => {
      expect(() =>
        Category.create({ id: 'cat_123', name: '', userId: 'user_456' })
      ).toThrow(DomainException)
    })

    it('throws on name exceeding max length', () => {
      expect(() =>
        Category.create({ id: 'cat_123', name: 'x'.repeat(256), userId: 'user_456' })
      ).toThrow(DomainException)
    })
  })

  describe('updateName', () => {
    it('updates name on valid category', () => {
      const category = Category.create({
        id: 'cat_123',
        name: 'Old Name',
        userId: 'user_456',
      })

      category.updateName('New Name')

      expect(category.name).toBe('New Name')
    })
  })

  describe('deactivate', () => {
    it('changes status to inactive', () => {
      const category = Category.create({
        id: 'cat_123',
        name: 'Electronics',
        userId: 'user_456',
      })

      category.deactivate()

      expect(category.status).toBe('inactive')
    })
  })
})
```

## 2. Use Case Tests (Application Layer)

Mock repository interfaces - test business logic only.

```typescript
// src/backend/application/category/use-cases/create-category.test.ts
import { describe, it, expect, vi, beforeEach } from 'vitest'
import { faker } from '@faker-js/faker'
import { CreateCategoryUseCase } from './create-category'
import type { ICategoryRepository } from '@/backend/domain/category/repositories'

describe('CreateCategoryUseCase', () => {
  let useCase: CreateCategoryUseCase
  let mockRepository: ICategoryRepository

  beforeEach(() => {
    mockRepository = {
      save: vi.fn(),
      findById: vi.fn(),
      findByUser: vi.fn(),
      delete: vi.fn(),
    }
    useCase = new CreateCategoryUseCase(mockRepository)
  })

  it('creates category and returns DTO', async () => {
    vi.mocked(mockRepository.save).mockResolvedValue(undefined)

    const result = await useCase.execute({
      name: 'Electronics',
      userId: 'user_456',
    })

    expect(result.name).toBe('Electronics')
    expect(result.id).toBeDefined()
    expect(mockRepository.save).toHaveBeenCalledTimes(1)
  })

  it('propagates domain exceptions', async () => {
    await expect(
      useCase.execute({ name: '', userId: 'user_456' })
    ).rejects.toThrow('Name is required')
  })

  it('propagates repository exceptions', async () => {
    vi.mocked(mockRepository.save).mockRejectedValue(
      new Error('Database error')
    )

    await expect(
      useCase.execute({ name: 'Valid', userId: 'user_456' })
    ).rejects.toThrow('Database error')
  })
})
```

## 3. Repository Tests (Infrastructure Layer)

Integration tests with DynamoDB Local.

### Test Database Helpers

```typescript
// src/test/db-helpers.ts
import { DynamoDBClient } from '@aws-sdk/client-dynamodb'
import { Table } from 'dynamodb-onetable'
import { schema } from '@/backend/infrastructure/database/schema'

const testClient = new DynamoDBClient({
  endpoint: process.env.DYNAMODB_LOCAL_ENDPOINT || 'http://localhost:8000',
  region: 'local',
  credentials: { accessKeyId: 'local', secretAccessKey: 'local' },
})

let testTable: Table

export const setupTestDb = async () => {
  testTable = new Table({
    client: testClient,
    name: process.env.TEST_TABLE_NAME || 'test-table',
    schema,
    partial: true,
  })

  try {
    await testTable.createTable()
  } catch (error: any) {
    if (error.name !== 'ResourceInUseException') throw error
  }
  return testTable
}

export const clearTestData = async (modelName: string) => {
  const Model = testTable.getModel(modelName)
  const items = await Model.scan({})
  for (const item of items) await Model.remove(item)
}

export const getTestTable = () => testTable
```

### Repository Test

```typescript
// src/backend/infrastructure/category/repositories/category-repository-impl.test.ts
import { describe, it, expect, beforeAll, beforeEach } from 'vitest'
import { faker } from '@faker-js/faker'
import { setupTestDb, clearTestData, getTestTable } from '@/test/db-helpers'
import { CategoryRepositoryImpl } from './category-repository-impl'
import { Category } from '@/backend/domain/category/entities'

describe('CategoryRepositoryImpl', () => {
  let repository: CategoryRepositoryImpl

  beforeAll(async () => {
    await setupTestDb()
    repository = new CategoryRepositoryImpl(getTestTable())
  })

  beforeEach(async () => {
    await clearTestData('Category')
  })

  describe('save', () => {
    it('persists category to database', async () => {
      const category = Category.create({
        id: faker.string.ulid(),
        name: 'Electronics',
        userId: 'user_456',
      })

      await repository.save(category)

      const found = await repository.findById(category.id, category.userId)
      expect(found?.name).toBe('Electronics')
    })
  })

  describe('findById', () => {
    it('returns null for non-existent category', async () => {
      const result = await repository.findById('non_existent', 'user_456')
      expect(result).toBeNull()
    })
  })

  describe('findByUser', () => {
    it('returns paginated results', async () => {
      const userId = faker.string.ulid()

      for (let i = 0; i < 5; i++) {
        const category = Category.create({
          id: faker.string.ulid(),
          name: `Category ${i}`,
          userId,
        })
        await repository.save(category)
      }

      const page1 = await repository.findByUser(userId, { limit: 2 })
      expect(page1.items).toHaveLength(2)
      expect(page1.nextCursor).toBeDefined()

      const page2 = await repository.findByUser(userId, {
        limit: 2,
        cursor: page1.nextCursor,
      })
      expect(page2.items).toHaveLength(2)
    })
  })
})
```

## 4. E2E Tests (Full Flow)

Test action → use case → repository → database.

```typescript
// src/features/category/actions/create-category.test.ts
import { describe, it, expect, beforeAll, beforeEach, vi } from 'vitest'
import { faker } from '@faker-js/faker'
import { setupTestDb, clearTestData } from '@/test/db-helpers'
import { createCategoryAction } from './create-category'

// Mock auth context
vi.mock('@saas4dev/auth', () => ({
  authServer: {
    api: {
      getSession: vi.fn().mockResolvedValue({ user: { id: 'user_123' } }),
    },
  },
}))

describe('createCategoryAction E2E', () => {
  beforeAll(async () => {
    await setupTestDb()
  })

  beforeEach(async () => {
    await clearTestData('Category')
  })

  it('creates category through full stack', async () => {
    const [result, err] = await createCategoryAction({ name: 'Electronics' })

    expect(err).toBeNull()
    expect(result?.name).toBe('Electronics')
    expect(result?.id).toBeDefined()
  })

  it('returns validation error for empty name', async () => {
    const [result, err] = await createCategoryAction({ name: '' })

    expect(result).toBeNull()
    expect(err).toBeDefined()
  })
})
```

## DI Container Mocking

For testing with DI Container, override registrations.

```typescript
// src/test/di-helpers.ts
import { DIContainer, TOKENS } from '@/backend/di'

export const mockRepository = <T>(token: symbol, mock: Partial<T>) => {
  const original = DIContainer.resolve(token)
  DIContainer.register(token, { useValue: mock as T })
  return () => DIContainer.register(token, { useValue: original })
}
```

```typescript
// Usage in tests
import { mockRepository } from '@/test/di-helpers'
import { TOKENS } from '@/backend/di'

describe('UseCase with mocked repo', () => {
  let restore: () => void

  beforeEach(() => {
    restore = mockRepository(TOKENS.CategoryRepository, {
      save: vi.fn(),
      findById: vi.fn().mockResolvedValue(null),
    })
  })

  afterEach(() => restore())

  it('uses mocked repository', async () => {
    // Test with mocked DI
  })
})
```

## Test Data Factory

```typescript
// src/test/factories/category-factory.ts
import { faker } from '@faker-js/faker'
import type { CategoryDTO } from '@/backend/application/category/dtos'

export const categoryFactory = {
  dto(overrides: Partial<CategoryDTO> = {}): CategoryDTO {
    return {
      id: faker.string.ulid(),
      name: faker.commerce.department(),
      status: 'active',
      userId: faker.string.ulid(),
      createdAt: new Date().toISOString(),
      updatedAt: new Date().toISOString(),
      ...overrides,
    }
  },

  createInput(overrides = {}) {
    return {
      name: faker.commerce.department(),
      ...overrides,
    }
  },
}
```

## Coverage by Layer

| Layer | Priority | Focus |
|-------|----------|-------|
| Entity | High | Business rules, validation |
| Use Case | High | Orchestration logic |
| Repository | Medium | Data persistence |
| Action | Low | Integration verification |

## CI/CD Integration

```yaml
name: Tests
on: [push, pull_request]

jobs:
  test:
    runs-on: ubuntu-latest
    services:
      dynamodb-local:
        image: amazon/dynamodb-local:latest
        ports:
          - 8000:8000

    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version: '20'
      - run: npm ci
      - run: npm test
        env:
          DYNAMODB_LOCAL_ENDPOINT: http://localhost:8000
```

## Running Tests

```bash
# All tests
npx vitest

# By layer
npx vitest src/backend/domain           # Entity tests
npx vitest src/backend/application      # Use Case tests
npx vitest src/backend/infrastructure   # Repository tests
npx vitest src/features                 # E2E tests

# With coverage
npx vitest --coverage
```

## Anti-Patterns

| Anti-Pattern | Correct Approach |
|--------------|-----------------|
| Testing Entity via database | Pure unit tests, no I/O |
| Mocking Entity internals | Mock Repository interface |
| Direct `new UseCase()` in tests | Use DI Container or explicit injection |
| Cross-feature imports in tests | Mock via DI Container |

## References

- Feature Architecture: `skills/feature-architecture/SKILL.md`
- DynamoDB OneTable: `skills/dynamodb-onetable/SKILL.md`

Overview

This skill generates a complete test suite following Clean Architecture test layers: Entity, Use Case, Repository, and E2E. It scaffolds Vitest-based tests with DI container mocking, DynamoDB Local integration for repository tests, and test data factories to accelerate reliable, maintainable tests. The goal is clear separation of concerns so each layer is tested at the right scope.

How this skill works

The skill creates test files in layer-specific locations and templates for each layer: pure unit entity tests, use case tests with mocked repositories, repository integration tests against DynamoDB Local, and E2E action tests exercising the full stack. It includes helpers for setting up/clearing a test DynamoDB table, DI container registry overrides for mocking, and factories for reproducible test data. Tests use Vitest, @faker-js/faker, and DynamoDB OneTable helpers.

When to use it

  • When establishing a testing strategy for a Clean Architecture backend
  • When adding new features and you need test templates per layer
  • When introducing DynamoDB persistence and you want integration test scaffolding
  • When you need DI-aware tests that replace infrastructure dependencies
  • When standardizing test patterns across a team or codebase

Best practices

  • Keep entity tests pure unit tests — no I/O or mocks
  • Mock repository interfaces in use case tests to exercise business logic only
  • Run repository tests against DynamoDB Local and clear test data between runs
  • Use DI container helpers to override and restore registrations in tests
  • Prefer test data factories for consistent, readable test inputs

Example use cases

  • Generate entity tests that assert validation rules and domain behavior
  • Create use case tests that verify orchestration and error propagation with mocked repos
  • Create repository integration tests that persist and query items in DynamoDB Local
  • Create E2E action tests that verify an action flows through use case and repository to DB
  • Provide CI job examples that spin up DynamoDB Local and run the full test suite

FAQ

Do repository tests require a running DynamoDB Local instance?

Yes — repository integration tests expect DynamoDB Local available (configurable endpoint). CI can start a DynamoDB Local service for tests.

How do I mock dependencies registered in the DI container?

Use the provided DI test helper to register a mock value for a token before the test and restore the original registration afterward.