Статьи

Тестирование JavaScript с нуля

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

Этот урок намерен изменить ваши взгляды. Мы начнем с самого начала: что такое тестирование и зачем вам это делать? Затем мы кратко поговорим о написании тестируемого кода, прежде чем, на самом деле, вы знаете, что проведете тестирование! Давайте доберемся до этого!



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


Как вы можете догадаться, проблема с тестированием JavaScript «обнови и кликни» имеет две стороны:

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

Написав тесты, которые проверяют все, что должен делать ваш код, вы можете убедиться, что ваш код в лучшей форме, прежде чем использовать его на веб-сайте. К тому времени, когда что-то действительно запускается в браузере, возможно, есть несколько точек отказа. Написание тестов позволяет вам сосредоточиться на каждой тестируемой части индивидуально; если каждая часть выполняет свою работу правильно, все должно работать без проблем (тестирование отдельных частей, как это, называется модульным тестированием).


Если вы полиглот программирования), возможно, вы провели тестирование на других языках. Но я обнаружил, что тестирование в JavaScript — это совсем другое дело. В конце концов, вы не создаете слишком много пользовательских интерфейсов, скажем, в PHP или Ruby. Часто мы выполняем DOM-работу в JavaScript, и как именно вы это тестируете?

Ну, работа с DOM — это не то, для чего вы хотите писать тесты; это логика Очевидно, что ключом здесь является разделение вашей логики и кода пользовательского интерфейса. Это не всегда легко; Я написал свою долю пользовательского интерфейса на основе jQuery, и он может довольно быстро запутаться. Это не только затрудняет тестирование, но также может быть сложно изменить переплетенную логику и код пользовательского интерфейса при изменении желаемого поведения. Я обнаружил, что использование методологий, таких как шаблоны (также шаблоны ) и pub / sub (также pub / sub ), облегчает написание кода и делает его более тестируемым.

Еще одна вещь, прежде чем мы начнем кодировать: как мы пишем наши тесты? Существует множество библиотек тестирования, которые вы можете использовать (и множество хороших руководств, которые научат вас их использовать; см. Ссылки в конце). Однако мы собираемся создать небольшую тестовую библиотеку с нуля. Это будет не так красиво, как в некоторых библиотеках, но вы точно увидите, что происходит.

Имея это в виду, давайте приступим к работе!


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

Когда вы узнаете больше о тестировании и тестировании библиотек, вы найдете множество методов тестирования для тестирования всевозможных особенностей. Однако все это можно свести к тому, равны ли две вещи: например,

  • Значение, возвращаемое из этой функции, равно тому, что мы ожидали получить обратно?
  • Является ли эта переменная того типа, который мы ожидали?
  • Есть ли в этом массиве ожидаемое количество элементов?

Итак, вот наш метод проверки на равенство:

1
2
3
4
5
6
7
var TEST = {
    areEqual: function (a, b, msg) {
        var result = (a === b);
        console.log( (result ? «PASS: » : «FAIL: «) + msg );
        return result;
    }
};

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

Вот метод areNotEqual (в том же объекте TEST ):

1
2
3
4
5
areNotEqual: function (a,b, msg) {
    var result = (a !== b);
    console.log( (result ? «PASS: » : «FAIL: «) + msg );
    return result;
}

Вы заметите две последние строки areEqual и areNotEqual одинаковые. Итак, мы можем вытащить их так:

