Scope и контекст в хуках

Jasmine — это фреймворк для модульного тестирования JavaScript, который строится вокруг понятий спеков (specs) и хуков (hooks). Понимание scope (области видимости) и контекста в хуках является ключевым для написания надежных и предсказуемых тестов.


Основы хуков

В Jasmine предусмотрены несколько видов хуков:

  • beforeAll(fn) — выполняется один раз перед всеми тестами в текущем describe.
  • beforeEach(fn) — выполняется перед каждым тестом в блоке.
  • afterEach(fn) — выполняется после каждого теста.
  • afterAll(fn) — выполняется один раз после всех тестов в блоке.

Эти хуки позволяют организовать подготовку окружения, очистку ресурсов и установку начальных условий для тестов.


Scope переменных в хуках

Локальные переменные в хуках ограничены блоком describe, в котором они объявлены. Рассмотрим пример:

describe('Пример scope', () => {
    let counter = 0;

    beforeEach(() => {
        counter += 1;
    });

    it('первый тест', () => {
        expect(counter).toBe(1);
    });

    it('второй тест', () => {
        expect(counter).toBe(2);
    });
});

Здесь переменная counter существует в области видимости блока describe. Каждый вызов beforeEach изменяет её значение, и это изменение видно во всех тестах внутри этого блока.

Если переменная объявляется внутри beforeEach, она будет локальной для этого вызова и недоступной в других тестах:

beforeEach(() => {
    let temp = 5; // доступна только в этом хуке
});

Контекст this в хуках

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

describe('Контекст this', function() {
    beforeEach(function() {
        this.value = 10;
    });

    it('использует this', function() {
        expect(this.value).toBe(10);
    });

    it('можно изменить this', function() {
        this.value += 5;
        expect(this.value).toBe(15);
    });
});

Особенности:

  • Контекст this уникален для каждого теста.
  • Использование стрелочных функций (() => {}) разрушает доступ к this, так как стрелочные функции наследуют контекст из внешнего блока.

Вложенные блоки describe

Вложенные describe создают иерархию scope и позволяют локализовать переменные и хуки:

describe('Внешний блок', () => {
    let outer = 'наружу';

    beforeEach(() => {
        outer += '!';
    });

    describe('Внутренний блок', () => {
        let inner = 'внутри';

        beforeEach(() => {
            inner += '?';
        });

        it('доступ к обеим переменным', () => {
            expect(outer).toBe('наружу!');
            expect(inner).toBe('внутри?');
        });
    });
});

Ключевые моменты:

  • beforeEach внешнего блока выполняется до beforeEach внутреннего блока.
  • Внутренний блок имеет доступ к переменным внешнего блока, но не наоборот.
  • Это позволяет структурировать сложные тестовые сценарии без конфликтов.

Комбинирование beforeAll и beforeEach

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

describe('beforeAll vs beforeEach', () => {
    let db;

    beforeAll(() => {
        db = initializeDatabase();
    });

    beforeEach(() => {
        db.reset();
    });

    it('первый тест', () => {
        expect(db.isEmpty()).toBeTrue();
    });

    it('второй тест', () => {
        expect(db.isEmpty()).toBeTrue();
    });
});

Здесь база данных инициализируется один раз, а каждый тест начинает с чистой базы, благодаря db.reset() в beforeEach.


Ограничения и подводные камни

  1. Использование стрелочных функций для хуков приведет к потере контекста this. Это важно учитывать, если планируется использовать this для хранения состояния между хуками и тестами.
  2. Переменные внутри хуков не должны иметь одинаковые имена с глобальными переменными — это создаёт путаницу из-за shadowing.
  3. Вложенные хуки выполняются в строгой иерархии: сначала внешние beforeEach, потом внутренние. Для afterEach порядок обратный: сначала внутренние, потом внешние.

Практические рекомендации

  • Для передачи состояния между хуками и тестами использовать либо переменные в describe, либо контекст this.
  • Минимизировать глобальные переменные — это повышает предсказуемость тестов.
  • Структурировать тесты с вложенными describe для логического разделения функционала.
  • Четко различать, когда нужен beforeAll (инициализация) и beforeEach (сброс состояния).

Понимание scope и контекста в Jasmine позволяет создавать тесты, которые легко поддерживать, расширять и комбинировать, сохраняя ясность и предсказуемость поведения даже в сложных сценариях.