Главное Авторские колонки Вакансии Образование
102 0 В избр. Сохранено
Авторизуйтесь
Вход с паролем

Разработка через тестирование: улучшаем навыки. Часть 2

В предыдущей статье мы рассмотрели теоретические аспекты. Самое время приступить к практике.
Мнение автора может не совпадать с мнением редакции

Сделаем простую реализацию стека в JavaScript с помощью разработки через тестирование.

Стек — структура данных, организованная по принципу LIFO: Last In, First Out. В стеке есть три основные операции:

push: добавление элемента

pop: удаление элемента

peek: добавление головного элемента

Создадим класс и назовем Stack. Чтобы усложнить задачу, предположим, что стек имеет фиксированную вместимость. Вот свойства и функции реализации нашего стека:

items: элементы стека. Мы будем использовать массив для реализации стека.

capacity: вместимость стека.

isEmpty(): возвращает true, если стек пуст, иначе false.

isFull(): возвращает true, если стек достигает максимальной емкости, т.е. когда вы не можете добавить другой элемент. В противном случае возвращает false.

push(element): добавляет элемент. Возвращает Full, если стек переполнен.

pop: удаляет элемент. Возвращает Empty, если стек пуст.

peek(): добавляет головной элемент.

Мы собираемся создать два файла stack.js и stack.spec.js. Я использовал расширение .spec.js, потому что привык к нему, но вы можете использовать .test.js или дать ему другое имя и переместить в __tests__.

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

Сначала проверим конструктор. Чтобы протестировать файл, вам необходимо импортировать файл стека:

<code><span class="hljs-keyword">const</span> Stack = <span class="hljs-built_in">require</span>(<span class="hljs-string">'./stack'</span>)</code>
Для тех, кому интересно, почему я не использовал здесь import — последняя стабильная версия Node.js не поддерживает эту функцию на сегодняшний день. Я мог бы добавить Babel, но не хочу перегружать вас. Когда вы тестируете класс или функцию, запустите тест и опишите, какой файл или класс вы тестируете. Здесь речь идет о стеке:
<code>describe(<span class="hljs-string">'Stack'</span>, () => { })</code>
Затем нам нужно проверить, что при инициализации стека создается пустой массив, и мы устанавливаем правильную вместимость. Итак, пишем следующий тест:
<code>it(<span class="hljs-string">'Should constructs the stack with a given capacity'</span>, () => { <span class="hljs-keyword">let</span> stack = <span class="hljs-keyword">new</span> Stack(<span class="hljs-number">3</span>) expect(stack.items).toEqual([]) expect(stack.capacity).toBe(<span class="hljs-number">3</span>) })</code>
Заметьте, что мы используем toEqual и не используем toBe для stack.items, потому что они не ссылаются на один и тот же массив.Теперь запустим yarn test stack.spec.js. Мы запускаем Jest в определенном файле, потому что мы не хотим, чтобы другие тесты были испорчены. Вот результат:Stack is not a constructor. Конечно. Мы до сих пор не создали наш стек и не сделали constructor. В stack.js создайте свой класс, конструктор и экспортируйте класс:
<code><span class="hljs-class"><span class="hljs-keyword"><span class="hljs-class"><span class="hljs-keyword">class</span></span></span> <span class="hljs-title"><span class="hljs-class"><span class="hljs-title">Stack</span></span></span> </span>{ <span class="hljs-keyword">constructor</span>() { } } <span class="hljs-built_in">module</span>.exports = Stack</code>
Запустите тест снова:Поскольку мы не устанавливали элементы в конструкторе, Jest ожидал, что элементы в массиве будут равны [], но они не определены. Затем вы должны инициализировать элементы:
<code><span class="hljs-keyword">constructor</span>() { <span class="hljs-keyword">this</span>.items = [] }</code>
Если вы снова запустите тест, вы получите такую же ошибку для capacity, поэтому вам также нужно будет установить вместимость:
<code><span class="hljs-keyword">constructor</span>(capacity) { <span class="hljs-keyword">this</span>.items = [] <span class="hljs-keyword">this</span>.capacity = capacity }</code>
Запустим тест:Да! Тест пройден. Вот что такое TDD. Надеюсь, теперь тестирование будет иметь для вас больше смысла! Продолжим?

isEmpty

