Статьи

Как проверить свой код JavaScript с помощью QUnit

QUnit , разработанный командой jQuery, является отличной средой для модульного тестирования вашего JavaScript. В этом уроке я расскажу, что конкретно представляет собой QUnit, и почему вам следует тщательно проверять свой код.

QUnit — это мощная среда модульного тестирования JavaScript, которая помогает вам отлаживать код. Он написан членами команды jQuery и является официальным набором тестов для jQuery. Но QUnit достаточно универсален для тестирования любого обычного кода JavaScript, и он даже способен тестировать серверный JavaScript с помощью некоторого механизма JavaScript, такого как Rhino или V8.

Если вы не знакомы с идеей «модульного тестирования», не беспокойтесь. Это не так сложно понять

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

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

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

Во-первых, это очень утомительно. Нажатие на самом деле не легкая работа, потому что вы должны убедиться, что все нажали, и очень вероятно, что вы пропустите одну или две вещи. Во-вторых, все, что вы сделали для тестирования, нельзя использовать повторно, а это значит, что найти регрессию непросто. Что такое регрессия? Представьте, что вы написали некоторый код и протестировали его, исправили все найденные ошибки и опубликовали его. Затем пользователь отправляет отзыв о новых ошибках и запрашивает новые функции. Вы возвращаетесь к коду, исправляете эти новые ошибки и добавляете эти новые функции. Что может произойти дальше, это то, что некоторые старые ошибки снова появляются, которые называются «регрессиями». Видите, теперь вы должны снова щелкнуть мышью, и скорее всего, вы больше не найдете эти старые ошибки; даже если вы это сделаете, пройдет некоторое время, прежде чем вы поймете, что проблема вызвана регрессией. В модульном тестировании вы пишете тесты для поиска ошибок, а после изменения кода вы снова фильтруете его через тесты. Если возникает регрессия, некоторые тесты обязательно будут провалены, и вы легко сможете их определить, зная, какая часть кода содержит ошибку. Поскольку вы знаете, что вы только что изменили, это легко исправить.

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

Я хотел бы упомянуть один из проектов Джона Резига : TestSwarm . Он выводит модульное тестирование JavaScript на новый уровень, распространяя его. Это веб-сайт, который содержит множество тестов, каждый может зайти туда, запустить некоторые тесты и вернуть результат обратно на сервер. Таким образом, код можно очень быстро протестировать в разных браузерах и даже на разных платформах.

Итак, как вы пишете модульные тесты с помощью QUnit? Во-первых, вам нужно настроить среду тестирования:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
<!DOCTYPE html>
<html>
<head>
    <title>QUnit Test Suite</title>
    <link rel=»stylesheet» href=»http://github.com/jquery/qunit/raw/master/qunit/qunit.css» type=»text/css» media=»screen»>
    <script type=»text/javascript» src=»http://github.com/jquery/qunit/raw/master/qunit/qunit.js»></script>
    <!— Your project file goes here —>
    <script type=»text/javascript» src=»myProject.js»></script>
    <!— Your tests file goes here —>
    <script type=»text/javascript» src=»myTests.js»></script>
</head>
<body>
    <h1 id=»qunit-header»>QUnit Test Suite</h1>
    <h2 id=»qunit-banner»></h2>
    <div id=»qunit-testrunner-toolbar»></div>
    <h2 id=»qunit-userAgent»></h2>
    <ol id=»qunit-tests»></ol>
</body>
</html>

Как видите, здесь используется размещенная версия фреймворка QUnit .

Код, который будет тестироваться, должен быть помещен в myProject.js, а ваши тесты — в myTests.js. Чтобы запустить эти тесты, просто откройте этот HTML-файл в браузере. Теперь пришло время написать несколько тестов.

Строительные блоки модульных тестов являются утверждениями.

Утверждение — это утверждение, которое предсказывает возвращаемый результат вашего кода. Если прогноз неверен, утверждение не выполнено, и вы знаете, что что-то пошло не так.

Чтобы выполнить утверждения, вы должны поместить их в контрольный пример:

01
02
03
04
05
06
07
08
09
10
11
12
// Let’s test this function
function isEven(val) {
    return val % 2 === 0;
}
 
test(‘isEven()’, function() {
    ok(isEven(0), ‘Zero is an even number’);
    ok(isEven(2), ‘So is two’);
    ok(isEven(-4), ‘So is negative four’);
    ok(!isEven(1), ‘One is not an even number’);
    ok(!isEven(-7), ‘Neither is negative seven’);
})

