Cómo TDD una implementación de lista en Java

1. Información general

En este tutorial, veremos una implementación de lista personalizada mediante el proceso de desarrollo basado en pruebas (TDD).

Esta no es una introducción a TDD, por lo que asumimos que ya tiene una idea básica de lo que significa y el interés sostenido para mejorar.

En pocas palabras, TDD es una herramienta de diseño que nos permite impulsar nuestra implementación con la ayuda de pruebas .

Un descargo de responsabilidad rápido: no nos estamos enfocando en crear una implementación eficiente aquí, solo lo usamos como una excusa para mostrar las prácticas de TDD.

2. Comenzando

Primero, definamos el esqueleto de nuestra clase:

public class CustomList implements List { private Object[] internal = {}; // empty implementation methods } 

La clase CustomList implementa la interfaz List , por lo que debe contener implementaciones para todos los métodos declarados en esa interfaz.

Para comenzar, solo podemos proporcionar cuerpos vacíos para esos métodos. Si un método tiene un tipo de retorno, podemos devolver un valor arbitrario de ese tipo, como nulo para Object o falso para booleano .

En aras de la brevedad, omitiremos los métodos opcionales, junto con algunos métodos obligatorios que no se utilizan con frecuencia.

3. Ciclos TDD

Desarrollar nuestra implementación con TDD significa que primero debemos crear casos de prueba , definiendo así los requisitos para nuestra implementación. Solo entonces crearemos o arreglaremos el código de implementación para que esas pruebas pasen.

De manera muy simplificada, los tres pasos principales de cada ciclo son:

