Pull to refresh

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

Reading time7 min
Views4.9K
Original author: Thomas Lombart
В предыдущей статье мы рассмотрели теоретические аспекты. Самое время приступить к практике.

image

Сделаем простую реализацию стека в 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__.

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

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

const Stack = require('./stack')

Для тех, кому интересно, почему я не использовал здесь import — последняя стабильная версия Node.js не поддерживает эту функцию на сегодняшний день. Я мог бы добавить Babel, но не хочу перегружать вас.

Когда вы тестируете класс или функцию, запустите тест и опишите, какой файл или класс вы тестируете. Здесь речь идет о стеке:

describe('Stack', () => {
})

Затем нам нужно проверить, что при инициализации стека создается пустой массив, и мы устанавливаем правильную вместимость. Итак, пишем следующий тест:

it('Should constructs the stack with a given capacity', () => {
  let stack = new Stack(3)
  expect(stack.items).toEqual([])
  expect(stack.capacity).toBe(3)
})

Заметьте, что мы используем toEqual и не используем toBe для stack.items, потому что они не ссылаются на один и тот же массив.

Теперь запустим yarn test stack.spec.js. Мы запускаем Jest в определенном файле, потому что мы не хотим, чтобы другие тесты были испорчены. Вот результат:

image

Stack is not a constructor. Конечно. Мы до сих пор не создали наш стек и не сделали constructor.

В stack.js создайте свой класс, конструктор и экспортируйте класс:

class Stack {
  constructor() {
  }
}
module.exports = Stack

Запустите тест снова:

image

Поскольку мы не устанавливали элементы в конструкторе, Jest ожидал, что элементы в массиве будут равны [], но они не определены. Затем вы должны инициализировать элементы:

constructor() {
  this.items = []
}

Если вы снова запустите тест, вы получите такую же ошибку для capacity, поэтому вам также нужно будет установить вместимость:

constructor(capacity) {
  this.items = []
  this.capacity = capacity
}

Запустим тест:

image

Да! Тест пройден. Вот что такое TDD. Надеюсь, теперь тестирование будет иметь для вас больше смысла! Продолжим?

isEmpty


Чтобы проверить isEmpty, мы собираемся инициализировать пустой стек, протестировать, если isEmpty возвращает true, добавить элемент и снова проверить его.

it('Should have an isEmpty function that returns true if the stack is empty and false otherwise', () => {
  let stack = new Stack(3)
  expect(stack.isEmpty()).toBe(true)
  stack.items.push(2)
  expect(stack.isEmpty()).toBe(false)
})

Когда вы запустите тест, вы должны получить следующую ошибку:

TypeError: stack.isEmpty is not a function

Чтобы решить эту проблему, нам надо создать isEmpty внутри класса Stack:

isEmpty () {
}

Если вы запустите тест, вы должны получить еще одну ошибку:

Expected: true
Received: undefined

В isEmpty ничего не добавляется. Stack пуст, если в нем нет элементов:

isEmpty () {
  return this.items.length === 0
}

isFull


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

Push


Здесь нам надо протестировать три вещи:

  • новый элемент должен быть добавлен в начало стека;
  • push возвращает «Full», если стек заполнен;
  • элемент, который был недавно добавлен, должен быть возвращен.

Создадим другой блок, используя describe для push. Вкладываем этот блок внутри основного.

describe('Stack.push', () => {
  
})

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


Создадим новый стек и добавим элемент. Последним элементом массива items должен быть только что добавленный элемент.

describe('Stack.push', () => {
  it('Should add a new element on top of the stack', () => {
    let stack = new Stack(3)
    stack.push(2)
    expect(stack.items[stack.items.length - 1]).toBe(2)
  })
})

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

push (element) {
 this.items.push(element)
}

Тест снова пройден. Вы что-то заметили? Мы сохраняем копию этой строки:

let stack = new Stack(3)

Это очень раздражает. К счастью, у нас есть метод beforeEach, который позволяет выполнять некоторую настройку перед каждым тестовым прогоном. Почему бы не воспользоваться этим?

let stack
beforeEach(() => {
 stack = new Stack(3)
})

Важно:



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

Важнее важного: мы также должны создать метод afterEach. Экземпляр стека теперь будет использоваться для всех тестов. Это может вызвать некоторые трудности. Сразу после beforeEach добавьте этот метод:

afterEach(() => {
 stack.items = []
})

Теперь вы можете удалить инициализацию стека во всех тестах. Вот полный тестовый файл:

const Stack = require('./stack')
describe('Stack', () => {
  let stack
  beforeEach(() => {
    stack = new Stack(3)
  })
  afterEach(() => {
    stack.items = []
  })
  it('Should constructs the stack with a given capacity', () => {
    expect(stack.items).toEqual([])
    expect(stack.capacity).toBe(3)
  })
  it('Should have an isEmpty function that returns true if the stack is empty and false otherwise', () => {
    stack.items.push(2)
    expect(stack.isEmpty()).toBe(false)
  })
  describe('Stack.push', () => {
    it('Should add a new element on top of the stack', () => {
      stack.push(2)
      expect(stack.items[stack.items.length - 1]).toBe(2)
    })
  })
})

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


