Callback-based асинхронность

Jasmine — это фреймворк для тестирования JavaScript, который поддерживает как синхронные, так и асинхронные сценарии. Асинхронное тестирование особенно важно при работе с сетевыми запросами, таймерами и другими операциями, выполняющимися вне основного потока выполнения. Callback-based подход — один из базовых способов обработки асинхронности.

Использование функции done

В Jasmine для обработки асинхронных операций применяется функция done. Она передаётся как аргумент в тело теста и вызывается по завершении асинхронной операции. Основные правила её использования:

it('асинхронный тест с callback', function(done) {
    setTimeout(function() {
        expect(true).toBe(true);
        done();
    }, 100);
});

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

  • done должна быть вызвана единожды в каждом тесте.
  • Если done не вызывается, тест завершится с ошибкой таймаута.
  • Можно использовать done.fail() для явного указания на провал теста при возникновении ошибки.
it('тест с обработкой ошибки', function(done) {
    asyncOperation(function(err, result) {
        if (err) {
            done.fail(err);
            return;
        }
        expect(result).toBe(42);
        done();
    });
});

Таймауты и управление временем

По умолчанию Jasmine ждёт завершения асинхронного теста 5 секунд. Этот интервал можно изменить с помощью jasmine.DEFAULT_TIMEOUT_INTERVAL:

describe('асинхронные операции', function() {
    beforeAll(function() {
        jasmine.DEFAULT_TIMEOUT_INTERVAL = 10000; // 10 секунд
    });

    it('длинная операция', function(done) {
        setTimeout(function() {
            expect(true).toBe(true);
            done();
        }, 8000);
    });
});

Важно учитывать, что чрезмерное увеличение таймаута может замедлить тестирование всего проекта.

Асинхронные beforeEach и afterEach

Callback-based подход применяется не только к отдельным тестам, но и к хукам Jasmine, таким как beforeEach и afterEach:

beforeEach(function(done) {
    initializeDatabase(function() {
        done();
    });
});

afterEach(function(done) {
    cleanupDatabase(function() {
        done();
    });
});

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

Вложенные асинхронные вызовы

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

it('несколько последовательных асинхронных вызовов', function(done) {
    firstAsync(function(result1) {
        expect(result1).toBe(true);
        secondAsync(function(result2) {
            expect(result2).toBe(42);
            done();
        });
    });
});

При необходимости параллельного выполнения нескольких операций можно использовать счётчик завершения:

it('параллельные операции', function(done) {
    let completed = 0;
    function checkDone() {
        completed += 1;
        if (completed === 2) done();
    }

    asyncOp1(checkDone);
    asyncOp2(checkDone);
});

Обработка ошибок в асинхронных тестах

Ошибки в асинхронных вызовах нельзя поймать обычным try-catch. Вместо этого используется done.fail():

it('обработка ошибок', function(done) {
    asyncOperation(function(err, result) {
        if (err) {
            done.fail('Произошла ошибка: ' + err.message);
            return;
        }
        expect(result).toBeTruthy();
        done();
    });
});

Этот подход обеспечивает корректное отображение ошибок в отчётах Jasmine и предотвращает зависание теста.

Преимущества callback-based подхода

  • Простота реализации для небольших асинхронных операций.
  • Не требует дополнительных библиотек или синтаксиса.
  • Совместим с существующим кодом, построенным на Node.js-style callbacks.

Ограничения и риски

  • При большом числе вложенных callback-ов код становится трудночитаемым («callback hell»).
  • Сложнее управлять параллельными асинхронными операциями.
  • Трудно комбинировать с современными Promise и async/await без адаптации.

Callback-based подход остаётся актуальным для понимания основ асинхронного тестирования и совместимости с legacy-кодом, однако для более сложных сценариев рекомендуется изучать промисы и async/await, которые делают код более линейным и читаемым.