Галоўная / Артыкулы / Тэставанне Enterprise Node.js: называнне AAA, пакрыць і фабрыкі тэстаў

Тэставанне Enterprise Node.js: называнне AAA, пакрыць і фабрыкі тэстаў

Выучыце, як структураваць тэсты для Node.js за дапамою шаблона AAA, збалансаваць пакрыцча на адзінкавыя тэсты і тэсты інтеграцыі, пераканацца ў правільнасці пяці ключовых рэзультатаў домены, а таксама ізаліраваць тэсты за дапамою фабрык.

1401 слоў

Нават самая рэтельна архітектура коду з часам будзе падаць у якосці, якщо няма нічога, што бы автаматычна яе пераглядала. Калі команды растуць, а колькасць функцый расширваецца, надзейны комплекс тэстаў стае галоўным захопам прытаманных бядаў, якія могу з’явіцца ў працоўным сераверы.

У гэтым разе акцэнт ставіцца на практыкі тэставання высокага рангу для Node.js: як пісаць тэсты, якія ўсё чытна, як заставляць тэсты быць незалежнымі адзін ад другога, як знаходзіць правы баланс межу тэстамі на роўні елементаў і інтеграцыйнымі тэстамі, а таксама як паўнастаць пяць основных рэзультатаў, якія можа даць будзь-якая частка логіки домэны.

1. Структура тэстаў і называнне: шаблон AAA

Спрэчыце сваія тэсты як жывую дакументацыю правілаў бізнесу, якія яны павінны пераканаць. Калі ў ночы адбываецца падзея, якая выклікае абыякшчыну ў процесе CI/CD, той, хто на службе, павінен негайна з’ясаваць што не запрацавало, за якіх умов і што павінна была здарыцца ў замене.

Шаблон AAA (Arrange-Act-Assert)

Структуруйце кожны тэст так, каб ён складаўся з трох чыста аддзеленых этапаў:

┌─────────────────────────────────────────────────────────┐
│ 1. ARRANGE                                              │
│    Set up preconditions, create inputs, mock dependencies│
└───────────────────────────┬─────────────────────────────┘
                            │
                            ▼
┌─────────────────────────────────────────────────────────┐
│ 2. ACT                                                  │
│    Execute the single domain operation being tested      │
└───────────────────────────┬─────────────────────────────┘
                            │
                            ▼
┌─────────────────────────────────────────────────────────┐
│ 3. ASSERT                                               │
│    Verify returned results, DB state, and side-effects  │
└─────────────────────────────────────────────────────────┘

Стандарт называння описавых тэстаў

Ухіліцеся ад нечыярых пазначэнняў, такіх як it('works') або it('should test user service'). У замене выберыце правілу называння, якое з самага пачатку чытальна адбівае мету тэста:

given [прыяўнік/кантэкст], when [дзея], then [ранеецькі рынак]

Неправильны спосаб: загадковыя ваказанні тэстаў

// orders.service.test.js
describe('orders service', () => {
  it('creates order', async () => {
    // Everything mashed together, no context
    const res = await orderService.createOrder({ userId: '123', items: [] });
    expect(res).toBeDefined();
  });
});

Правільны спосаб: тэсты AAA, якія самі сабе документуюць

// components/orders/orders.service.test.js
const { orderService } = require('./orders.service');
const { ValidationError } = require('../../shared/errors/AppError');
const userFactory = require('../../../test/factories/user.factory');

describe('OrderService.createOrder', () => {
  it('given an empty cart, when creating an order, then it throws a ValidationError', async () => {
    // ARRANGE
    const user = await userFactory.build();
    const payload = { userId: user.id, items: [] };

    // ACT & ASSERT
    await expect(orderService.createOrder(payload))
      .rejects
      .toThrow(ValidationError);
  });
});

2. Тэсты на елементах проты тэстаў інтеграцыі: як знайсці правильны баланс

