Development through testing: improving skills

Original author: Thomas Lombart
  • Transfer
In the previous article, we looked at theoretical aspects. It's time to start practicing.

image

Let's make a simple implementation of the stack in javascript using test-driven development.

A stack is a data structure organized on the principle of LIFO: Last In, First Out. There are three main operations on the stack:

push : adding a
pop element : deleting a
peek element : adding a head element

Let's create a class and name it Stack. To complicate the task, suppose the stack has a fixed capacity. Here are the properties and functions of implementing our stack:

items : stack items. We will use an array to implement the stack.
capacity : the capacity of the stack.
isEmpty () : returns true if the stack is empty, otherwise false.
isFull () : returns true if the stack reaches its maximum capacity, i.e. when you cannot add another item. Otherwise, returns false.
push (element) : adds an element. Returns Full if the stack is full.
pop : deletes the item. Returns Empty if stack is empty.
peek () : adds a head element.

We are going to create two stack.js and stack.spec.js files . I used the .spec.js extensionbecause I'm used to it, but you can use .test.js or give it a different name and move it to __tests__ .

As we practice in development through testing, we will write a failing test.

First check the constructor. To test a file, you need to import a stack file:

const Stack = require('./stack')

For those who are interested, why I did not use import here - the latest stable version of Node.js does not support this feature today. I could add Babel, but I do not want to overload you.

When you are testing a class or function, run the test and describe which file or class you are testing. Here we are talking about the stack:

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

Then we need to check that when the stack is initialized, an empty array is created, and we set the correct capacity. So, we write the following test:

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

Notice that we use toEqual and do not use toBe for stack.items , because they do not refer to the same array.

Now run yarn test stack.spec.js . We run jest in a specific file, because we don’t want other tests to be corrupted. Here is the result:

image

Stack is not a constructor . Of course. We still have not created our stack and have not done a constructor.

In stack.js, create your class constructor and export the class:

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

Run the test again:

image

Since we did not set the elements in the constructor, Jest expected the elements in the array to be equal to [], but they are not defined. Then you must initialize the elements:

constructor() {
  this.items = []
}

If you run the test again, you will get the same error for capacity , so you will also need to set the capacity:

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

Run the test:

image

Yes! Test passed . That's what TDD is. Hopefully now testing will make more sense to you! Continue?

isEmpty


To test isEmpty , we are going to initialize an empty stack, test if isEmpty returns true, add an item and check it again.

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)
})

When you run a test, you should get the following error:

TypeError: stack.isEmpty is not a function

To solve this problem, we need to create isEmpty inside the Stack class :

isEmpty () {
}

If you run the test, you should get another error:

Expected: true
Received: undefined

The isEmpty nothing is added. Stack is empty if there are no items in it:

isEmpty () {
  returnthis.items.length === 0
}

isFull


This is the same as isEmpty , since this exercise tests this function using TDD. You will find a solution at the very end of the article.

Push


Here we need to test three things:

  • A new element must be added to the top of the stack;
  • push returns "Full" if the stack is full;
  • The item that was recently added must be returned.

Create another block using describe for push . We put this block inside the main one.

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

Add item


Create a new stack and add an item. The last item in the items array should be the item you just added.

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)
  })
})

If you run the test, you will see that push is not defined and this is normal. push will need a parameter to add something to the stack:

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

The test is passed again. Did you notice something? We keep a copy of this line:

let stack = new Stack(3)

This is very annoying. Fortunately, we have a beforeEach method that allows you to perform some customization before each test run. Why not take advantage of this?

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

Important:



The stack must be declared before beforeEach . In fact, if you define it in the beforeEach method , the stack variable will not be defined in all tests, because it is not in the right area.

More important: we also need to create the afterEach method . The stack instance will now be used for all tests. This can cause some difficulties. Immediately after beforeEach, add this method:

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

Now you can remove the stack initialization in all tests. Here is the complete test file:

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)
    })
  })
})

Return Value Testing


There is a test:

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

When you run the test, you will receive:

Expected: 2
Received: undefined

Nothing returns inside the push ! We need to fix this:

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

Return full if stack is full


In this test, we need to first fill the stack, add an element, make sure that nothing extra has been added to the stack and that the return value is 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')
})

You will see this error when run the test:

Expected: 3
Received: 4

So the item is added. This is what we wanted. First you need to check if the stack is full before adding something:

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

Test passed.

Exercise: pop and peek It's

time to practice. Test and implement pop and peek .

Tips:

  • pop is very similar to push
  • peek also looks like a push
  • So far, we have not refactored the code, because it was not necessary. In these functions there can be a way to refactor your code after writing tests and passing them. Do not worry, changing the code, tests for this and need.

Do not look at the decision below without trying to make it yourself. The only way to progress is to try, experiment, and practice.

Decision


Well, how is the exercise? Did you do it? If not, do not be discouraged. Testing takes time and effort.

classStack{
  constructor (capacity) {
    this.items = []
    this.capacity = capacity
  }
  isEmpty () {
    returnthis.items.length === 0
  }
  isFull () {
    returnthis.items.length === this.capacity
  }
  push (element) {
    if (this.isFull()) {
      return'Full'
    }
    this.items.push(element)
    return element
  }
  pop () {
    returnthis.isEmpty() ? 'Empty' : this.items.pop()
  }
  peek () {
    returnthis.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')
    })
  })
})

If you look at the files, you will see that I used the triple condition in pop and peek. This is what I refactor. The old implementation looked like this:

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

Development through testing allows us to refactor the code after the tests were written, I found a shorter implementation, without worrying about the behavior of my tests.

Run the test again:


image

Tests improve the quality of the code. I hope you now understand all the benefits of testing and will use TDD more often.

Did we manage to convince you that testing is essential and improves the quality of the code? Rather, read the 2 part of the article about the development through testing. And who did not read the first, follow the link .

Also popular now: