@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 предполагает следующие преимущества:
- Описание тестов бизнес-языком (как противопоставление техническому языку)
- Разбитие тестов на отчётливые шаги/степы даёт переиспользование и пермутабельность. Пермутабельность это возможность составить новые тесты, например edge-кейсы, с использованием уже существующих степов без написания новых или с минимальным их написанием.
- Сам подход разбития на степы даёт возможность добавления вариативности в тесты. Это преимущество не воплощено в cucumber/gherkin, но теоретически всегда было возможно. И воплощено здесь.
Недостатки фреймворка cucumber, в частности языка gherkin
Для описания последовательности тестов используется текстовый язык, и для привязки степов в коде к мнемоникам в тесте используются регулярные выражения. Изначально задумывалось, что язык gherkin позволит писать тесты не-программистам, т.е. бизнес-аналитикам, продукт-овнерам, тестировщикам-мануальщикам. Однако, по факту, во всех проектах, где я работал, не-программисты всегда сопротивляются этому и активно отказываются в этом участвовать, а то и вовсе саботируют эту возможность. Отсюда получается, что gherkin не дал того для чего был задуман, но привнёс сложности, которые не нужны тем людям, кто реально пишет тесты - программистам и тестировщикам-автоматизаторам. Сложности заключаются в следующем:
- Для матчинга используются регулярные выражения, которые добавляют некоторую когнитивную сложность сами по себе.
- Неочевидные ошибки, когда две регулярки могут поматчиться на мнемонику или наоборот ни одна, степ написанный для одного сценария может сломать тест в другой части приложения.
- Все степы становятся "глобальными", что накладывает на них дополнительные требования, их нужно писать "более универсальными", чтобы они работали в максимально возможном количестве комбинаций.
- Глобальность также добавляет проблему пересечения контекстов, где степы, которые имеют похожее "звучание", и можно по ошибке вставить мнемонику принадлежащую "чужому контексту", и также получить нерабочий тест.
- Поскольку язык текстовый, то сильно ограничена возможность передачи параметров, они могут представлять из себя только строку или число. Есть также механизм передачи таблиц (состоящих также из строк и чисел), но он просто ужасен, пользоваться им неудобно.
От этих недостатков свободен данный подход.
- Степы это обычные TS функции и для их использования нужно явно заимпортировать нужный степ и вставить в функцию given, when или then.
- Уровень "глобальности" может определить сам разработчик, можно сделать как глобальные шаги, так и локальные, которые можно использовать только в контексте тестов определённой фичи или группы фич
- Параметром могут быть не только строки и числа, но и 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);
}
}