agentskills.codes
RE

react-testing-conventions

Testing conventions for React Native with TypeScript. Use this skill when writing tests, reviewing test code, or setting up test infrastructure. Covers unit tests, integration tests, component tests, test doubles (dummy, stub, spy, mock), builder pattern for fixtures, and the testing pyramid.

Install

mkdir -p .claude/skills/react-testing-conventions && curl -L -o skill.zip "https://agentskills.codes/api/skills/download/14646" && unzip -o skill.zip -d .claude/skills/react-testing-conventions && rm skill.zip

Installs to .claude/skills/react-testing-conventions

Activation

This is the description your AI agent reads to decide when to run this skill — the better it matches your request, the more reliably it fires.

Testing conventions for React Native with TypeScript. Use this skill when writing tests, reviewing test code, or setting up test infrastructure. Covers unit tests, integration tests, component tests, test doubles (dummy, stub, spy, mock), builder pattern for fixtures, and the testing pyramid.
293 chars✓ has a “when” triggerlonger than Claude Code's old 250-char listing cap (fine on current versions)

About this skill

Testing Conventions

Testing conventions for React Native applications with TypeScript.

Stack: Jest • React Native Testing Library • TypeScript

Core Principles

Testing Pyramid

Respect the testing pyramid — more unit tests, fewer integration tests, even fewer E2E tests:

        /\
       /  \      E2E (Maestro)
      /----\     Few, slow, expensive
     /      \
    /--------\   Integration
   /          \  Some, medium speed
  /------------\
 /              \ Unit (*.test.ts)
/________________\ Many, fast, cheap

FIRST Principles

Tests must be:

PrincipleDescription
FastTests run quickly — milliseconds, not seconds
IndependentTests don't depend on each other, can run in any order
RepeatableSame result every time, no flakiness
Self-validatingPass or fail, no manual inspection needed
TimelyWritten at the same time as the code (or before with TDD)

File Organization

Naming

TypeExtensionExample
Unit test.test.tsLogin.usecase.test.ts
E2E test (Maestro).yamllogin-flow.yaml

Colocation (Unit & Integration)

Test files live next to the files they test:

modules/authentication/core/usecases/
├── Login.usecase.ts
└── Login.usecase.test.ts

modules/authentication/ui/viewModels/
├── useLogin.viewModel.tsx
└── useLogin.viewModel.test.ts

modules/authentication/infrastructure/adapters/
├── AuthApi.adapter.ts
└── AuthApi.adapter.test.ts

E2E Tests (Maestro)

E2E tests live in a dedicated folder at project root:

tests/
├── flows/
│   ├── authentication/
│   │   ├── login-flow.yaml
│   │   └── logout-flow.yaml
│   └── events/
│       ├── create-event-flow.yaml
│       └── delete-event-flow.yaml
└── utils/
    └── common-steps.yaml

Test Anatomy

AAA Pattern

Every test follows Arrange, Act, Assert:

it("should return user when credentials are valid", async () => {
  // Arrange
  const authRepository = new AuthRepositoryStub();
  const useCase = new LoginUseCase(authRepository);
  const credentials = { email: "[email protected]", password: "password123" };

  // Act
  const result = await useCase.execute(credentials);

  // Assert
  expect(result.success).toBe(true);
  expect(result.data.email).toBe("[email protected]");
});

Test Naming

Pattern: should [expected behavior] when [condition]

// ✅ Good
it("should return failure when email is invalid", ...)
it("should disable button when form is incomplete", ...)
it("should invalidate queries when mutation succeeds", ...)

// ❌ Bad
it("test login", ...)
it("works", ...)
it("handles error", ...)

One logical assertion per test

// ✅ Good — one behavior tested
it("should return failure when password is too short", async () => {
  const result = await useCase.execute({ email: "[email protected]", password: "123" });

  expect(result.success).toBe(false);
  expect(result.error.type).toBe("VALIDATION_ERROR");
});

// ❌ Bad — testing multiple unrelated behaviors
it("should validate all fields", async () => {
  // Testing email validation
  const result1 = await useCase.execute({ email: "", password: "valid123" });
  expect(result1.success).toBe(false);

  // Testing password validation
  const result2 = await useCase.execute({ email: "[email protected]", password: "" });
  expect(result2.success).toBe(false);

  // Testing success case
  const result3 = await useCase.execute({
    email: "[email protected]",
    password: "valid123",
  });
  expect(result3.success).toBe(true);
});

Test Doubles

When to use what

Test DoubleUse CaseLayer
DummyPlaceholder, never actually usedCore, UI
StubReturns predefined dataCore, UI
SpyRecords calls, verifies interactionsInfrastructure
MockSpy + predefined behaviorInfrastructure

Rule: Stub the Core, Mock the Infrastructure

// ✅ Core/UI tests — use stubs
// We test behavior, not implementation
const authRepository = new AuthRepositoryStub();
const useCase = new LoginUseCase(authRepository);

