Размер и scope тестов

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

1. Размер тестов

Тесты в Jasmine можно классифицировать по размеру:

  • Unit-тесты — это тесты, которые проверяют отдельные функции, методы или классы. Их цель — удостовериться, что каждый отдельный компонент работает корректно в изоляции. Unit-тесты, как правило, быстрые и не должны зависеть от внешних сервисов, баз данных или других частей системы. Они фокусируются только на логике конкретной функции или компонента.

  • Интеграционные тесты — проверяют, как несколько компонентов взаимодействуют друг с другом. Такие тесты имеют больший размер, поскольку они охватывают более широкий контекст. В отличие от unit-тестов, интеграционные тесты могут включать взаимодействие с базой данных, API, файловыми системами или другими частями системы.

  • Системные тесты — тестируют всю систему или её значительные части. Обычно они охватывают рабочий процесс приложения от начала до конца. Например, системный тест может проверять, как пользователь взаимодействует с интерфейсом, а система корректно обрабатывает запросы.

  • E2E-тесты (End-to-End) — это тесты, которые имитируют действия пользователя в полном рабочем процессе. Они взаимодействуют с приложением на уровне пользовательского интерфейса и могут проверять всю логику приложения, включая взаимодействие с сервером.

Jasmine чаще всего используется для unit-тестов, но он поддерживает создание и других типов тестов.

2. Область видимости (scope) тестов

Scope теста в Jasmine — это его контекст выполнения. Это определяет, какие переменные и функции доступны внутри теста. Управление областью видимости позволяет изолировать тесты друг от друга и минимизировать зависимости.

  • Global scope — глобальная область видимости, которая может включать глобальные переменные, которые доступны во всех тестах. Но использование глобальных переменных в тестах нежелательно, так как это может привести к непредсказуемым результатам. Важно, чтобы тесты были изолированы друг от друга, чтобы результат одного теста не повлиял на другой.

  • Test suite scope — область видимости для всего набора тестов. В Jasmine тесты группируются в “test suites”, которые обрабатываются как единое целое. Такие тесты могут использовать общие подготовительные функции (например, beforeAll и afterAll), которые выполняются до и после всех тестов внутри набора.

  • Test case scope — каждый индивидуальный тест, определенный через it(), имеет собственную область видимости. В идеале, каждый тест должен быть независимым и не зависеть от состояния других тестов. Это значит, что данные, созданные в одном тесте, не должны быть использованы в других тестах, и наоборот.

  • Setup и teardown scope — Jasmine предлагает функции beforeEach и afterEach, которые позволяют подготовить среду для каждого теста и очистить её после выполнения. Это важно для того, чтобы каждый тест был выполнен в чистом состоянии и не наследовал состояние предыдущего теста.

3. Изоляция тестов

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

  • beforeEach/afterEach — с помощью этих хуков можно подготовить и очистить тестовую среду для каждого теста. Например, можно создать объект перед тестом, а после теста его удалить.

    describe('Пример с использованием beforeEach и afterEach', function() {
      let obj;
    
      beforeEach(function() {
        obj = new MyClass();
      });
    
      afterEach(function() {
        obj = null;
      });
    
      it('должен работать корректно', function() {
        expect(obj.method()).toBe(true);
      });
    });
  • beforeAll/afterAll — для случаев, когда настройка или очистка необходима только один раз для всего набора тестов. Эти хуки выполняются один раз перед и после всех тестов в рамках описанного набора. Они полезны, когда подготовка или очистка занимают много времени и не зависят от конкретных тестов.

    describe('Пример с использованием beforeAll и afterAll', function() {
      let db;
    
      beforeAll(function() {
        db = connectToDatabase();
      });
    
      afterAll(function() {
        db.disconnect();
      });
    
      it('должен извлекать данные', function() {
        expect(db.fetchData()).toBeDefined();
      });
    });

4. Должны ли тесты быть атомарными?

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

Пример плохого теста:

it('должен добавить и удалить пользователя', function() {
  let user = addUser('John');
  expect(user).toBeDefined();
  removeUser(user);
  expect(isUserRemoved(user)).toBe(true);
});

Пример хорошего теста:

it('должен добавить пользователя', function() {
  let user = addUser('John');
  expect(user).toBeDefined();
});

it('должен удалить пользователя', function() {
  let user = addUser('John');
  removeUser(user);
  expect(isUserRemoved(user)).toBe(true);
});

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

5. Мок-объекты и шпионов в Jasmine

Когда речь идет об изоляции тестов, в Jasmine можно использовать мок-объекты и шпионов для имитации внешних зависимостей. Моки позволяют тестировать компоненты без реальных зависимостей, а шпионы дают возможность следить за тем, как были вызваны функции или методы.

  • Шпионы (spies) позволяют отслеживать вызовы функций, их аргументы и возвращаемые значения. Использование шпионов помогает убедиться, что внешние методы были вызваны правильным образом.

    it('должен вызвать метод отправки данных', function() {
      let spy = spyOn(api, 'sendData');
      api.sendData('test');
      expect(spy).toHaveBeenCalled();
    });
  • Моки (mocks) используются для имитации объектов или сервисов, с которыми компонент взаимодействует. Например, можно создать мок-объект базы данных, чтобы не обращаться к настоящей базе во время теста.

6. Выбор размера теста

Правильный выбор размера теста зависит от контекста.

  • Unit-тесты должны быть быстро исполнимыми, независимыми и хорошо покрывать логику функции.
  • Интеграционные тесты полезны, когда нужно проверить взаимодействие между компонентами, но они могут быть более сложными и медленными.
  • Системные и E2E-тесты имеют самую большую область видимости и часто занимают значительное время на выполнение, но они необходимы для проверки работы всей системы в целом.

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