Чтобы проверить isEmpty, мы собираемся инициализировать пустой стек, протестировать, если isEmpty возвращает true, добавить элемент и снова проверить его.
<code>it(<span class="hljs-string">'Should have an isEmpty function that returns true if the stack is empty and false otherwise'</span>, () => { <span class="hljs-keyword">let</span> stack = <span class="hljs-keyword">new</span> Stack(<span class="hljs-number">3</span>) expect(stack.isEmpty()).toBe(<span class="hljs-literal">true</span>) stack.items.push(<span class="hljs-number">2</span>) expect(stack.isEmpty()).toBe(<span class="hljs-literal">false</span>) })</code>
Когда вы запустите тест, вы должны получить следующую ошибку:
<code><span class="hljs-built_in">TypeError</span>: stack.isEmpty is not a <span class="hljs-function"><span class="hljs-keyword">function</span></span></code>
Чтобы решить эту проблему, нам надо создать isEmpty внутри класса Stack:
<code>isEmpty () { }</code>
Если вы запустите тест, вы должны получить еще одну ошибку:
<code>Expected: <span class="hljs-literal">true</span> Received: <span class="hljs-literal">undefined</span></code>
В isEmpty ничего не добавляется. Stack пуст, если в нем нет элементов:
<code>isEmpty () { <span class="hljs-keyword">return</span> <span class="hljs-keyword">this</span>.items.length === <span class="hljs-number">0</span> }</code>

isFull

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

Push

Здесь нам надо протестировать три вещи:
  • новый элемент должен быть добавлен в начало стека;
  • push возвращает Full, если стек заполнен;
  • элемент, который был недавно добавлен, должен быть возвращен.
Создадим другой блок, используя describe для push. Вкладываем этот блок внутри основного.
<code>describe(<span class="hljs-string">'Stack.push'</span>, () => { })</code>

Добавление элемента

Создадим новый стек и добавим элемент. Последним элементом массива items должен быть только что добавленный элемент.
<code>describe(<span class="hljs-string">'Stack.push'</span>, () => { it(<span class="hljs-string">'Should add a new element on top of the stack'</span>, () => { <span class="hljs-keyword">let</span> stack = <span class="hljs-keyword">new</span> Stack(<span class="hljs-number">3</span>) stack.push(<span class="hljs-number">2</span>) expect(stack.items[stack.items.length - <span class="hljs-number">1</span>]).toBe(<span class="hljs-number">2</span>) }) })</code>
Если вы запустите тест, то увидите, что push не определен и это нормально. push потребуется параметр, чтобы что-то добавить в стек:
<code>push (element) { <span class="hljs-keyword">this</span>.items.push(element) }</code>
Тест снова пройден. Вы что-то заметили? Мы сохраняем копию этой строки:
<code><span class="hljs-keyword">let</span> stack = <span class="hljs-keyword">new</span> Stack(<span class="hljs-number">3</span>)</code>
Это очень раздражает. К счастью, у нас есть метод beforeEach, который позволяет выполнять некоторую настройку перед каждым тестовым прогоном. Почему бы не воспользоваться этим?
<code><span class="hljs-keyword">let</span> stack beforeEach(<span class="hljs-function"><span class="hljs-params"><span class="hljs-function"><span class="hljs-params">()</span></span></span> =></span> { stack = <span class="hljs-keyword">new</span> Stack(<span class="hljs-number">3</span>) })</code>

Важно:

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

Важнее важного:

мы также должны создать метод afterEach. Экземпляр стека теперь будет использоваться для всех тестов. Это может вызвать некоторые трудности. Сразу после beforeEach добавьте этот метод:
<code>afterEach(<span class="hljs-function"><span class="hljs-params"><span class="hljs-function"><span class="hljs-params">()</span></span></span> =></span> { stack.items = [] })</code>
Теперь вы можете удалить инициализацию стека во всех тестах. Вот полный тестовый файл:
<code><span class="hljs-keyword">const</span> Stack = <span class="hljs-built_in">require</span>(<span class="hljs-string">'./stack'</span>) describe(<span class="hljs-string">'Stack'</span>, () => { <span class="hljs-keyword">let</span> stack beforeEach(<span class="hljs-function"><span class="hljs-params"><span class="hljs-function"><span class="hljs-params">()</span></span></span> =></span> { stack = <span class="hljs-keyword">new</span> Stack(<span class="hljs-number">3</span>) }) afterEach(<span class="hljs-function"><span class="hljs-params"><span class="hljs-function"><span class="hljs-params">()</span></span></span> =></span> { stack.items = [] }) it(<span class="hljs-string">'Should constructs the stack with a given capacity'</span>, () => { expect(stack.items).toEqual([]) expect(stack.capacity).toBe(<span class="hljs-number">3</span>) }) it(<span class="hljs-string">'Should have an isEmpty function that returns true if the stack is empty and false otherwise'</span>, () => { stack.items.push(<span class="hljs-number">2</span>) expect(stack.isEmpty()).toBe(<span class="hljs-literal">false</span>) }) describe(<span class="hljs-string">'Stack.push'</span>, () => { it(<span class="hljs-string">'Should add a new element on top of the stack'</span>, () => { stack.push(<span class="hljs-number">2</span>) expect(stack.items[stack.items.length - <span class="hljs-number">1</span>]).toBe(<span class="hljs-number">2</span>) }) }) })</code>

