# Console Tests

> Write tests for the akash-network/console monorepo following established team patterns and reviewer expectations. Use this skill whenever you need to write, fix, review, or refactor tests in the console project — including unit tests, functional tests, integration tests, or E2E tests for both frontend (deploy-web) and backend (api, notifications, indexer, provider-proxy). Also trigger when the user mentions 'write tests', 'add tests', 'fix tests', 'test this', 'spec file', or asks about testing patterns in the console codebase. When in doubt about whether to use this skill for a testing task in this repo, USE IT.

- Skill: `akash-network/console-tests` (Agent Skill, multi-file: 3 files)
- Install (CLI): `npx skillmds@latest add akash-network/console-tests`
- Raw SKILL.md: https://api.skillmd.com/api/skills/akash-network/console-tests/raw
- Safety review: pending
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: Web & Frontend
- Author: akash-network (https://skillmd.com/u/akash-network)
- Updated: 2026-09-17
- Page: https://skillmd.com/skills/akash-network/console-tests

---


# Console Test Writing Guide

Testing conventions and patterns for the akash-network/console monorepo.

## Deciding What Type of Test to Write

Choose the lowest-effective test level:

| Level | When to use in this repo | Mocking strategy |
|-------|-------------------------|-----------------|
| **Unit** (default) | Components, hooks, services, utilities | All deps mocked via DI (`mock<T>()`) — never `vi.mock()` |
| **Integration** | Services with heavy DB logic or Repository patterns; **required for every pg-boss job handler** | Real DB fixtures; mock only 3rd-party calls |
| **Functional** | Black-box HTTP endpoint verification | `nock` for external calls only; never mock internal services |
| **E2E** | Post-deployment verification (beta/prod) | No mocks; happy path only; Playwright with semantic locators |

Functional tests should only fail when functional requirements change, not from refactoring. Don't write functional tests for simple routes — test the service layer directly.

## Universal Rules (All Test Types)

### The `setup` Function Pattern

This is the most important convention. Never use `beforeEach` with shared mutable variables.

- Define a `setup()` function at the **bottom** of the root `describe` block
- It constructs the unit under test and returns it along with all mocked dependencies
- It accepts a **single parameter** with an **inline type definition**
- Do **not** specify the return type — let TypeScript infer it
- Each test calls `setup()` independently — no shared mutable state across tests

```typescript
describe(BalancesService.name, () => {
  it("returns balances for a given address", async () => {
    const { service, balanceRepository } = setup();
    balanceRepository.findByAddress.mockResolvedValue([{ denom: "uakt", amount: "1000" }]);

    const result = await service.getBalances("akash1abc...");

    expect(result).toEqual([{ denom: "uakt", amount: "1000" }]);
  });

  it("returns empty array when no balances found", async () => {
    const { service, balanceRepository } = setup();
    balanceRepository.findByAddress.mockResolvedValue([]);

    const result = await service.getBalances("akash1xyz...");

    expect(result).toEqual([]);
  });

  function setup(input?: { customConfig?: Partial<BalancesConfig> }) {
    const balanceRepository = mock<BalanceRepository>();
    const config = mockConfigService<BalancesConfig>({ ...input?.customConfig });
    const service = new BalancesService(balanceRepository, config);
    return { service, balanceRepository, config };
  }
});
```

Skip `setup` only if creating the object under test is trivially simple (e.g., testing a pure function with no dependencies).

### Test Description Conventions

**Root `describe`**: Use `SubjectUnderTest.name` (the actual reference, not a string). This enables IDE refactoring and reference-finding.

```typescript
// Good
describe(StripeWebhookService.name, () => { ... });

// Bad
describe("StripeWebhookService", () => { ... });
```

**Nested `describe`**: Use either a method name or a `"when ..."` condition.

```typescript
describe(StripeWebhookService.name, () => {
  describe("handleChargeRefunded", () => { ... });
  describe("when payment method is missing", () => { ... });
});
```

**`it` blocks**: Use present simple, 3rd person singular. Do NOT prepend with "should".

```typescript
// Good
it("returns early when customer ID is missing", async () => { ... });
it("updates both limits and isTrialing when endTrial is true", async () => { ... });

// Bad
it("should return early when customer ID is missing", async () => { ... });
```

### Mocking: `mock<T>()` from `vitest-mock-extended`

Always use `mock<T>()` for typed mocks. Never use `jest.mock()` or `vi.mock()` for module-level mocking — it causes OOM with heavy component trees and couples tests to implementation details.

```typescript
import { mock } from "vitest-mock-extended";

const userRepository = mock<UserRepository>();
const stripeService = mock<StripeService>();
const logger = mock<LoggerService>();
```

**Prefer `mock<T>()` over `as unknown as <Type>` for hand-built doubles.** When stubbing a hook return value or service that has many fields, build it with `mock<T>()` and override only the fields the test cares about. Hand-built objects coerced through `as unknown as` silently survive contract changes upstream and hide real regressions.

```typescript
// Good — stays in sync with the real type, lets the test focus on what matters
const useCustomUser: typeof DEPENDENCIES.useCustomUser = () =>
  mock<ReturnType<typeof DEPENDENCIES.useCustomUser>>({
    user: { username: "alice" },
    isLoading: false
  });

// Bad — `UseCustomUser` could grow a required field and this still compiles
const useCustomUser = () =>
  ({ user: { username: "alice" }, isLoading: false }) as unknown as ReturnType<typeof DEPENDENCIES.useCustomUser>;
```

Reserve `as unknown as` for the rare case where the type is structurally incompatible (e.g. branded types) and `mock<T>()` cannot satisfy it.

For config services, use `mockConfigService<T>()` (in `apps/api/test/mocks/config-service.mock.ts`):

```typescript
const billingConfig = mockConfigService<BillingConfigService>({
  DEPLOYMENT_GRANT_DENOM: "uakt",
  TRIAL_ALLOWANCE_EXPIRATION_DAYS: 14
});
```

### Vitest Imports

Always explicitly import from `vitest`:

```typescript
import { describe, expect, it, vi } from "vitest";
```

Explicit imports make dependencies visible and simplify TypeScript types.

### No `if` Statements in Assertions

Never use conditional logic in test expectations. If you need to narrow a type, use `as` assertions. Otherwise, skipped assertions silently pass when they shouldn't.

```typescript
// Good
const result = await service.validate(cert) as CertValidationResultError;
expect(result.ok).toBe(false);
expect(result.code).toBe("unknownCertificate");

// Bad
const result = await service.validate(cert);
if (!result.ok) {
  expect(result.code).toBe("unknownCertificate");
}
```

### Arrange-Act-Assert (AAA)

Follow the AAA pattern. Setup prepares state; the test validates it. Don't mix concerns.

```typescript
it("creates a deployment grant", async () => {
  // Arrange
  const { service, grantService } = setup();
  grantService.create.mockResolvedValue(mockGrant);

  // Act
  const result = await service.createGrant("akash1abc...");

  // Assert
  expect(result).toEqual(mockGrant);
  expect(grantService.create).toHaveBeenCalledWith("akash1abc...");
});
```

### Test Error Paths the Code Handles

If a service catches and transforms errors, test those paths. Don't skip error coverage just because the happy path works — and flag unhandled error scenarios rather than ignoring them.

## Frontend Tests (deploy-web)

Read @references/frontend-patterns.md for the full set of frontend-specific patterns including the DEPENDENCIES pattern, hook testing, query testing, and container testing.

Key points:
- Use `getBy*` for presence assertions (`toBeInTheDocument()`), and `queryBy*` for absence assertions (`not.toBeInTheDocument()`)
- Use the `DEPENDENCIES` export + `dependencies` prop for component DI (never `vi.mock`) — this covers components, hooks, and any other heavy imports
- Use `MockComponents()` helper to auto-mock dependencies
- Services are injected via `useServices` hook, not via `DEPENDENCIES` prop
- Use `setupQuery()` utility for React Query hook tests
- Use `renderHook` from `@testing-library/react` for hook tests

## API Unit Tests

Read @references/api-patterns.md for the full set of API-specific patterns including service testing, config mocking, and seeder patterns.

Key points:
- Construct services manually with `mock<T>()` dependencies
- Use seeders for test data (`apps/api/test/seeders/`)
- Use function-based seeders (not class-based)
- Use `@faker-js/faker` for randomized data in seeders
- Seeder accepts `Partial<T>` overrides with sensible defaults

## Background Job Handlers (apps/api)

A pg-boss job handler needs an **integration** test (`*.handler.integration.ts`), not a unit spec. A unit spec cannot cover it, because the two ways a job fails in production are both invisible to mocks:

- the worker installs a fake system user and an **empty** CASL ability, so any ability-scoped repository call throws `ForbiddenError` — the job then dies after its retries with no user-facing error
- a query that matches no rows reports success while doing nothing

Mocked repositories consult no ability and return whatever the test told them to, so they cannot produce either outcome.

Rules:

- Run the handler through the real worker — `useJobWorkers` + `startWorkers`, never `handler.handle(payload)` alone. Calling `handle` directly bypasses the worker context and tests an execution environment that does not exist in production.
- Assert on **end state** (rows, job rows), never on `expect(repo.method).toHaveBeenCalled()`. A write that matched zero rows satisfies the spy and fails the row read.
- Wait with `expectJobCompleted(jobName)` rather than only waiting on a side effect, so a refused job reports its cause instead of timing out opaquely.
- Fake collaborators at the tsyringe boundary with `vi.spyOn(container.resolve(X), "m")`, or with `nock` for HTTP. `test:ci-setup` starts only the database and the mock OAuth2 server, so anything reaching notifications, provider-proxy, the tx signer, or KMS must be faked or the test dies on `ECONNREFUSED`.
- Keep the existing unit spec. Integration coverage supersedes it as the guarantee, but removing specs is separate work with its own coverage argument.

The shared helpers live in `apps/api/test/services/job-queue-harness.ts` and the DB seeders in `apps/api/test/seeders/db/`. `apps/api/src/deployment/services/delete-unbacked-deployment-setting/delete-unbacked-deployment-setting.handler.integration.ts` is the reference example.

An exception is fair for a handler that touches no table and makes no ability-scoped call — a pure pass-through to another service. Such a test asserts a spy was called after a queue round trip, which tests the harness rather than the handler. Say so rather than writing it.

## API Functional Tests

Read @references/api-patterns.md for functional test setup details.

Key points:
- **Whitebox seeding, blackbox testing**: Seed data at the DB/repository level, but interact only through HTTP
- Each spec file gets its own database via `TestDatabaseService` (auto-created, migrated, dropped)
- Don't resolve controllers/services from the DI container — don't call application services or other endpoints to set up state
- Mock external HTTP calls with `nock`, not internal services
- Use a plain HTTP client (`fetch()`-based helpers) for Hono apps, `supertest` for NestJS — avoid framework-internal methods like `app.request()`
- Each test verifies a single endpoint's behavior in isolation
- Use function-based seeders to create test fixtures in the real database
- Write race condition tests for upsert operations

## Notifications Tests (NestJS)

- Use `@nestjs/testing` `Test.createTestingModule()` for DI
- Use `MockProvider()` helper to create NestJS providers with `vitest-mock-extended` mocks
- Functional tests use `supertest` with a real NestJS app and per-file test databases

## E2E / Playwright Tests

- Use semantic locators: `getByRole`, `getByLabel`, `getByPlaceholder` — never CSS selectors or `data-testid`
- `getByRole('button', { name: /Submit/ })` — don't add redundant `aria-label` to buttons that already have text
- Page Objects abstract UI interactions but must NOT contain assertions
- Use `waitFor(...)` instead of `setTimeout` — timeouts cause flakiness
- No `console.log` or manual screenshots — use Playwright's built-in traces (`npx playwright --ui`)
- No randomness in test data — it causes flaky failures
- Use `Promise.all` with `context.waitForEvent("page")` for navigation patterns

## Test File Organization

- Test files are co-located with source: `my-service.ts` → `my-service.spec.ts`
- Frontend: `*.spec.tsx` for components, `*.spec.ts` for logic
- API unit: `src/**/*.spec.ts`
- API integration: `src/**/*.integration.ts`
- API functional: `test/functional/**/*.spec.ts`
- API e2e: `test/e2e/**/*.spec.ts`
- Seeders: `test/seeders/` in each app

## After Writing Tests

Verify before committing:

1. **Run the test suite** in the affected app: `npm test` (or `npm run test:unit` / `npm run test:functional` depending on scope)
2. **Check for type errors**: `npx tsc --noEmit` in the app directory
3. **Run the linter**: `npm run lint -- --quiet`
4. **Review test output** — ensure no skipped tests, no unexpected console warnings, and all assertions are meaningful (not just `expect(true).toBe(true)`)
5. **Verify coverage** — the new test should cover the behavior described in the issue or PR, not just hit lines

