Инструменты торговли

Автоматизированные тесты — это, по сути, просто код, который выдает ошибку или вызывает ошибку, если что-то не так. Большинство библиотек или платформ тестирования предоставляют различные примитивы, упрощающие написание тестов.

Как упоминалось в предыдущем разделе, эти примитивы почти всегда включают способ определения независимых тестов (называемых тестовыми примерами ) и предоставления утверждений. Утверждения — это способ объединить проверку результата и выдачу ошибки, если что-то не так, и их можно считать основным примитивом всех примитивов тестирования.

На этой странице описан общий подход к этим примитивам. Выбранная вами структура, вероятно, имеет что-то подобное, но это не точная ссылка.

Например:

import { fibonacci, catalan } from '../src/math.js';
import { assert, test, suite } from 'a-made-up-testing-library';

suite('math tests', () => {
  test('fibonacci function', () => {
    // check expected fibonacci numbers against our known actual values
    // with an explanation if the values don't match
    assert.equal(fibonacci(0), 0, 'Invalid 0th fibonacci result');
    assert.equal(fibonacci(13), 233, 'Invalid 13th fibonacci result');
  });
  test('relationship between sequences', () => {
    // catalan numbers are greater than fibonacci numbers (but not equal)
    assert.isAbove(catalan(4), fibonacci(4));
  });
  test('bugfix: check bug #4141', () => {
    assert.isFinite(fibonacci(0)); // fibonacci(0) was returning NaN
  })
});

В этом примере создается группа тестов (иногда называемая набором ), называемая «математическими тестами», и определяются три независимых тестовых примера, каждый из которых выполняет несколько утверждений. Эти тестовые случаи обычно можно адресовать или запускать индивидуально, например, с помощью флага фильтра в вашем средстве выполнения тестов.

Помощники утверждений как примитивы

Большинство платформ тестирования, включая Vitest, включают в себя набор помощников по проверке утверждений для объекта assert , которые позволяют быстро проверять возвращаемые значения или другие состояния на соответствие некоторым ожиданиям . Это ожидание часто представляет собой «заведомо хорошие» ценности. В предыдущем примере мы знаем, что 13-е число Фибоначчи должно быть 233, поэтому мы можем подтвердить это напрямую, используя assert.equal .

Вы также можете ожидать, что значение примет определенную форму, будет больше другого значения или будет иметь какое-то другое свойство. В этом курсе не будет рассмотрен весь спектр возможных помощников по проверке утверждений , но среды тестирования всегда предоставляют как минимум следующие базовые проверки:

  • Проверка «правдивости» , часто описываемая как проверка «ОК», проверяет, истинно ли условие, аналогично тому, как вы могли бы написать if , который проверяет, является ли что-то успешным или правильным. Обычно это предоставляется как assert(...) или assert.ok(...) и принимает одно значение плюс необязательный комментарий.

  • Проверка на равенство, например, в примере математического теста, в котором вы ожидаете, что возвращаемое значение или состояние объекта будет равно известному хорошему значению. Они предназначены для примитивного равенства (например, для чисел и строк) или ссылочного равенства (это один и тот же объект). По сути, это просто проверка «правдивости» со сравнением == или === .

    • JavaScript различает свободное ( == ) и строгое ( === ) равенство. Большинство тестовых библиотек предоставляют методы assert.equal и assert.strictEqual соответственно.
  • Глубокие проверки на равенство, которые расширяют проверки на равенство, включая проверку содержимого объектов, массивов и других более сложных типов данных, а также внутреннюю логику для перемещения объектов для их сравнения. Это важно, поскольку в JavaScript нет встроенного способа сравнения содержимого двух объектов или массивов. Например, [1,2,3] == [1,2,3] всегда ложно. Платформы тестирования часто включают помощники deepEqual или deepStrictEqual .

Помощники утверждений, которые сравнивают два значения (а не только проверку «правдивости»), обычно принимают два или три аргумента:

  • Фактическое значение, сгенерированное тестируемым кодом или описывающее состояние, требующее проверки.
  • Ожидаемое значение, обычно жестко запрограммированное (например, буквальное число или строка).
  • Необязательный комментарий, описывающий, что ожидается или что могло произойти сбой, который будет включен в случае сбоя в этой строке.