Тестирование возвращаемого значения

Есть тест:

<code>it(<span class="hljs-string">'Should return the new element pushed at the top of the stack'</span>, () => { <span class="hljs-keyword">let</span> elementPushed = stack.push(<span class="hljs-number">2</span>) expect(elementPushed).toBe(<span class="hljs-number">2</span>) })</code>
Когда вы запустите тест, получите:
<code>Expected: <span class="hljs-number">2</span> Received: <span class="hljs-literal">undefined</span></code>
Ничего не возвращается внутри push! Мы должны исправить это:
<code>push (element) { <span class="hljs-keyword">this</span>.items.push(element) <span class="hljs-keyword">return</span> element }</code>

Возвращение full, если стек переполнен

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

<code>it(<span class="hljs-string">'Should return full if one tries to push at the top of the stack while it is full'</span>, () => { stack.items = [<span class="hljs-number">1</span>, <span class="hljs-number">2</span>, <span class="hljs-number">3</span>] <span class="hljs-keyword">let</span> element = stack.push(<span class="hljs-number">4</span>) expect(stack.items[stack.items.length - <span class="hljs-number">1</span>]).toBe(<span class="hljs-number">3</span>) expect(element).toBe(<span class="hljs-string">'Full'</span>) })</code>
Вы увидите эту ошибку, когда запустите тест:
<code>Expected: <span class="hljs-number">3</span> Received: <span class="hljs-number">4</span></code>
Итак, элемент добавлен. Это то, что мы и хотели. Сначала нужно проверить, заполнен ли стек, прежде чем добавить что-то:
<code>push (element) { <span class="hljs-keyword">if</span> (<span class="hljs-keyword">this</span>.isFull()) { <span class="hljs-keyword">return</span> <span class="hljs-string">'Full'</span> } <span class="hljs-keyword">this</span>.items.push(element) <span class="hljs-keyword">return</span> element }</code>
Тест пройден.

Упражнение: pop и peek

Самое время попрактиковаться. Протестируйте и реализуйте pop и peek.

Подсказки:

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

Решение