Здесь мы определили функцию isEven, которая определяет, является ли число четным, и мы хотим протестировать эту функцию, чтобы убедиться, что она не возвращает неправильных ответов.

Сначала мы вызываем test (), который создает тестовый пример; первый параметр — это строка, которая будет отображаться в результате, а второй параметр — это функция обратного вызова, которая содержит наши утверждения. Эта функция обратного вызова будет вызвана после запуска QUnit.

Мы написали пять утверждений, все из которых являются логическими. Булево утверждение ожидает, что его первый параметр будет истинным. Второй параметр также является сообщением, которое будет отображаться в результате.

Вот что вы получите после запуска теста:

тест для isEven ()

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

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

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
// Let’s test this function
function isEven(val) {
    return val % 2 === 0;
}
 
test(‘isEven()’, function() {
    ok(isEven(0), ‘Zero is an even number’);
    ok(isEven(2), ‘So is two’);
    ok(isEven(-4), ‘So is negative four’);
    ok(!isEven(1), ‘One is not an even number’);
    ok(!isEven(-7), ‘Neither does negative seven’);
 
    // Fails
    ok(isEven(3), ‘Three is an even number’);
})

Вот результат:

тест содержит ошибочное утверждение для isEven ()

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

ok () — не единственное утверждение, которое предоставляет QUnit. Существуют и другие виды утверждений, которые полезны при тестировании вашего проекта:

Утверждение сравнения equals () ожидает, что его первый параметр (который является фактическим значением) равен его второму параметру (который является ожидаемым значением). Это похоже на ok (), но выводит как фактические, так и ожидаемые значения, что значительно упрощает отладку. Как и ok (), он принимает необязательный третий параметр в качестве сообщения для отображения.

Так что вместо:

1
2
3
test(‘assertions’, function() {
    ok( 1 == 1, ‘one equals one’);
})
булево утверждение

Вы должны написать:

1
2
3
test(‘assertions’, function() {
    equals( 1, 1, ‘one equals one’);
})
утверждение сравнения

Обратите внимание на последний «1», который является значением сравнения.

И если значения не равны:

1
2
3
test(‘assertions’, function() {
    equals( 2, 1, ‘one equals one’);
})
несостоятельное утверждение сравнения

Это дает намного больше информации, делая жизнь намного проще.

Утверждение сравнения использует «==» для сравнения своих параметров, поэтому оно не обрабатывает сравнение массивов или объектов:

1
2
3
4
5
6
test(‘test’, function() {
    equals( {}, {}, ‘fails, these are different objects’);
    equals( {a: 1}, {a: 1} , ‘fails’);
    equals( [], [], ‘fails, there are different arrays’);
    equals( [1], [1], ‘fails’);
})

Чтобы проверить это равенство, QUnit предлагает еще одно утверждение: идентичное утверждение .

Идентичное утверждение, same (), ожидает те же параметры, что и equals (), но это глубокое рекурсивное утверждение сравнения, которое работает не только с примитивными типами, но также с массивами и объектами. Утверждения в предыдущем примере все пройдут, если вы измените их на идентичные утверждения:

1
2
3
4
5
6
test(‘test’, function() {
    same( {}, {}, ‘passes, objects have the same content’);
    same( {a: 1}, {a: 1} , ‘passes’);
    same( [], [], ‘passes, arrays have the same content’);
    same( [1], [1], ‘passes’);
})

Обратите внимание, что same () использует ‘===’ для сравнения, когда это возможно, поэтому это пригодится при сравнении специальных значений:

1
2
3
4
5
6
test(‘test’, function() {
    equals( 0, false, ‘true’);
    same( 0, false, ‘false’);
    equals( null, undefined, ‘true’);
    same( null, undefined, ‘false’);
})

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

Вы даже можете организовать тестовые случаи в разные модули, вызвав функцию модуля:

1
2
3
4
5
6
7
module(‘Module A’);
test(‘a test’, function() {});
test(‘an another test’, function() {});
 
module(‘Module B’);
test(‘a test’, function() {});
test(‘an another test’, function() {});
структурные утверждения

В предыдущих примерах все утверждения вызываются синхронно, что означает, что они выполняются одно за другим. В реальном мире есть также много асинхронных функций, таких как ajax-вызовы или функции, вызываемые setTimeout () и setInterval (). Как мы можем проверить эти виды функций? QUnit предоставляет специальный вид теста, называемый «асинхронный тест», который предназначен для асинхронного тестирования:

Давайте сначала попробуем написать это регулярно:

