Desarrollo a través de pruebas: mejora de habilidades

En el artículo anterior, examinamos aspectos teóricos. Es hora de comenzar a practicar.

imagen

Hagamos una implementación simple de la pila en JavaScript usando el desarrollo a través de pruebas.

Pila: una estructura de datos organizada según el principio de LIFO: Last In, First Out. Hay tres operaciones principales en la pila:

empujar : agregar elemento
pop : eliminar un elemento
vistazo : agregar elemento de cabeza

Crea una clase y llámala Pila. Para complicar la tarea, suponga que la pila tiene una capacidad fija. Aquí están las propiedades y funciones de implementación de nuestra pila:

artículos : artículos en la pila. Usaremos una matriz para implementar la pila.
capacidad : capacidad de apilamiento.
isEmpty () : devuelve verdadero si la pila está vacía, de lo contrario es falso.
isFull () : devuelve verdadero si la pila alcanza la capacidad máxima, es decir, cuando no puede agregar otro elemento. De lo contrario, devuelve falso.
push (elemento) : agrega un elemento. Devuelve Completo si la pila está llena.
pop : elimina el elemento. Devuelve Vacío si la pila está vacía.
peek () : agrega un elemento head.

Vamos a crear dos archivos stack.js y stack.spec.js . Usé la extensión .spec.js porque estoy acostumbrado, pero puedes usar .test.js o darle un nombre diferente y moverlo a __tests__ .

Como practicamos el desarrollo mediante pruebas, escribiremos una prueba fallida.

Primero, verifique el constructor. Para probar el archivo, debe importar el archivo de pila:

const Stack = require('./stack') 

Para aquellos que estén interesados ​​en por qué no utilicé la importación aquí, la última versión estable de Node.js no es compatible con esta característica hoy. Podría agregar a Babel, pero no quiero sobrecargarlo.

Cuando esté probando una clase o función, ejecute una prueba y describa qué archivo o clase está probando. Esto es sobre la pila:

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

Luego, debemos comprobar que cuando se inicializa la pila, se crea una matriz vacía y establecemos la capacidad correcta. Entonces, escribimos la siguiente prueba:

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

Tenga en cuenta que usamos toEqual y no usamos toBe para stack.items , porque no hacen referencia a la misma matriz.

Ahora ejecute la prueba de hilo stack.spec.js . Ejecutamos Jest en un archivo específico porque no queremos que otras pruebas se corrompan. Aquí está el resultado:

imagen

Stack no es un constructor . Por supuesto Todavía no hemos creado nuestra pila y no hemos creado un constructor.

En stack.js crea tu clase, constructor y exporta la clase:

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

Ejecute la prueba nuevamente:

imagen

Como no establecimos los elementos en el constructor, Jest esperaba que los elementos en la matriz fueran iguales a [], pero no estaban definidos. Entonces deberías inicializar los elementos:

 constructor() { this.items = [] } 

Si vuelve a ejecutar la prueba, obtendrá el mismo error de capacidad , por lo que también deberá configurar la capacidad:

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

Ejecute la prueba:

imagen

Si! Prueba aprobada Esto es lo que es TDD. ¡Espero que ahora las pruebas tengan más sentido para ti! Continuar?

isEmpty


Para probar isEmpty , vamos a inicializar una pila vacía, probar si isEmpty devuelve verdadero, agregar un elemento y verificarlo nuevamente.

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

Cuando ejecuta la prueba, debería obtener el siguiente error:

 TypeError: stack.isEmpty is not a function 

Para resolver este problema, necesitamos crear isEmpty dentro de la clase Stack :

 isEmpty () { } 

Si ejecuta la prueba, debería obtener otro error:

 Expected: true Received: undefined 

No se agrega nada a isEmpty . La pila está vacía si no hay elementos en ella:

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

isFull


Esto es lo mismo que isEmpty , ya que este ejercicio prueba esta función usando TDD. Encontrará la solución al final del artículo.

Empujar


Aquí necesitamos probar tres cosas:

  • Se debe agregar un nuevo elemento a la parte superior de la pila.
  • push devuelve "Full" si la pila está llena;
  • Un artículo que ha sido agregado recientemente debe ser devuelto.

Cree otro bloque usando describe for push . Ponemos este bloque dentro del principal.

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

Agregar elemento


Crea una nueva pila y agrega un elemento. El último elemento en la matriz de elementos debe ser el elemento que acaba de agregar.

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

Si ejecuta la prueba, verá que la inserción no está definida y esto es normal. push necesitará un parámetro para agregar algo a la pila:

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

Prueba aprobada de nuevo. ¿Notaste algo? Mantenemos una copia de esta línea:

 let stack = new Stack(3) 

Esto es muy molesto. Afortunadamente, tenemos un método beforeEach que le permite realizar algunas personalizaciones antes de cada ejecución de prueba. ¿Por qué no aprovechar esto?

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

Importante:



la pila debe declararse antes de cada uno . De hecho, si lo define en el método beforeEach , la variable de pila no se definirá en todas las pruebas porque no está en el área correcta.

Más importante que importante: también debemos crear un método afterEach . La instancia de pila ahora se usará para todas las pruebas. Esto puede causar algunas dificultades. Inmediatamente después de cada uno, agregue este método:

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

Ahora puede eliminar la inicialización de la pila en todas las pruebas. Aquí está el archivo de prueba completo:

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

Prueba de valor de retorno


Hay una prueba:

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

Cuando ejecute la prueba, obtendrá:

 Expected: 2 Received: undefined 

¡Nada vuelve dentro del empuje ! Debemos arreglar esto:

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

Volver lleno si la pila está llena


En esta prueba, primero necesitamos llenar la pila, agregar un elemento, asegurarnos de que no se haya agregado nada superfluo a la pila y que el valor de retorno sea Completo .

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

Verá este error cuando ejecute la prueba:

 Expected: 3 Received: 4 

Entonces se agrega el artículo. Esto es lo que queríamos. Primero debe verificar si la pila está llena antes de agregar algo:

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

Prueba aprobada

Ejercicio: pop y vistazo

Es hora de practicar. Probar e implementar pop and peek .

Consejos:

  • pop es muy similar a empujar
  • echar un vistazo también es como empujar
  • Hasta ahora, no hemos refactorizado el código, porque esto no era necesario. Puede haber una forma en estas funciones de refactorizar su código después de escribir las pruebas y pasarlas. No se preocupe, cambiando el código, se necesitan pruebas para esto.

No mire la solución a continuación sin intentar hacerlo usted mismo. La única forma de progresar es intentar, experimentar y practicar.

Solución


Bueno, ¿cómo va el ejercicio? Lo hiciste Si no, no se desanime. Las pruebas requieren tiempo y esfuerzo.

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

Si miras los archivos, verás que usé la condición triple en pop and peek. Esto es lo que refactoré. La implementación anterior se veía así:

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

El desarrollo a través de las pruebas nos permite refactorizar el código después de que se hayan escrito las pruebas. Encontré una implementación más corta sin preocuparme por el comportamiento de mis pruebas.

Ejecute la prueba nuevamente:


imagen

Las pruebas mejoran la calidad del código. Espero que ahora comprenda todos los beneficios de las pruebas y use TDD con más frecuencia.

¿Te convencemos de que las pruebas son cruciales y mejoran la calidad del código? Más bien, lea la parte 2 del artículo sobre desarrollo a través de pruebas. Y quién no leyó el primero, sigue el enlace .

Source: https://habr.com/ru/post/es430876/


All Articles