Ну как упражнение? Справились? Если нет, не расстраивайтесь. Тестирование требует времени и усилий.
<code><span class="hljs-class"><span class="hljs-keyword"><span class="hljs-class"><span class="hljs-keyword">class</span></span></span> <span class="hljs-title"><span class="hljs-class"><span class="hljs-title">Stack</span></span></span> </span>{ <span class="hljs-keyword">constructor</span> (capacity) { <span class="hljs-keyword">this</span>.items = [] <span class="hljs-keyword">this</span>.capacity = capacity } isEmpty () { <span class="hljs-keyword">return</span> <span class="hljs-keyword">this</span>.items.length === <span class="hljs-number">0</span> } isFull () { <span class="hljs-keyword">return</span> <span class="hljs-keyword">this</span>.items.length === <span class="hljs-keyword">this</span>.capacity } push (element) { <span class="hljs-keyword">if</span> (<span class="hljs-keyword">this</span>.isFull()) { <span class="hljs-keyword">return</span> <span class="hljs-string">'Full'</span> } <span class="hljs-keyword">this</span>.items.push(element) <span class="hljs-keyword">return</span> element } pop () { <span class="hljs-keyword">return</span> <span class="hljs-keyword">this</span>.isEmpty() ? <span class="hljs-string">'Empty'</span> : <span class="hljs-keyword">this</span>.items.pop() } peek () { <span class="hljs-keyword">return</span> <span class="hljs-keyword">this</span>.isEmpty() ? <span class="hljs-string">'Empty'</span> : <span class="hljs-keyword">this</span>.items[<span class="hljs-keyword">this</span>.items.length - <span class="hljs-number">1</span>] } } <span class="hljs-built_in">module</span>.exports = Stack <span class="hljs-keyword">const</span> Stack = <span class="hljs-built_in">require</span>(<span class="hljs-string">'./stack'</span>) describe(<span class="hljs-string">'Stack'</span>, () => { <span class="hljs-keyword">let</span> stack beforeEach(<span class="hljs-function"><span class="hljs-params"><span class="hljs-function"><span class="hljs-params">()</span></span></span> =></span> { stack = <span class="hljs-keyword">new</span> Stack(<span class="hljs-number">3</span>) }) afterEach(<span class="hljs-function"><span class="hljs-params"><span class="hljs-function"><span class="hljs-params">()</span></span></span> =></span> { stack.items = [] }) it(<span class="hljs-string">'Should construct the stack with a given capacity'</span>, () => { expect(stack.items).toEqual([]) expect(stack.capacity).toBe(<span class="hljs-number">3</span>) }) it(<span class="hljs-string">'Should have an isEmpty function that returns true if the stack is empty and false otherwise'</span>, () => { expect(stack.isEmpty()).toBe(<span class="hljs-literal">true</span>) stack.items.push(<span class="hljs-number">2</span>) expect(stack.isEmpty()).toBe(<span class="hljs-literal">false</span>) }) it(<span class="hljs-string">'Should have an isFull function that returns true if the stack is full and false otherwise'</span>, () => { expect(stack.isFull()).toBe(<span class="hljs-literal">false</span>) stack.items = [<span class="hljs-number">4</span>, <span class="hljs-number">5</span>, <span class="hljs-number">6</span>] expect(stack.isFull()).toBe(<span class="hljs-literal">true</span>) }) describe(<span class="hljs-string">'Push'</span>, () => { it(<span class="hljs-string">'Should add a new element on top of the stack'</span>, () => { stack.push(<span class="hljs-number">2</span>) expect(stack.items[stack.items.length - <span class="hljs-number">1</span>]).toBe(<span class="hljs-number">2</span>) }) it(<span class="hljs-string">'Should return the new element pushed at the top of the stack'</span>, () => { <span class="hljs-keyword">let</span> elementPushed = stack.push(<span class="hljs-number">2</span>) expect(elementPushed).toBe(<span class="hljs-number">2</span>) }) it(<span class="hljs-string">'Should return full if one tries to push at the top of the stack while it is full'</span>, () => { stack.items = [<span class="hljs-number">1</span>, <span class="hljs-number">2</span>, <span class="hljs-number">3</span>] <span class="hljs-keyword">let</span> element = stack.push(<span class="hljs-number">4</span>) expect(stack.items[stack.items.length - <span class="hljs-number">1</span>]).toBe(<span class="hljs-number">3</span>) expect(element).toBe(<span class="hljs-string">'Full'</span>) }) }) describe(<span class="hljs-string">'Pop'</span>, () => { it(<span class="hljs-string">'Should removes the last element at the top of a stack'</span>, () => { stack.items = [<span class="hljs-number">1</span>, <span class="hljs-number">2</span>, <span class="hljs-number">3</span>] stack.pop() expect(stack.items).toEqual([<span class="hljs-number">1</span>, <span class="hljs-number">2</span>]) }) it(<span class="hljs-string">'Should returns the element that have been just removed'</span>, () => { stack.items = [<span class="hljs-number">1</span>, <span class="hljs-number">2</span>, <span class="hljs-number">3</span>] <span class="hljs-keyword">let</span> element = stack.pop() expect(element).toBe(<span class="hljs-number">3</span>) }) it(<span class="hljs-string">'Should return Empty if one tries to pop an empty stack'</span>, () => { <em>// By default, the stack is empty</em> expect(stack.pop()).toBe(<span class="hljs-string">'Empty'</span>) }) }) describe(<span class="hljs-string">'Peek'</span>, () => { it(<span class="hljs-string">'Should returns the element at the top of the stack'</span>, () => { stack.items = [<span class="hljs-number">1</span>, <span class="hljs-number">2</span>, <span class="hljs-number">3</span>] <span class="hljs-keyword">let</span> element = stack.peek() expect(element).toBe(<span class="hljs-number">3</span>) }) it(<span class="hljs-string">'Should return Empty if one tries to peek an empty stack'</span>, () => { <em>// By default, the stack is empty</em> expect(stack.peek()).toBe(<span class="hljs-string">'Empty'</span>) }) }) })</code>
Если вы посмотрите на файлы, вы увидите, что я использовал тройное условие в pop и peek. Это то, что я рефакторил. Старая реализация выглядела так:
<code><span class="hljs-keyword">if</span> (<span class="hljs-keyword">this</span>.isEmpty()) { <span class="hljs-keyword">return</span> <span class="hljs-string">'Empty'</span> } <span class="hljs-keyword">return</span> <span class="hljs-keyword">this</span>.items.pop()</code>

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

Запустим тест еще раз:

Тесты улучшают качество кода. Надеюсь, вы теперь понимаете всю пользу тестирования и будете чаще применять TDD.

Перевод статьи How to get better at testing with test-driven development от Digital Skynet :)

0
В избр. Сохранено
Авторизуйтесь
Вход с паролем