1
2
3
4
5
test(‘asynchronous test’, function() {
    setTimeout(function() {
        ok(true);
    }, 100)
})
неподходящий пример асинхронного теста

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

Вот правильная версия:

01
02
03
04
05
06
07
08
09
10
11
12
test(‘asynchronous test’, function() {
    // Pause the test first
    stop();
     
    setTimeout(function() {
        ok(true);
 
        // After the assertion has been called,
        // continue the test
        start();
    }, 100)
})
правильный пример асинхронного теста

Здесь мы используем stop (), чтобы приостановить тестовый случай, и после того, как утверждение было вызвано, мы используем start () для продолжения.

Вызов stop () сразу после вызова test () довольно распространен; поэтому QUnit предоставляет ярлык: asyncTest (). Вы можете переписать предыдущий пример так:

01
02
03
04
05
06
07
08
09
10
11
asyncTest(‘asynchronous test’, function() {
    // The test is automatically paused
     
    setTimeout(function() {
        ok(true);
 
        // After the assertion has been called,
        // continue the test
        start();
    }, 100)
})

Следует обратить внимание на одну вещь: setTimeout () всегда будет вызывать свою функцию обратного вызова, но что делать, если это пользовательская функция (например, вызов ajax). Как вы можете быть уверены, что функция обратного вызова будет вызвана? И если обратный вызов не вызывается, start () вызываться не будет, и весь модуль тестирования будет зависать:

модульное тестирование зависает

Итак, вот что вы делаете:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
// A custom function
function ajax(successCallback) {
    $.ajax({
        url: ‘server.php’,
        success: successCallback
    });
}
 
test(‘asynchronous test’, function() {
    // Pause the test, and fail it if start() isn’t called after one second
    stop(1000);
     
    ajax(function() {
        // …asynchronous assertions
 
        start();
    })
})

Вы передаете тайм-аут для stop (), который сообщает QUnit: «если start () не вызывается после этого тайм-аута, вы должны провалить этот тест». Вы можете быть уверены, что все тестирование не будет зависать, и вы будете уведомлены, если что-то пойдет не так.

Как насчет нескольких асинхронных функций? Где вы положили начало ()? Вы помещаете это в setTimeout ():

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// A custom function
function ajax(successCallback) {
    $.ajax({
        url: ‘server.php’,
        success: successCallback
    });
}
 
test(‘asynchronous test’, function() {
    // Pause the test
    stop();
     
    ajax(function() {
        // …asynchronous assertions
    })
 
    ajax(function() {
        // …asynchronous assertions
    })
 
    setTimeout(function() {
        start();
    }, 2000);
})

Время ожидания должно быть достаточно продолжительным, чтобы можно было вызывать оба обратных вызова перед продолжением теста. Но что, если один из обратных вызовов не вызывается? Как ты можешь это знать? Вот где ожидаем, что ожидается ():

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
// A custom function
function ajax(successCallback) {
    $.ajax({
        url: ‘server.php’,
        success: successCallback
    });
}
 
test(‘asynchronous test’, function() {
    // Pause the test
    stop();
 
    // Tell QUnit that you expect three assertions to run
    expect(3);
 
    ajax(function() {
        ok(true);
    })
 
    ajax(function() {
        ok(true);
        ok(true);
    })
 
    setTimeout(function() {
        start();
    }, 2000);
})

Вы передаете число, чтобы ожидать (), чтобы сообщить QUnit, что вы ожидаете, что X будет выполнять много подтверждений, если одно из утверждений не будет вызвано, число не будет совпадать, и вы будете уведомлены, что что-то пошло не так.

Существует также ярлык для ожидаемого (): вы просто передаете число в качестве второго параметра для test () или asyncTest ():

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
// A custom function
function ajax(successCallback) {
    $.ajax({
        url: ‘server.php’,
        success: successCallback
    });
}
 
// Tell QUnit that you expect three assertion to run
test(‘asynchronous test’, 3, function() {
    // Pause the test
    stop();
 
    ajax(function() {
        ok(true);
    })
 
    ajax(function() {
        ok(true);
        ok(true);
    })
 
    setTimeout(function() {
        start();
    }, 2000);
})

Это все, что вам нужно знать, чтобы начать работу с QUnit. Модульное тестирование — отличный способ проверить ваш код перед его публикацией. Если вы раньше не писали юнит-тесты, самое время начать! Спасибо за прочтение!

  • Подпишитесь на нас в Твиттере или подпишитесь на ленту Nettuts + RSS для получения лучших учебных материалов по веб-разработке.