testing

par redis

Unit/integration testing standards for RedisInsight using Jest and Testing Library: test structure, the `renderComponent` helper, faker for test data, mocking…

npx skills add https://github.com/redis/redisinsight --skill testing

Testing Standards and Practices

Core Principles

  • Write tests for all new features
  • Follow AAA pattern: Arrange, Act, Assert
  • Use descriptive test names: "should do X when Y"
  • CRITICAL: Never use fixed time waits - tests must be deterministic
  • CRITICAL: Use faker library (@faker-js/faker) for test data

Test Organization

describe('FeatureService', () => {
  describe('findById', () => {
    it('should return entity when found', () => {});
    it('should throw NotFoundException when not found', () => {});
  });

  describe('create', () => {
    it('should create entity with valid data', () => {});
    it('should throw error with invalid data', () => {});
  });
});

Frontend Testing (Jest + Testing Library)

Running Specific Tests

# Run a specific test file
node 'node_modules/.bin/jest' 'redisinsight/ui/src/path/to/Component.spec.tsx' -c 'jest.config.cjs'

# Run a specific test by name (use -t flag)
node 'node_modules/.bin/jest' 'redisinsight/ui/src/path/to/Component.spec.tsx' -c 'jest.config.cjs' -t 'test name pattern'

# Example:
node 'node_modules/.bin/jest' 'redisinsight/ui/src/slices/tests/browser/keys.spec.ts' -c 'jest.config.cjs' -t 'refreshKeyInfoAction'

CRITICAL: Always Use Shared renderComponent Helper

Create a renderComponent helper for each component test file:

import { faker } from '@faker-js/faker';
// Pull structured data from a shared factory (redisinsight/ui/src/mocks/factories/);
// only build ad-hoc primitives/callbacks inline.

describe('MyComponent', () => {
  // Default props: factory for the entity, inline only for callbacks/primitives
  const mockUser = UserFactory.build();
  const defaultProps: MyComponentProps = {
    user: mockUser,
    onComplete: jest.fn(),
  };

  // Shared render helper
  const renderComponent = (propsOverride?: Partial<MyComponentProps>) => {
    const props = { ...defaultProps, ...propsOverride };

    return render(
      <Provider store={store}>
        <MyComponent {...props} />
      </Provider>
    );
  };

  beforeEach(() => {
    jest.clearAllMocks();
  });

  it('should render component', () => {
    renderComponent();
    expect(screen.getByText(defaultProps.name)).toBeInTheDocument();
  });

  it('should handle click', async () => {
    const mockOnComplete = jest.fn();
    renderComponent({ onComplete: mockOnComplete });

    fireEvent.click(screen.getByRole('button'));

    await waitFor(() => {
      expect(mockOnComplete).toHaveBeenCalledTimes(1);
    });
  });
});

Benefits:

  • Centralized setup (providers, router, theme)
  • Default props defined once
  • Easy prop overrides per test
  • No duplicate setup code

Complex Component Setup

For components requiring Router, ThemeProvider, etc., include them in renderComponent:

const renderComponent = (propsOverride?: Partial<Props>) => {
  const props = { ...defaultProps, ...propsOverride }

  return render(
    <Provider store={store}>
      <BrowserRouter>
        <ThemeProvider theme={theme}>
          <Component {...props} />
        </ThemeProvider>
      </BrowserRouter>
    </Provider>
  )
}

Testing with Redux

Create a test store with configureStore for Redux-connected components:

const createTestStore = (initialState = {}) => {
  return configureStore({
    reducer: { user: userSlice.reducer },
    preloadedState: initialState,
  });
};

const renderComponent = (propsOverride?: Partial<Props>, storeState = {}) => {
  const testStore = createTestStore(storeState);
  // render with testStore
};

Query Priorities (Testing Library)

Prefer accessible queries (as users would interact):

// ✅ PREFERRED
screen.getByRole('button', { name: /submit/i });
screen.getByLabelText('Email');
screen.getByPlaceholderText('Enter name');

// ⚠️ LAST RESORT
screen.getByTestId('user-profile');

// ❌ AVOID
wrapper.find('.button-class');

Testing Async Behavior

// ✅ GOOD: waitFor with proper queries
await waitFor(() => {
  expect(screen.getByText('Data loaded')).toBeInTheDocument();
});

// ✅ GOOD: waitForElementToBeRemoved
await waitForElementToBeRemoved(() => screen.queryByText('Loading...'));

// ✅ GOOD: findBy queries (built-in waiting)
const element = await screen.findByText('Async content');

// ❌ BAD: Fixed timeouts (flaky tests)
await new Promise((resolve) => setTimeout(resolve, 1000));

Mocking API Calls (MSW)

Use Mock Service Worker for API mocking:

import { rest } from 'msw';
import { setupServer } from 'msw/node';

const server = setupServer(
  rest.get('/api/users/:id', (req, res, ctx) => {
    return res(
      ctx.json({
        id: req.params.id,
        name: faker.person.fullName(),
      }),
    );
  }),
);

beforeAll(() => server.listen());
afterEach(() => server.resetHandlers());
afterAll(() => server.close());

Backend Testing (NestJS/Jest)

Service Test Pattern

import { Factory } from 'fishery';
import { faker } from '@faker-js/faker';

// Define factory for User entity
const userFactory = Factory.define<User>(() => ({
  id: faker.string.uuid(),
  name: faker.person.fullName(),
  email: faker.internet.email(),
}));

describe('UserService', () => {
  let service: UserService;
  let repository: Repository<User>;

  const mockRepository = {
    find: jest.fn(),
    findOne: jest.fn(),
    save: jest.fn(),
    update: jest.fn(),
    delete: jest.fn(),
  };

  beforeEach(async () => {
    const module = await Test.createTestingModule({
      providers: [
        UserService,
        {
          provide: getRepositoryToken(User),
          useValue: mockRepository,
        },
      ],
    }).compile();

    service = module.get<UserService>(UserService);
    repository = module.get<Repository<User>>(getRepositoryToken(User));
  });

  afterEach(() => {
    jest.clearAllMocks();
  });

  it('should return user when found', async () => {
    const mockUser = userFactory.build();
    mockRepository.findOne.mockResolvedValue(mockUser);

    const result = await service.findById(mockUser.id);

    expect(result).toEqual(mockUser);
  });
});

Controller Test Pattern

import { Factory } from 'fishery';
import { faker } from '@faker-js/faker';

const userFactory = Factory.define<User>(() => ({
  id: faker.string.uuid(),
  name: faker.person.fullName(),
  email: faker.internet.email(),
}));

describe('UserController', () => {
  let controller: UserController;
  let service: UserService;

  const mockService = {
    findAll: jest.fn(),
    findById: jest.fn(),
    create: jest.fn(),
  };

  beforeEach(async () => {
    const module = await Test.createTestingModule({
      controllers: [UserController],
      providers: [{ provide: UserService, useValue: mockService }],
    }).compile();

    controller = module.get<UserController>(UserController);
  });

  it('should return user from service', async () => {
    const mockUser = userFactory.build();
    mockService.findById.mockResolvedValue(mockUser);

    const result = await controller.findById(mockUser.id);

    expect(result).toEqual(mockUser);
  });
});

Integration Tests (E2E)

describe('UserController (e2e)', () => {
  let app: INestApplication;

  beforeAll(async () => {
    const module = await Test.createTestingModule({
      imports: [AppModule],
    }).compile();

    app = module.createNestApplication();
    await app.init();
  });

  afterAll(async () => {
    await app.close();
  });

  it('/users (GET)', () => {
    return request(app.getHttpServer())
      .get('/users')
      .expect(200)
      .expect((res) => {
        expect(Array.isArray(res.body)).toBe(true);
      });
  });
});

E2E Testing (Playwright)

import { Factory } from 'fishery';
import { faker } from '@faker-js/faker';

const userDataFactory = Factory.define(() => ({
  name: faker.person.fullName(),
  email: faker.internet.email(),
}));

test.describe('User Management', () => {
  test('should create new user', async ({ page }) => {
    const userData = userDataFactory.build();

    await page.goto('/users');
    await page.click('text=Add User');
    await page.fill('[name="name"]', userData.name);
    await page.fill('[name="email"]', userData.email);
    await page.click('text=Submit');

    // ✅ Use proper waits
    await expect(page.locator(`text=${userData.name}`)).toBeVisible();
  });
});

Best Practices

Always Use Faker for Test Data

// ✅ GOOD: Use faker
const user = {
  id: faker.string.uuid(),
  name: faker.person.fullName(),
  email: faker.internet.email(),
  age: faker.number.int({ min: 18, max: 100 }),
};

// ❌ BAD: Hardcoded data
const user = { id: '123', name: 'Test User' };

Use Factories Instead of Static Mocks

Use Fishery for creating test data factories with sensible defaults and overrides.

CRITICAL — UI factories live in redisinsight/ui/src/mocks/factories/, organized by domain (<domain>/<TypeName>.factory.ts), exported as <TypeName>Factory. Before writing test data in a UI spec, check for an existing factory there and reuse it; if none fits, add a new factory file in that directory rather than inlining mocks in the spec.

// ✅ GOOD: reuse the shared factory (redisinsight/ui/src/mocks/factories/database/DBInstance.factory.ts)
import { DBInstanceFactory } from 'uiSrc/mocks/factories/database/DBInstance.factory';

