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.zipInstalls 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.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:
| Principle | Description |
|---|---|
| Fast | Tests run quickly — milliseconds, not seconds |
| Independent | Tests don't depend on each other, can run in any order |
| Repeatable | Same result every time, no flakiness |
| Self-validating | Pass or fail, no manual inspection needed |
| Timely | Written at the same time as the code (or before with TDD) |
File Organization
Naming
| Type | Extension | Example |
|---|---|---|
| Unit test | .test.ts | Login.usecase.test.ts |
| E2E test (Maestro) | .yaml | login-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 Double | Use Case | Layer |
|---|---|---|
| Dummy | Placeholder, never actually used | Core, UI |
| Stub | Returns predefined data | Core, UI |
| Spy | Records calls, verifies interactions | Infrastructure |
| Mock | Spy + predefined behavior | Infrastructure |
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
| Type | File | Function |
|---|---|---|
| Entity | User.builder.ts | userBuilder() |
| Props | EventCard.props.builder.ts | eventCardPropsBuilder() |
| Params | LoginParams.builder.ts | loginParamsBuilder() |
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.*