Также довольно распространенной практикой является объединение утверждений для создания различных проверок, поскольку редко можно правильно подтвердить состояние вашей системы само по себе. Например:

  test('JWT parse', () => {
    const json = decodeJwt('eyJieSI6InNhbXRob3Ii…');

    assert.ok(json.payload.admin, 'user should be admin');
    assert.deepEqual(json.payload.groups, ['role:Admin', 'role:Submitter']);
    assert.equal(json.header.alg, 'RS265')
    assert.isAbove(json.payload.exp, +new Date(), 'expiry must be in future')
  });

Vitest использует библиотеку утверждений Chai внутри себя для предоставления своих помощников утверждений, и может быть полезно просмотреть ее ссылку, чтобы увидеть, какие утверждения и помощники могут подойти вашему коду.

Утверждения Fluent и BDD

Некоторые разработчики предпочитают стиль утверждений, который можно назвать разработкой на основе поведения (BDD) или утверждениями в стиле Fluent . Их также называют помощниками «ожидания», поскольку отправной точкой для проверки ожиданий является метод с именем « expect() .

Ожидайте, что помощники будут вести себя так же, как утверждения, написанные как простые вызовы методов, такие как assert.ok или assert.strictDeepEquals , но некоторые разработчики считают, что их легче читать. Утверждение BDD может выглядеть следующим образом:

// A failure here would generate "Expect result to be an array that does include 42"
const result = await possibleMeaningsOfLife();
expect(result).to.be.an('array').that.does.include(42);

// or a simpler form
expect(result).toBe('array').toContainEqual(42);

// the same in assert might be
assert.typeOf(result, 'array', 'Expected the result to be an array');
assert.include(result, 42, 'Expected the result to include 42');

Этот стиль утверждений работает благодаря технологии, называемой цепочкой методов, при которой объект, возвращаемый expect , может постоянно связываться вместе с дальнейшими вызовами методов. Некоторые части вызова, включая to.be и that.does в предыдущем примере, не имеют функции и включены только для того, чтобы облегчить чтение вызова и, возможно, для создания автоматического комментария в случае неудачного теста. (Примечательно, что expect обычно не поддерживает необязательный комментарий, поскольку цепочка должна четко описывать сбой.)

Многие среды тестирования поддерживают как Fluent/BDD, так и обычные утверждения. Vitest, например , экспортирует оба подхода Chai и имеет свой, чуть более лаконичный подход к BDD. Jest, с другой стороны, по умолчанию включает только метод ожидания .

Групповые тесты по файлам

При написании тестов мы уже склонны обеспечивать неявную группировку — вместо того, чтобы все тесты располагались в одном файле, обычно тесты пишутся в нескольких файлах. Фактически, специалисты по тестированию обычно знают, что файл предназначен для тестирования, только благодаря предопределенному фильтру или регулярному выражению — например, vitest включает в себя все файлы в вашем проекте, которые заканчиваются расширением типа «.test.jsx» или «.spec». .ts» («.test» и «.spec» плюс ряд допустимых расширений).

Тесты компонентов обычно располагаются в файле, равном тестируемому компоненту, как в следующей структуре каталогов:

Список файлов в каталоге, включая UserList.tsx и UserList.test.tsx.
Файл компонента и связанный тестовый файл.

Аналогично, модульные тесты обычно размещаются рядом с тестируемым кодом. Каждый из сквозных тестов может находиться в отдельном файле, а интеграционные тесты могут даже размещаться в отдельных папках. Эти структуры могут быть полезны, когда в сложных тестовых случаях требуются собственные файлы поддержки, не связанные с тестированием, например библиотеки поддержки, необходимые только для теста.

Групповые тесты внутри файлов

Как использовалось в предыдущих примерах, общепринятой практикой является размещение тестов внутри вызова suite() , который группирует тесты, настроенные вами с помощью test() . Пакеты обычно не являются тестами сами по себе, но они помогают обеспечить структуру, группируя связанные тесты или цели путем вызова переданного метода. Для test() переданный метод описывает действия самого теста.

Как и в случае с утверждениями, во Fluent/BDD существует довольно стандартная эквивалентность группировке тестов. Некоторые типичные примеры сравниваются в следующем коде:

// traditional/TDD
suite('math tests', () => {
  test('handle zero values', () => {
    assert.equal(fibonacci(0), 0);
  });
});

// Fluent/BDD
describe('math tests', () => {
  it('should handle zero values', () => {
    expect(fibonacci(0)).toBe(0);
  });
})

В большинстве фреймворков suite и describe ведут себя одинаково, как и test и it , в отличие от больших различий между использованием expect и assert для записи утверждений.

Другие инструменты имеют слегка отличающиеся подходы к организации наборов и тестов. Например, встроенный инструмент запуска тестов Node.js поддерживает вложенные вызовы test() для неявного создания иерархии тестов. Однако Vitest допускает такое вложение только с помощью suite() и не запускает test() , определенный внутри другого test() .

Как и в случае с утверждениями, помните, что точная комбинация методов группировки, предоставляемая вашим технологическим стеком , не так уж важна . В этом курсе они будут рассмотрены в абстрактном виде, но вам нужно будет выяснить, как они применимы к вашему выбору инструментов.

Методы жизненного цикла

Одной из причин группировать тесты, даже неявно на верхнем уровне файла, является предоставление методов настройки и удаления, которые выполняются для каждого теста или один раз для группы тестов. Большинство фреймворков предоставляют четыре метода:

Для каждого `test()` или `it()` Один раз в люкс
Перед тестовыми запусками `beforeEach()` `beforeAll()`
После тестовых запусков `afterEach()` `afterAll()`

Например, вы можете захотеть предварительно заполнить базу данных виртуальных пользователей перед каждым тестом, а затем очистить ее:

suite('user test', () => {
  beforeEach(() => {
    insertFakeUser('bob@example.com', 'hunter2');
  });
  afterEach(() => {
    clearAllUsers();
  });

  test('bob can login', async () => { … });
  test('alice can message bob', async () => { … });
});

Это может быть полезно для упрощения ваших тестов. Вы можете использовать общий код настройки и демонтажа, а не дублировать его в каждом тесте. Кроме того, если сам код установки и демонтажа выдает ошибку, это может указывать на структурные проблемы, не связанные с провалом самих тестов.

Общие советы

Вот несколько советов, которые следует помнить, думая об этих примитивах.

Примитивы — это ориентир

Помните, что инструменты и примитивы здесь и на следующих нескольких страницах не будут точно соответствовать Vitest, Jest, Mocha, Web Test Runner или любой другой конкретной платформе. Хотя мы использовали Vitest в качестве общего руководства, обязательно сопоставьте его с выбранной вами платформой.

При необходимости смешивайте и сопоставляйте утверждения.

По сути, тесты — это код, который может выдавать ошибки. Каждый бегун предоставит примитив, скорее всего, test() , для описания отдельных тестовых случаев.

Но если этот бегун также предоставляет assert() , expect() и помощники по утверждениям, помните, что эта часть больше ориентирована на удобство, и вы можете пропустить ее, если вам нужно. Вы можете запустить любой код, который может вызвать ошибку, включая другие библиотеки утверждений или старый добрый оператор if .

Настройка IDE может спасти жизнь

Обеспечение доступа вашей IDE, такой как VSCode, к автозаполнению и документации по выбранному вами инструменту тестирования может повысить вашу продуктивность. Например, в библиотеке утверждений Chai имеется более 100 методов assert , и наличие встроенной документации по нужному методу может оказаться удобным.

Это может быть особенно важно для некоторых платформ тестирования, которые заполняют глобальное пространство имен своими методами тестирования. Это небольшая разница, но часто можно использовать библиотеки тестирования без их импорта , если они автоматически добавляются в глобальное пространство имен:

// some.test.js
test('using test as a global', () => { … });

Мы рекомендуем импортировать помощники , даже если они поддерживаются автоматически, потому что это дает вашей IDE четкий способ поиска этих методов. (Возможно, вы столкнулись с этой проблемой при создании React, поскольку в некоторых базах кода есть магический глобальный React , а в некоторых его нет, и требуется, чтобы он был импортирован во все файлы, использующие React.)

// some.test.js
import { test } from 'vitest';
test('using test as an import', () => { … });