Адна з частаўых запытанняў у тэставанні на Node.js — гэта як распадзеліць зусілля межы тэстамі на елементах, якія запускаюцца быстра і сильна завісяць ад мокавання, і тэстамі інтеграцыі, якія запускаюцца медленней, але выкананыя над рэальнымі базамі данных і сецесіямі паветра.

                      ┌──────────────────────────┐
                      │    End-to-End (E2E)      │  ~10% of tests
                      │  (Full stack / Cypress)  │  (Slowest, high confidence)
                      └────────────┬─────────────┘
                                   │
                      ┌────────────┴─────────────┐
                      │    Integration Tests     │  ~40% of tests
                      │ (Real DB / HTTP Endpoints│  (Medium speed, real wiring)
                      └────────────┬─────────────┘
                                   │
                      ┌────────────┴─────────────┐
                      │       Unit Tests         │  ~50% of tests
                      │ (Isolated Domain Logic)  │  (Fastest, instant feedback)
                      └──────────────────────────┘

Тэсты на елементах: чыстая логіка домэны

Тэсты на елементах прагледзяцца выканаваць бізнес-логіку, якая знаходзіцца ў сервісах домэны, у абсалютнай ізоляцыі. Усё, што выходзіць за межы, такое як рэпазітарыя баз данных або зовнішні кліенты API, павінны быць замененыя на стабы або мокі.

  • Што яны пакрываюць: сервісы домэны, логіку вычыслення і спяльныя дапаможнікі.
  • Як быстро яны запускаюцца: зазвычай менш чым за мілісекунду на тэст.
  • Што забараняецца: не падчыпляцься да сеті, няма доступу да жывой базы дадзеных, няма адказу да системы файлаў.
  • Тэсты інтеграцыі: рэальная схема з’яўлення і інфраструктура

    Тэсты інтеграцыі паўтараюць, што ваш код правільна супрацоўвае з рэальнымі зовнішнімі системамі (PostgreSQL, Redis, RabbitMQ) і з фрэймворкам веба, які вы вядзеце (Express або Fastify).

    • Што яны пераключаюць: запыты да рэпазітарыяў, канцэнты API (через supertest) і прыемнікі зямлі.
    • Як шыбка яны выконваюцца: працыягае ад дзесяткаў да сотняў мілісекунд кожны.
    • На чым яны спакоююцца: на базах дадзеных у контэйнерах, створаных за дапамойкай такіх інструментаў, як Testcontainers або Docker Compose, такім чынам пераканальваючыся ў рэальным выконанні SQL або NoSQL.

    3. 5 основных рэзультатаў служб домэна

    Калі вы пісваеце тэсты на адзінкавасьць чы раўнасць для методу бізнес-сэрвіса, гэта функцыя домэна можа стварыць аж пяць разных типоў рэзультатаў. Прыступны набор тэстаў павінен аблучыць кожны рэзультат, які стосуецца тэставанай операцыі:

    ┌────────────────────────────────────────────────────────────────────────┐
    │                        Domain Service Operation                        │
    └──────┬──────────────┬──────────────┬──────────────────┬────────────────┘
           │              │              │                  │
           ▼              ▼              ▼                  ▼
    ┌─────────────┐┌─────────────┐┌─────────────┐┌─────────────────────┐┌──────────────┐
    │  1. Return  ││  2. State   ││ 3. Outgoing ││ 4. Events Published ││ 5. Telemetry │
    │    Value    ││   Changes   ││  API Calls  ││  (Broker / Queue)   ││   / Logs     │
    └─────────────┘└─────────────┘└─────────────┘└─────────────────────┘└──────────────┘
    

    Прыклад: Тэставанне ўсіх 5 рэзультатаў

    Разглядзіце, як бы вы тэставалі цэлую операцыю домэна, такую як orderService.checkoutOrder.

    // components/orders/orders.service.test.js
    
    describe('OrderService.checkoutOrder', () => {
      it('given a valid order, when checkout occurs, then fulfills all 5 outcomes', async () => {
        // -------------------------------------------------------------
        // ARRANGE: Setup Mocks & Dependencies
        // -------------------------------------------------------------
        const mockOrderRepo = {
          findById: jest.fn().mockResolvedValue({ id: 'ord_123', status: 'PENDING', total: 100 }),
          updateStatus: jest.fn().mockResolvedValue({ id: 'ord_123', status: 'PAID', total: 100 })
        };
    
        const mockPaymentGateway = {
          charge: jest.fn().mockResolvedValue({ transactionId: 'txn_999', success: true })
        };
    
        const mockEventBus = {
          publish: jest.fn().mockResolvedValue(true)
        };
    
        const mockLogger = {
          info: jest.fn()
        };
    
        const orderService = createOrderService({
          orderRepo: mockOrderRepo,
          paymentGateway: mockPaymentGateway,
          eventBus: mockEventBus,
          logger: mockLogger
        });
    
        // -------------------------------------------------------------
        // ACT: Execute Domain Action
        // -------------------------------------------------------------
        const result = await orderService.checkoutOrder({ orderId: 'ord_123', paymentToken: 'tok_visa' });
    
        // -------------------------------------------------------------
        // ASSERT: Verify All 5 Outcomes
        // -------------------------------------------------------------
    
        // Outcome 1: Verify Return Value
        expect(result).toEqual(expect.objectContaining({
          id: 'ord_123',
          status: 'PAID'
        }));
    
        // Outcome 2: Verify Database State Change
        expect(mockOrderRepo.updateStatus).toHaveBeenCalledWith('ord_123', 'PAID');
    
        // Outcome 3: Verify Outgoing Third-Party Call
        expect(mockPaymentGateway.charge).toHaveBeenCalledWith({
          amount: 100,
          token: 'tok_visa'
        });
    
        // Outcome 4: Verify Message Queue / Event Emission
        expect(mockEventBus.publish).toHaveBeenCalledWith(
          'order.completed',
          expect.objectContaining({ orderId: 'ord_123' })
        );
    
        // Outcome 5: Verify Telemetry / Observability
        expect(mockLogger.info).toHaveBeenCalledWith(
          expect.stringContaining('Order ord_123 successfully checked out')
        );
      });
    });
    

    4. Запобежанне взаізалежнасці тэстаў за дапамогою фабрык тэстаў

    Аднай з галоўных прычынаў ненадзеянных набораў тэстаў є спяльны зменны стан — тэсты, якія залежаць ад глобальных фіксатураў, спяльных рэядоў у базе дадзенаў чы залишкаў з пярэднега запуску тэста.

    Проблема зі спяльнымі фіксатурамі

    // ❌ BAD: Hardcoded shared static data across test files
    const testUser = { id: '123', email: 'john@example.com' };
    
    // If Test A mutates testUser.email, Test B fails unpredictably!
    

    Рашэння: Дынамічныя фабрыкі тэстаў

    Рашэнь — павяроцца на фабрыкі тэстаў, якія з кожнага вызыву ствараюць свежыя, унікальныя даны, замест таго каб пераўтараць статычныя об’екты ў розных файлах.

    // test/factories/user.factory.js
    const { crypto } = require('crypto');
    
    class UserFactory {
      static build(overrides = {}) {
        const randomId = Math.random().toString(36).substring(7);
    
        return {
          id: `usr_${randomId}`,
          email: `user_${randomId}@example.com`,
          role: 'CUSTOMER',
          createdAt: new Date(),
          ...overrides // Allow callers to override specific properties
        };
      }
    
      static async create(dbClient, overrides = {}) {
        const user = this.build(overrides);
        await dbClient.query(
          'INSERT INTO users (id, email, role, created_at) VALUES ($1, $2, $3, $4)',
          [user.id, user.email, user.role, user.createdAt]
        );
        return user;
      }
    }
    
    module.exports = UserFactory;
    

    Іспытанне ў інтэграцыйных тэстах

    За дапамогою такога падходу кожны тэст атрымлея свой самастоятельны набор дадзеных, што значыць, што засобы для запуску тэстаў, такія як Jest, Vitest або вбудованы Node Test Runner, можу запускаць тэсты паралельна без выклікання супернагонкі:

    it('given an admin user, when fetching reports, then returns data', async () => {
      // Generates fresh, isolated database record with admin role override
      const adminUser = await UserFactory.create(dbClient, { role: 'ADMIN' });
    
      const response = await request(app)
        .get('/api/v1/reports')
        .set('x-user-id', adminUser.id);
    
      expect(response.status).toBe(200);
    });
    

    Чакліст архітэктуры для часткі 3

    Перш чым смярдзеце, што настройка тэстаў завершана, перагляньце свой код на адпаведнасць следуючаму чаклісту:

    • Структура AAA: Чы кожны тэст чытна раздзелены на секціі Arrange, Act і Assert?
    • Опісавае называнне: Чы заглавы тэстаў описваюць контекст, дзеянне і адначаковы рэзультат, следуючы формату given... when... then...?
  • Збалансаваная піраміда: Чы рэгулярнае выкарыстоўванне шыракіх тэстаў для логіки домэна, а інтеграцыйныя тэсты — толькі для базы дадзеных і граніц HTTP?
  • Працавае адзін усіх пяць параметраў: Чы вы пераканальваліся ў значэннях, якія вяртаюцца, змянах у базе дадзеных, выкліканні служб трэціх сторон, генераваных здарэннях і прыеме логаў, калі гэта неабходна?
  • Няма спакушанага стану: Чы фабрыкі ствараюць аўтанонімныя записы для кожнага тэсту, а не дазволяюць тэстам спакушацца за дапамогою глобальных налашчэнняў?
  • Спадневаная літэратура