Есть тест:

it('Should return the new element pushed at the top of the stack', () => {
  let elementPushed = stack.push(2)
  expect(elementPushed).toBe(2)
})

Когда вы запустите тест, получите:

Expected: 2
Received: undefined

Ничего не возвращается внутри push! Мы должны исправить это:

push (element) {
  this.items.push(element)
  return element
}

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


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

it('Should return full if one tries to push at the top of the stack while it is full', () => {
  stack.items = [1, 2, 3]
  let element = stack.push(4)
  expect(stack.items[stack.items.length - 1]).toBe(3)
  expect(element).toBe('Full')
})

Вы увидите эту ошибку, когда запустите тест:

Expected: 3
Received: 4

Итак, элемент добавлен. Это то, что мы и хотели. Сначала нужно проверить, заполнен ли стек, прежде чем добавить что-то:

push (element) {
  if (this.isFull()) {
    return 'Full'
  }
  
  this.items.push(element)
  return element
}

Тест пройден.

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

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

Подсказки:

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

Не смотрите на решение ниже, не попытавшись сделать самостоятельно. Единственный способ прогресса — пытаться, экспериментировать и практиковаться.

Решение


Ну как упражнение? Справились? Если нет, не расстраивайтесь. Тестирование требует времени и усилий.

class Stack {

  constructor (capacity) {

    this.items = []

    this.capacity = capacity

  }


  isEmpty () {

    return this.items.length === 0

  }



  isFull () {

    return this.items.length === this.capacity

  }



  push (element) {

    if (this.isFull()) {

      return 'Full'

    }

    this.items.push(element)

    return element

  }



  pop () {

    return this.isEmpty() ? 'Empty' : this.items.pop()

  }



  peek () {

    return this.isEmpty() ? 'Empty' : this.items[this.items.length - 1]

  }

}



module.exports = Stack


const Stack = require('./stack')



describe('Stack', () => {

  let stack



  beforeEach(() => {

    stack = new Stack(3)

  })



  afterEach(() => {

    stack.items = []

  })



  it('Should construct the stack with a given capacity', () => {

    expect(stack.items).toEqual([])

    expect(stack.capacity).toBe(3)

  })



  it('Should have an isEmpty function that returns true if the stack is empty and false otherwise', () => {

    expect(stack.isEmpty()).toBe(true)

    stack.items.push(2)

    expect(stack.isEmpty()).toBe(false)

  })



  it('Should have an isFull function that returns true if the stack is full and false otherwise', () => {

    expect(stack.isFull()).toBe(false)

    stack.items = [4, 5, 6]

    expect(stack.isFull()).toBe(true)

  })



  describe('Push', () => {

    it('Should add a new element on top of the stack', () => {

      stack.push(2)

      expect(stack.items[stack.items.length - 1]).toBe(2)

    })



    it('Should return the new element pushed at the top of the stack', () => {

      let elementPushed = stack.push(2)

      expect(elementPushed).toBe(2)

    })



    it('Should return full if one tries to push at the top of the stack while it is full', () => {

      stack.items = [1, 2, 3]

      let element = stack.push(4)

      expect(stack.items[stack.items.length - 1]).toBe(3)

      expect(element).toBe('Full')

    })

  })



  describe('Pop', () => {

    it('Should removes the last element at the top of a stack', () => {

      stack.items = [1, 2, 3]

      stack.pop()

      expect(stack.items).toEqual([1, 2])

    })



    it('Should returns the element that have been just removed', () => {

      stack.items = [1, 2, 3]

      let element = stack.pop()

      expect(element).toBe(3)

    })



    it('Should return Empty if one tries to pop an empty stack', () => {

      // By default, the stack is empty

      expect(stack.pop()).toBe('Empty')

    })

  })



  describe('Peek', () => {

    it('Should returns the element at the top of the stack', () => {

      stack.items = [1, 2, 3]

      let element = stack.peek()

      expect(element).toBe(3)

    })



    it('Should return Empty if one tries to peek an empty stack', () => {

      // By default, the stack is empty

      expect(stack.peek()).toBe('Empty')

    })

  })

})

Если вы посмотрите на файлы, вы увидите, что я использовал тройное условие в pop и peek. Это то, что я рефакторил. Старая реализация выглядела так:

if (this.isEmpty()) {
  return 'Empty'
}
return this.items.pop()

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

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


image

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

Нам удалось вас убедить, что тестирование крайне важно и улучшает качество кода? Скорее читайте 2 часть статьи про разработку через тестирование. А кто не читал первую, переходите по ссылке.
Tags:
Hubs:
Total votes 10: ↑7 and ↓3+4
Comments2

Articles