01
02
03
04
05
06
07
08
09
10
11
12
var TEST = {
    areEqual: function (a, b, msg) {
        return this._output(a === b, msg)
    },
    areNotEqual: function (a,b, msg) {
        return this._output(a !== b, msg);
    },
    _output : function (result, msg) {
        console[result ?
        return result;
    }
};

Большой! Здесь хорошо то, что мы можем добавить другие «сахарные» методы, используя методы, которые мы уже написали:

01
02
03
04
05
06
07
08
09
10
11
TEST.isTypeOf = function (obj, type, msg) {
    return this.areEqual(typeof obj, type, msg);
};
  
TEST.isAnInstanceOf = function (obj, type, msg) {
    return this._output(obj instanceof type, msg);
}
  
TEST.isGreaterThan = function (val, min, msg) {
    return this._output(val > min, msg);
}

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


Итак, давайте создадим супер-простую фотогалерею, используя нашу мини-среду TEST для создания нескольких тестов. Здесь я упомяну, что хотя разработка через тестирование — отличная практика, мы не будем использовать ее в этом учебнике, в первую очередь потому, что ее нельзя изучить в одном учебнике; Требуется много практики, чтобы действительно впасть Когда вы начинаете, легче написать немного кода, а затем протестировать его.

Итак, начнем. Конечно, нам понадобится HTML для нашей галереи. Мы оставим это довольно простым:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
<!DOCTYPE html>
<html>
  <head>
    <title> Testing in JavaScript </title>
    <link rel=»stylesheet» href=»gallery.css» />
  </head>
  <body>
    <section id=»gal-1″ class=»gallery»>
      <img src=»images/road.jpg» />
      <ul>
        <li><img src=»images/apple-thumb.jpg» /></li>
        <li><img src=»images/bikes-thumb.jpg» /></li>
        <li><img src=»images/bridge-thumb.jpg» /></li>
        <li><img src=»images/post-thumb.jpg» /></li>
        <li><img src=»images/road-thumb.jpg» /></li>
      </ul>
    </section>
  
    <script src=»test.js»></script>
    <script src=»gallery.js»></script>
    <script src=»gallery-test.js»></script>
  </body>
</html>

Здесь следует отметить две основные вещи: во-первых, у нас есть <section> который содержит очень простую разметку для нашей галереи изображений. Нет, это, вероятно, не очень надежно, но дает нам кое-что для работы. Затем обратите внимание, что мы подключаем три <script> s: один — наша маленькая тестовая библиотека, как было показано выше. Одним из них является галерея, которую мы создадим. Последний из них содержит тесты для нашей галереи. Обратите также внимание на пути к изображениям: к имени файла пиктограммы добавляется «-thumb». Вот как мы найдем увеличенную версию изображения.

Я знаю, что вам не gallery.css получить gallery.css , поэтому gallery.css это в файл gallery.css :

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
.gallery {
    background: #ececec;
    overflow: hidden;
    width: 620px;
}
.gallery > img {
    margin: 20px 20px 0;
    padding: 0;
}
.gallery ul {
    list-style-type: none;
    margin: 20px;
    padding: 0;
    overflow:hidden;
}
.gallery li {
    float:left;
    margin: 0 10px;
}
.gallery li:first-of-type {
    margin-left: 0px;
}
.gallery li:last-of-type {
    margin-right: 0px;
}

Теперь вы можете загрузить это в своем браузере и увидеть что-то вроде этого:

ОКЕЙ УЖЕ! Давайте напишем немного JavaScript, не так ли?


Откройте этот файл gallery.js . Вот как мы начинаем:

01
02
03
04
05
06
07
08
09
10
11
12
13
var Gallery = (function () {
    var Gallery = {},
      
    galleryPrototype = {
    };
  
    Gallery.create = function (id) {
        var gal = Object.create(galleryPrototype);
          
        return gal;
    };
    return Gallery;
}());

Мы будем добавлять больше, но это хорошее начало. Мы использовали вызывающую себя анонимную функцию (или выражение, вызываемое сразу), чтобы все было вместе. Наша «внутренняя» переменная Gallery будет возвращена и будет значением нашей общедоступной переменной Gallery . Как видите, вызов Gallery.create создаст новый объект галереи с Object.create . Если вы не знакомы с Object.create , он просто создает новый объект, используя объект, который вы передаете в качестве прототипа нового объекта (он также довольно совместим с браузером ). Мы Gallery.create этот прототип и добавим в наш метод Gallery.create . Но теперь давайте напишем наш первый тест:

1
2
3
var gal = Gallery.create(«gal-1»);
  
TEST.areEqual(typeof gal, «object», «Gallery should be an object»);

Мы начинаем с создания «экземпляра» Gallery ; Затем мы запускаем тест, чтобы увидеть, является ли возвращаемое значение объектом.

Поместите эти две строки в нашу gallery-test.js ; Теперь откройте нашу страницу index.html в браузере и откройте консоль JavaScript. Вы должны увидеть что-то вроде этого:


Большой! Наш первый тест пройден!


Далее мы заполним наш метод Gallery.create . Как вы увидите, мы не беспокоимся о том, чтобы сделать этот пример кода сверхнадежным, поэтому мы будем использовать некоторые вещи, которые несовместимы с каждым когда-либо созданным браузером. А именно, document.querySelector / document.querySelectorAll ; Кроме того, мы будем использовать только современную обработку событий браузера. Не стесняйтесь заменить свою любимую библиотеку, если хотите.

Итак, начнем с некоторых объявлений:

1
2
3
4
5
6
7
8
9
var gal = Object.create(galleryPrototype), ul, i = 0, len;
gal.el = document.getElementById(id);
ul = gal.el.querySelector(«ul»);
  
gal.imgs = gal.el.querySelectorAll(«ul li img»);
gal.displayImage = gal.el.querySelector(«img:first-child»);
gal.idx = 0;
gal.going = false;
gal.ids = [];

Четыре переменные: в частности, наш объект галереи и узел <ul> галереи (мы будем использовать i и len через минуту). Затем шесть объектов на нашем объекте gal :

  • gal.el является «корневым» узлом в разметке нашей галереи.
  • gal.imgs — это список <li> которые содержат наши миниатюры.
  • gal.displayImage — это большое изображение в нашей галерее.
  • gal.idx — это индекс текущего просматриваемого изображения.
  • gal.going — логическое значение: это true если галерея перебирает изображения.
  • gal.ids будет список идентификаторов для изображений в нашей галерее. Например, если миниатюра названа «dog-thumb.jpg», то «dog» — это идентификатор, а «dog.jpg» — изображение размера дисплея.

Обратите внимание, что элементы DOM также имеют методы querySelector и querySelectorAll . Мы можем использовать gal.el.querySelector чтобы гарантировать, что мы выбираем только элементы в разметке этой галереи.

Теперь заполните gal.ids идентификаторами изображений:

1
2
3
4
5
len = gal.imgs.length;
  
for (; i < len; i++ ) {
    gal.ids[i] = gal.imgs[i].getAttribute(«src»).split(«-thumb»)[0].split(«/»)[1];
}

Довольно просто, верно?

Наконец, давайте подключим обработчик событий к <ul> .

1
2
3
4
5
6
ul.addEventListener(«click», function (e) {
    var i = [].indexOf.call(gal.imgs, e.target);
    if (i > -1) {
        gal.set(i);
    }
}, false);

Мы начнем с проверки, находится ли в нашем списке изображений самый нижний элемент, получивший щелчок ( e.target ; мы не беспокоимся о поддержке oldIE здесь); Так как в NodeList нет метода indexOf , мы будем использовать версию массива (если вы не знакомы с call и apply JavaScript, см. наш краткий совет по этому вопросу. ). Если это больше, чем -1, мы передадим его в gal.set . Мы еще не написали этот метод, но мы доберемся до него.

Теперь вернемся к нашему файлу gallery-test.js и напишем несколько тестов, чтобы убедиться, что у нашего экземпляра Gallery правильные свойства:

1
2
3
TEST.areEqual(gal.el.id, «gal-1», «Gallery.el should be the one we specified»);
  
TEST.areEqual(gal.idx, 0, «Gallery index should start at zero»);

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


Теперь вернемся к тому объекту galleryPrototype который в настоящее время пуст. Именно здесь мы разместим все методы «экземпляров» нашей Gallery . Начнем с метода set : это самый важный метод, потому что он действительно изменяет отображаемое изображение. Требуется либо индекс изображения, либо строка идентификатора изображения.

1
2
3
4
5
6
7
8
// within `galleryProtytype`
set: function (i) {
    if (typeof i === &#39;string&#39;) {
        i = this.ids.indexOf(i);
    }
    this.displayImage.setAttribute(«src», «images/» + this.ids[i] + «.jpg»);
    return (this.idx = i);
}

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

Теперь давайте gallery-test.js этот метод (обратно в gallery-test.js ):

1
2
3
4
5
6
7
TEST.areEqual(gal.set(4), 4, «Gallery.set (with number) should return the same number passed in»);
  
TEST.areEqual(gal.displayImage.getAttribute(«src»), «images/road.jpg», «Gallery.set (with number) should change the displayed image»);
  
TEST.areEqual(gal.set(«post»), 3, «Gallery.set (with string) should move to appropriate image»);
  
TEST.areEqual(gal.displayImage.getAttribute(«src»), «images/post.jpg», «Gallery.set (with string) should change the displayed images»);

Мы тестируем наш тестовый метод с числом и строковым параметром для set . В этом случае мы можем проверить src для изображения и убедиться, что пользовательский интерфейс настроен соответствующим образом; не всегда возможно или необходимо убедиться, что то, что видит пользователь, отвечает соответствующим образом (без использования чего-либо подобного ); вот где полезен тип тестирования по клику. Тем не менее, мы можем сделать это здесь, так что мы будем.

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

Итак, давайте перейдем к некоторым методам, которые перемещаются между изображениями. Они могут быть полезны, если вы хотите иметь кнопки «Далее» и «Предыдущие» для циклического перемещения по изображениям (у нас не будет этих кнопок, но мы добавим вспомогательные методы).

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

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
// inside `galleryPrototype`
next: function () {
    if (this.idx === this.imgs.length — 1) {
        return this.set(0);
    }
    return this.set(this.idx + 1);
},
prev: function () {
    if (this.idx === 0) {
        return this.set(this.imgs.length — 1);
    }
    return this.set(this.idx — 1);
},
curr: function () {
    return this.idx;
},

Хорошо, так что на самом деле это не так сложно. Оба эти метода являются «сахарными» методами для использования set . Если мы на последнем изображении ( this.idx === this.imgs.length -1 ), мы set(0) . Если мы на первом месте ( this.idx === 0 ), мы set(this.imgs.length -1) . В противном случае просто добавьте или вычтите один из текущего индекса. Не забывайте, что мы возвращаем именно то, что возвращается из set вызова.

У нас также есть метод curr . Это совсем не сложно: просто возвращает текущий индекс. Мы проверим это чуть позже.

Итак, давайте проверим эти методы.

1
2
3
TEST.areEqual(gal.next(), 4, «Gallery should advance on .next()»);
  
TEST.areEqual(gal.prev(), 3, «Gallery should go back on .prev()»);

Они идут после наших предыдущих тестов, поэтому 4 и 3 — это значения, которые мы ожидаем. И они проходят!

Остался только один кусочек: это автоматическое фотоциклирование. Мы хотим иметь возможность вызывать gal.start() , воспроизводя изображения. Конечно, это будет метод gal.stop() .

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
// inside `galleryPrototype`
start: function (time) {
    var thiz = this;
    time = time ||
    this.interval = setInterval(function () {
        thiz.next();
    }, time);
    this.going = true;
    return true;
},
stop: function () {
    clearInterval(this.interval);
    this.going = false;
    return true;
},

Наш метод start будет принимать параметр: количество миллисекунд, в течение которых отображается одно изображение; если параметр не указан, по умолчанию используется значение 3000 (3 секунды). Затем мы просто используем setInterval для функции, которая будет вызывать next в соответствующее время. Конечно, мы не можем забыть установить this.going в true . Наконец, мы возвращаем true .

stop не так уж сложно. Поскольку мы сохранили интервал как this.interval , мы можем использовать clearInterval для его завершения. Затем мы устанавливаем this.going в false и возвращаем true.

У нас будет две удобные функции, позволяющие узнать, работает ли галерея:

1
2
3
4
5
6
isGoing: function () {
    return this.going;
},
isStopped: function () {
    return !this.going;
}

Теперь давайте проверим эту функциональность.

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
gal.set(0);
  
TEST.areEqual(gal.start(), true, «Gallery should being looping»);
  
TEST.areEqual(gal.curr(), 0, «Current image index should be 0»);
  
setTimeout(function () {
  
    TEST.areEqual(gal.curr(), 1, «Current image index should be 1»);
  
    TEST.areEqual(gal.isGoing(), true, «Gallery should be going»);
  
    TEST.areEqual(gal.stop(), true, «Gallery should be stopped»);
  
    setTimeout(function () {
  
        TEST.areEqual(gal.curr(), 1, «Current image should still be 1»);
  
        TEST.areEqual(gal.isStopped(), true, «Gallery should still be stopped»);
  
    }, 3050);
}, 3050);

Это немного сложнее, чем наши предыдущие наборы тестов: мы начинаем с использования gal.set(0) чтобы убедиться, что мы начинаем в начале. Затем мы вызываем gal.start() чтобы начать цикл. Затем мы проверяем, что gal.curr() возвращает 0, что означает, что мы все еще просматриваем первое изображение. Теперь мы будем использовать setTimeout чтобы подождать 3050 мс (чуть больше 3 секунд), прежде чем продолжить наши тесты. Внутри этого setTimeout мы сделаем еще один gal.curr() ; теперь индекс должен быть равен 1. Затем мы проверим, что gal.isGoing() имеет значение true. Далее мы остановим галерею gal.stop() . Теперь мы используем другой setTimeout чтобы ждать еще почти 3 секунды; если галерея действительно остановилась, изображение не будет зациклено, поэтому gal.curr() все равно должно быть равно 1; это то, что мы тестируем за время ожидания. Наконец, мы убедимся, что наш метод isStopped работает.

Если эти испытания прошли, поздравляю! Мы завершили нашу Gallery и ее тесты.


Если вы раньше не пробовали тестирование, я надеюсь, что вы видели, насколько простым может быть тестирование в JavaScript. Как я упоминал в начале этого урока, хорошее тестирование, скорее всего, потребует от вас написания JavaScript немного иначе, чем вы привыкли. Однако я обнаружил, что легко тестируемый JavaScript — это также легко поддерживаемый JavaScript.

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