// ✅ Infrastructure tests — use spies/mocks
// We test the implementation itself
jest.spyOn(global, "fetch").mockResolvedValue(mockResponse);

Dummy

Never actually used, just satisfies the type:

class LoggerDummy implements Logger {
  log(_message: string): void {
    // Do nothing
  }
}

// Usage
const useCase = new SomeUseCase(realDependency, new LoggerDummy());

Stub

Returns predefined data:

class AuthRepositoryStub implements AuthRepository {
  async login(_params: LoginParams): Promise<Result<User, AuthError>> {
    return ok({
      id: "user-1",
      email: "[email protected]",
      displayName: "Test User",
    });
  }

  async logout(): Promise<Result<void, AuthError>> {
    return ok(undefined);
  }
}

// Configurable stub
class AuthRepositoryStub implements AuthRepository {
  private loginResult: Result<User, AuthError> = ok(userBuilder().build());

  withLoginSuccess(user: User): this {
    this.loginResult = ok(user);
    return this;
  }

  withLoginFailure(error: AuthError): this {
    this.loginResult = fail(error);
    return this;
  }

  async login(_params: LoginParams): Promise<Result<User, AuthError>> {
    return this.loginResult;
  }
}

// Usage
const stub = new AuthRepositoryStub().withLoginFailure({
  type: "INVALID_CREDENTIALS",
});

Spy / Mock (Infrastructure only)

// Testing an adapter's implementation
describe("AuthApiAdapter", () => {
  it("should call fetch with correct parameters", async () => {
    const fetchSpy = jest.spyOn(global, "fetch").mockResolvedValue({
      ok: true,
      json: async () => ({ id: "1", email: "[email protected]" }),
    } as Response);

    const adapter = new AuthApiAdapter();
    await adapter.login({ email: "[email protected]", password: "password" });

    expect(fetchSpy).toHaveBeenCalledWith(
      "https://api.example.com/auth/login",
      expect.objectContaining({
        method: "POST",
        body: JSON.stringify({
          email: "[email protected]",
          password: "password",
        }),
      })
    );
  });
});

Builder Pattern

Use builders to create test fixtures (entities, props, etc.):

Entity Builder

// modules/authentication/core/entities/User.builder.ts
import { User } from "./User.entity";

export const userBuilder = () => {
  const user: User = {
    id: "user-123",
    email: "[email protected]",
    displayName: "Default User",
    createdAt: "2024-01-01T00:00:00Z",
  };

  const builder = {
    id: (id: string) => {
      user.id = id;
      return builder;
    },
    email: (email: string) => {
      user.email = email;
      return builder;
    },
    displayName: (displayName: string) => {
      user.displayName = displayName;
      return builder;
    },
    createdAt: (createdAt: string) => {
      user.createdAt = createdAt;
      return builder;
    },
    build: () => user,
  };

  return builder;
};

// Usage
const user = userBuilder()
  .email("[email protected]")
  .displayName("Custom")
  .build();

Props Builder

// modules/events/ui/components/EventCard.props.builder.ts
import { EventCardProps } from "./EventCard";

export const eventCardPropsBuilder = () => {
  const props: EventCardProps = {
    title: "Default Event",
    date: "2024-06-15T10:00:00Z",
    attendees: 10,
    onPress: jest.fn(),
  };

  const builder = {
    title: (title: string) => {
      props.title = title;
      return builder;
    },
    date: (date: string) => {
      props.date = date;
      return builder;
    },
    attendees: (attendees: number) => {
      props.attendees = attendees;
      return builder;
    },
    onPress: (onPress: VoidFunction) => {
      props.onPress = onPress;
      return builder;
    },
    build: () => props,
  };

  return builder;
};

Builder naming

TypeFileFunction
EntityUser.builder.tsuserBuilder()
PropsEventCard.props.builder.tseventCardPropsBuilder()
ParamsLoginParams.builder.tsloginParamsBuilder()

Testing Hooks

Custom renderHook

Use the custom renderHook that provides dependencies. See references/test-utils.md for the full implementation.

// modules/app/react/renderHook.tsx
export function renderHook<Result, Props>(
  renderCallback: (props: Props) => Result,
  options?: {
    initialProps?: Props;
    wrapper?: ComponentType<{ children: ReactNode }>;
    dependencies?: Partial<Dependencies>;
  }
): RenderHookResult<Result, Props>;

Usage

import { act } from "@testing-library/react-native";
import { renderHook } from "@app/react/renderHook";
import { useLoginViewModel } from "./useLogin.viewModel";

describe("useLoginViewModel", () => {
  it("should update state to success when login succeeds", async () => {
    const authRepositoryStub = new AuthRepositoryStub().withLoginSuccess(
      userBuilder().build()
    );

    const { result } = renderHook(() => useLoginViewModel(), {
      dependencies: { authRepository: authRepositoryStub },
    });

    await act(async () => {
      await result.current.handlers.login("[email protected]", "password123");
    });

    expect(result.c

---

*Content truncated.*

Search skills

Search the agent skills registry