  1. Pruebas de redacción: defina los requisitos en forma de pruebas.
  2. Implementación de funciones: haga que las pruebas pasen sin centrarse demasiado en la elegancia del código
  3. Refactorización: mejore el código para que sea más fácil de leer y mantener mientras sigue pasando las pruebas

Repasaremos estos ciclos TDD para algunos métodos de la interfaz List , comenzando con los más simples.

4. El método isEmpty

El método isEmpty es probablemente el método más sencillo definido en la interfaz List . Aquí está nuestra implementación inicial:

@Override public boolean isEmpty() { return false; }

Esta definición de método inicial es suficiente para compilar. El cuerpo de este método se verá "obligado" a mejorar cuando se agreguen más y más pruebas.

4.1. El primer ciclo

Escribamos el primer caso de prueba que asegure que el método isEmpty devuelve verdadero cuando la lista no contiene ningún elemento:

@Test public void givenEmptyList_whenIsEmpty_thenTrueIsReturned() { List list = new CustomList(); assertTrue(list.isEmpty()); }

La prueba dada falla porque el método isEmpty siempre devuelve falso . Podemos hacer que pase simplemente cambiando el valor de retorno:

@Override public boolean isEmpty() { return true; }

4.2. El segundo ciclo

Para confirmar que el método isEmpty devuelve falso cuando la lista no está vacía, necesitamos agregar al menos un elemento:

@Test public void givenNonEmptyList_whenIsEmpty_thenFalseIsReturned() { List list = new CustomList(); list.add(null); assertFalse(list.isEmpty()); }

Ahora se requiere una implementación del método add . Este es el método de adición con el que comenzamos:

@Override public boolean add(E element) { return false; }

La implementación de este método no funciona porque no se realizan cambios en la estructura de datos internos de la lista. Actualicémoslo para almacenar el elemento agregado:

@Override public boolean add(E element) { internal = new Object[] { element }; return false; }

Nuestra prueba aún falla ya que el método isEmpty no se ha mejorado. Vamos a hacer eso:

@Override public boolean isEmpty() { if (internal.length != 0) { return false; } else { return true; } }

La prueba de no vacío pasa en este punto.

4.3. Refactorización

Ambos casos de prueba que hemos visto hasta ahora pasan, pero el código del método isEmpty podría ser más elegante.

Vamos a refactorizarlo:

@Override public boolean isEmpty() { return internal.length == 0; }

Podemos ver que las pruebas pasan, por lo que la implementación del método isEmpty está completa ahora.

5. El método del tamaño

Esta es nuestra implementación inicial del método de tamaño que permite que la clase CustomList compile:

@Override public int size() { return 0; }

5.1. El primer ciclo

Usando el método add existente , podemos crear la primera prueba para el método de tamaño , verificando que el tamaño de una lista con un solo elemento es 1 :

@Test public void givenListWithAnElement_whenSize_thenOneIsReturned() { List list = new CustomList(); list.add(null); assertEquals(1, list.size()); }

La prueba falla porque el método de tamaño devuelve 0 . Hagamos que pase con una nueva implementación:

@Override public int size() { if (isEmpty()) { return 0; } else { return internal.length; } }

5.2. Refactorización

Podemos refactorizar el método de tamaño para hacerlo más elegante:

@Override public int size() { return internal.length; }

La implementación de este método ahora está completa.

6. El método get

Aquí está la implementación inicial de get :

@Override public E get(int index) { return null; }

6.1. El primer ciclo

Echemos un vistazo a la primera prueba de este método, que verifica el valor del elemento individual en la lista:

@Test public void givenListWithAnElement_whenGet_thenThatElementIsReturned() { List list = new CustomList(); list.add("baeldung"); Object element = list.get(0); assertEquals("baeldung", element); }

La prueba pasará con esta implementación del método get :

@Override public E get(int index) { return (E) internal[0]; }

6.2. Mejora

Por lo general, agregamos más pruebas antes de realizar mejoras adicionales en el método get . Esas pruebas necesitarían otros métodos de la interfaz List para implementar las aserciones adecuadas.

However, these other methods aren't mature enough, yet, so we break the TDD cycle and create a complete implementation of the get method, which is, in fact, not very hard.

It's easy to imagine that get must extract an element from the internal array at the specified location using the index parameter:

@Override public E get(int index) { return (E) internal[index]; }

7. The add Method

This is the add method we created in section 4:

@Override public boolean add(E element) { internal = new Object[] { element }; return false; }

7.1. The First Cycle

The following is a simple test that verifies the return value of add:

@Test public void givenEmptyList_whenElementIsAdded_thenGetReturnsThatElement() { List list = new CustomList(); boolean succeeded = list.add(null); assertTrue(succeeded); }

We must modify the add method to return true for the test to pass:

@Override public boolean add(E element) { internal = new Object[] { element }; return true; }

Although the test passes, the add method doesn't cover all cases yet. If we add a second element to the list, the existing element will be lost.

7.2. The Second Cycle

Here's another test adding the requirement that the list can contain more than one element:

@Test public void givenListWithAnElement_whenAnotherIsAdded_thenGetReturnsBoth() { List list = new CustomList(); list.add("baeldung"); list.add(".com"); Object element1 = list.get(0); Object element2 = list.get(1); assertEquals("baeldung", element1); assertEquals(".com", element2); }

The test will fail since the add method in its current form doesn't allow more than one element to be added.

Let's change the implementation code:

@Override public boolean add(E element) { Object[] temp = Arrays.copyOf(internal, internal.length + 1); temp[internal.length] = element; internal = temp; return true; }

The implementation is elegant enough, hence we don't need to refactor it.

8. Conclusion

Este tutorial pasó por un proceso de desarrollo basado en pruebas para crear parte de una implementación de lista personalizada . Con TDD, podemos implementar requisitos paso a paso, mientras mantenemos la cobertura de la prueba en un nivel muy alto. Además, se garantiza que la implementación será comprobable, ya que fue creada para que las pruebas pasen.

Tenga en cuenta que la clase personalizada creada en este artículo solo se usa con fines de demostración y no debe adoptarse en un proyecto del mundo real.

El código fuente completo de este tutorial, incluidos los métodos de prueba e implementación que se dejaron fuera por brevedad, se puede encontrar en GitHub.