npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2025 – Pkg Stats / Ryan Hefner

@vr1/be-driven-syn

v1.0.3

Published

syntax sugar to write pw and jest tests in bdd style

Readme

@vr1/be-driven-syn

Синтаксический сахар, для того чтобы тесты, такие как pw или jest, писать в виде bdd тестов.

Мотивация

Цель появления этой библиотеки - получить преимущества подхода к тестированию через given-when-then, известного как Behavior Testing или Behavior Driven Development, и при этом не получить недостатков "главного" фреймворка этот подход воплощающего - cucumber.

Подход BDD предполагает следующие преимущества:

  1. Описание тестов бизнес-языком (как противопоставление техническому языку)
  2. Разбитие тестов на отчётливые шаги/степы даёт переиспользование и пермутабельность. Пермутабельность это возможность составить новые тесты, например edge-кейсы, с использованием уже существующих степов без написания новых или с минимальным их написанием.
  3. Сам подход разбития на степы даёт возможность добавления вариативности в тесты. Это преимущество не воплощено в cucumber/gherkin, но теоретически всегда было возможно. И воплощено здесь.

Недостатки фреймворка cucumber, в частности языка gherkin

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

  1. Для матчинга используются регулярные выражения, которые добавляют некоторую когнитивную сложность сами по себе.
  2. Неочевидные ошибки, когда две регулярки могут поматчиться на мнемонику или наоборот ни одна, степ написанный для одного сценария может сломать тест в другой части приложения.
  3. Все степы становятся "глобальными", что накладывает на них дополнительные требования, их нужно писать "более универсальными", чтобы они работали в максимально возможном количестве комбинаций.
  4. Глобальность также добавляет проблему пересечения контекстов, где степы, которые имеют похожее "звучание", и можно по ошибке вставить мнемонику принадлежащую "чужому контексту", и также получить нерабочий тест.
  5. Поскольку язык текстовый, то сильно ограничена возможность передачи параметров, они могут представлять из себя только строку или число. Есть также механизм передачи таблиц (состоящих также из строк и чисел), но он просто ужасен, пользоваться им неудобно.

От этих недостатков свободен данный подход.

  1. Степы это обычные TS функции и для их использования нужно явно заимпортировать нужный степ и вставить в функцию given, when или then.
  2. Уровень "глобальности" может определить сам разработчик, можно сделать как глобальные шаги, так и локальные, которые можно использовать только в контексте тестов определённой фичи или группы фич
  3. Параметром могут быть не только строки и числа, но и enum, массивы, POJO объекты, и вообще любые типы данных доступные в TS. Тут хочется порекомендовать разработчику не злоупотреблять этой возможностью, и не использовать в качестве параметров степов такие сущности, как например стримы или сервисы, т.е. не скатываться на обратную сторону этой монеты.

использование

Перед использованием, нужно подготовить обёртки, которые конвертируют сценарий в тест jest или другого провайдера

import { TestWrapper, AssertWrapper } from "@vr1/be-driven-syn";

const testWrapper: TestWrapper = (name: string, testFunc: (data: object) => Promise<void>) => {
  test(name, async () => {
    await testFunc({}); // начальное состояние сквозного контекста
  });
};

const assertWrapper: AssertWrapper = {
  expectThrow: async (code: () => void | Promise<void>, customText?: string): Promise<void> => {
    // alternatively can use expect.toThrow
    let error: Error | null = null;
    try {
      await code();
    } catch (e) {
      error = e as Error;
    }
    if (error == null) {
      throw new Error(customText);
    }
  },
  expectIs: function <T>(val1: T, val2: T): void {
    expect(val1).toBe(val2);
  },
  expectIsNot: function <T>(val1: T, val2: T): void {
    expect(val1).not.toBe(val2);
  },
  expectTrue: function (val: boolean): void {
    expect(val).toBe(true);
  },
  expectFalse: function (val: boolean): void {
    expect(val).toBe(false);
  },
  expectEqual: function <T>(val1: T, val2: T): void {
    expect(val1).toEqual(val2);
  }
};

export const scenario = (name: string) => scenario(name, { testWrapper, assertWrapper });

Интеграция с allure

дополнительно к testWrapper нужно создать stepWrapper

import { StepWrapper } from "@vr1/be-driven-syn";
export const stepWrapper: StepWrapper = async (type: StepType, name: string, func: () => Promise<void>): Promise<void> => {
  // create allure tag from step name
  await func();
};

пример теста

scenario("test name")
  .given(givenStep, 'argValue')
  .when(whenStep)
  .and(anotherWhenStep)
  .then(thenStep);

пример степов

class _ {
  @given("here you can write text with interpolated {0} arg ")
  public static givenStep(arg: string): void {
    // step content
  }
  @given() // step name will be 'Given I have a document ("table", "xlsx")'
  public static async iHaveADocument(name: string, ext: string): Promise<void> { 
    // code that opens document for a test
  }

  @when()
  //when step
}

export const { givenStep, iHaveADocument } = _;

Пример ассертов

import { Test } from "@vr1/be-driven-interface";
class _ {
  @then()
  public static async documentAccessShouldBe(this: Test, expected: AccessType): Promise<void> {
    const access = await getCurrentDocumentAccessType();
    this.expect(access).is(expected); // проверяется строгое равенство
  }
  @then() 
  public static async userShouldBe(this: Test, expected: User): Promise<void> { 
    const user = await getCurrentUser();
    this.expect(user).equal(expected); // производится "глубокое" сравнение
  }
}

Вариации тестов

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

scenario('make text bold')
  .given(iHaveADocument, 'table', 'xlsx')
  .vary(
    vary => vary.when(iMakeTextBoldViaContextMenu),
    vary => vary.when(iMakeTextBoldViaToolbar)
  )
  .then(textShouldBeBold);

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

Также в вариациях можно дополнять имя теста

scenario('make text bold')
  .given(iHaveADocument, 'table', 'xlsx')
  .vary(
    ['via context', vary => vary.when(iMakeTextBoldViaContextMenu)],
    ['via toolbar', vary => vary.when(iMakeTextBoldViaToolbar)]
  )
  .then(textShouldBeBold);

такой сценарий сгенерирует два теста 'make text bold via context' и 'make text bold via toolbar' соответственно.

Передача данных между степами.

Иногда возникает необходимость передать какие либо данные, в основном id сущностей от одного степа к другому. Запись этих промежуточных данных в глобальные переменные или поля класса - ошибка. В cucumber для этого используется т.н. World Здесь для этих целей используется сквозной контекст

пример использования

import { Test } from "@vr1/be-driven-interface";

class _ {
  @given()
  public static iHaveAUser(name: string): { userId: number } { // id созданного пользователя подмешается в контекст
    const id = await createUser(name);
    return { userId: id };
  }
  @given() 
  public static async userHasAccessToDocument(this: Test<{ userId: number }>, file: string): Promise<void> { 
    // в this будут все ранее добавленные поля, нужно указать лишь те, которые нужны для этого степа
    await addDocumentAccess(file, this.userId); 
  }
}