const instance = DBInstanceFactory.build();
const cluster = DBInstanceFactory.build({
  connectionType: ConnectionType.Cluster,
});
const many = DBInstanceFactory.buildList(5);

// ✅ GOOD: a new factory file, when none exists yet
// redisinsight/ui/src/mocks/factories/<domain>/<TypeName>.factory.ts
import { Factory } from 'fishery';
import { faker } from '@faker-js/faker';

const userFactory = Factory.define<User>(({ sequence }) => ({
  id: faker.string.uuid(),
  name: faker.person.fullName(),
  email: faker.internet.email(),
  age: faker.number.int({ min: 18, max: 100 }),
}));

// Usage - flexible and reusable
const user1 = userFactory.build();
const user2 = userFactory.build({ age: 25 });
const user3 = userFactory.build({ name: 'Specific Name' });
const users = userFactory.buildList(5); // Create multiple

// ❌ BAD: Static mock objects
const mockUser1 = {
  id: '123',
  name: 'User 1',
  email: 'user1@test.com',
  age: 30,
};

Benefits of Fishery factories:

  • Easy to override specific properties per test
  • Consistent default values across tests
  • Single source of truth for mock structure
  • Better maintainability when types change
  • Built-in support for sequences and traits

Never Use Fixed Timeouts

// ❌ BAD: Fixed timeout
await new Promise((resolve) => setTimeout(resolve, 1000));
await page.waitForTimeout(2000);

// ✅ GOOD: Wait for condition
await waitFor(() => {
  expect(element).toBeInTheDocument();
});

await page.waitForSelector('[data-test="result"]');

Mock External Dependencies

// ✅ GOOD: Mock services
jest.mock('uiSrc/services/api', () => ({
  apiService: {
    get: jest.fn(),
    post: jest.fn(),
  },
}));

Parameterized Tests with it.each

Use it.each for multiple tests with the same body but different inputs:

// ✅ GOOD: Parameterized tests
it.each([
  { description: 'null', value: null },
  { description: 'undefined', value: undefined },
  { description: 'empty string', value: '' },
  { description: 'whitespace only', value: '   ' },
])('should return error when input is $description', async ({ value }) => {
  const result = await service.processInput(value);
  expect(result.status).toBe('error');
});

Benefits:

  • DRY: Single test body shared across all cases
  • Maintainability: Changes to test logic only need to be made once
  • Readability: Test cases are clearly defined in a table
  • Easier to extend: Adding new test cases is just adding a new row

Test Edge Cases

Always test:

  • Empty arrays/objects
  • Null/undefined values
  • Error scenarios
  • Boundary conditions
  • Loading states

Testing Checklist

  • All new features have tests
  • Tests use faker for data generation
  • No fixed timeouts (use waitFor)
  • Tests follow AAA pattern
  • Descriptive test names
  • Shared renderComponent helper used
  • Default props defined
  • Edge cases covered
  • Error scenarios tested
  • Mocks cleaned up between tests
  • Integration tests for API endpoints
  • E2E tests for critical flows
  • Coverage meets thresholds (80%+)

Plus de skills de redis

docs-sync
redis
Analyser l'implémentation et la configuration de la branche master pour trouver la documentation manquante, incorrecte ou obsolète dans docs/, README.md et les READMEs par package. Utiliser…
official
implement-command
redis
Add a new Redis command (or command variant) to node-redis end-to-end — the `<NAME>.ts` Command file, its registration with JSDoc in the package…
official
maintainer-review
redis
Examiner une URL de problème ou de demande de tirage GitHub en tant que mainteneur de node-redis, avec une évaluation progressive pour déterminer si la revendication est réelle, pratiquement importante, déjà…
official
pr-draft-summary
redis
Créez le bloc de résumé PR-ready requis, la suggestion de branche, le titre et la description de brouillon pour node-redis. Doit être utilisé avant la réponse finale chaque fois que le…
official
runtime-behavior-probe
redis
Planifier et exécuter des investigations sur le comportement d'exécution avec des scripts sonde TypeScript temporaires, des matrices de validation, des contrôles d'état et des rapports axés sur les résultats. Utiliser…
official
backend
redis
Modèles de développement backend NestJS pour l'API RedisInsight : structure des modules, services, contrôleurs, DTOs, injection de dépendances et gestion des erreurs. Utiliser lorsque…
official
branches
redis
Utiliser le kebab-case en minuscules avec le préfixe de type et l'identifiant du ticket. Les noms de branches doivent respecter les règles du workflow GitHub Actions (voir .github/workflows/enforce-branch-name-rules.yml).
official
code-quality
redis
Code-quality standards for RedisInsight: TypeScript strictness, naming conventions (camelCase, PascalCase, UPPER_SNAKE_CASE), linting rules, no `any` without…
official