Pruebas Unitarias Parte 1: ¿Cómo escribir código “Testeable”?


Esta es la 1era parte de una serie de posts sobre tests unitarios. Hablaremos sobre que es una prueba unitaria, como escribir código que sea fácil de probar y como codificar estas pruebas usando JUnit y Mockito.

Una prueba unitaria es un tipo de prueba de software que tiene como objetivo verificar el funcionamiento correcto de un componente individual de una aplicación aislándolo del resto de su entorno. Para realmente probar un componente de manera individual, es necesario tener pleno control sobre su entorno (entradas y dependencias), ya que de no hacerlo, estaremos probando el componente más sus dependencias, lo que hará más difícil identificar la causa raíz de algún defecto ya que no sabremos si el problema se encuentra en el componente que estamos probando o en alguna de sus dependencias. Hacer esto puede parecer complicado debido a que la mayoría de los componentes que tenemos en nuestras aplicaciones dependen de uno o más componentes para su funcionamiento (por ejemplo, un servicio puede depender de un repositorio para obtener información de una base de datos). Sin embargo, si desarrollamos nuestros componentes con la idea de que deben ser “testeables” desde un inicio, esto será  muy sencillo.

Un componente puede considerarse “testeable” si:

  1. Sigue el Principio de Responsabilidad Única
  2. Está correctamente encapsulado y tiene una interfaz pública bien definida
  3. Se puede aislar fácilmente del resto del sistema
  4. Sus salidas son observables, es decir, se puede observar fácilmente los resultados de una ejecución
  5. Tiene un comportamiento determinístico (ejecutar un mismo método varias veces con las mismas entradas siempre dará el mismo resultado)

El siguiente código es un ejemplo de una clase NO “testeable”.

public class PayrollServiceImpl {

    private EmployeeRepositoryImpl employeeRepository;
    private NotificationServiceImpl notificationService;

    public PayrollServiceImpl() {
        this.employeeRepository = new EmployeeRepositoryImpl();
        this.notificationService = new NotificationServiceImpl();
    }

    public void increaseSalary(int employeeId, BigDecimal amount) {
        Employee employee = this.employeeRepository.findEmployee(employeeID);

        // only employees with more than 5 years of experience can get a raise
        if (employee.getYearsOfExperience() > 5) {
            employee.setSalary(employee.getSalary() + amount);
            this.employeeRepository.save(employee);

            // notify employee about the raise
            this.notificationService.notifySalaryIncreased(employee);
        }
    }

}

Esta clase tiene varios problemas. En primer lugar, no se puede aislar fácilmente del resto del sistema ya que su constructor crea las dependencias explícitamente. En segundo lugar, las dependencias son clases concretas (no interfaces), lo que aumenta el acomplamiento de nuestra clase y hace más difícil controlar el comportamiento de las dependencias. Finalmente, no hay forma de observar cuál fue el resultado de llamar al método increaseSalary.

Una más “testeable” del código se muestra a continuación:

public class PayrollServiceImpl {

	private EmployeeRepository employeeRepository;
	private NotificationService notificationService;

	private static final int NOT_ENOUGH_EXPERIENCE = 405;
	private static final int SALARY_INCREASED = 123;

	public PayrollServiceImpl(EmployeeRepository employeeRepository, NotificationService notificationService) {
		this.employeeRepository = employeeRepository;
		this.notificationService = notificationService;
	}

	public int increaseSalary(int employeeId, BigDecimal amount) {
		Employee employee = this.employeeRepository.findEmployee(employeeId);
		// only employees with more than 5 years of experience can get a raise
		if (employee.getYearsOfExperience() > 5) {
			employee.setSalary(employee.getSalary() + amount);
			this.employeeRepository.save(employee);

			// notify employee about the raise
			this.notificationService.notifySalaryIncreased(employee);

			return SALARY_INCREASED;
		} else {
			return NOT_ENOUGH_EXPERIENCE;
		}
	}

}

Los problemas de la versión anterior de la clase PayrollServiceImpl se corrigieron de la siguiente manera:

  1. El constructor de la clase ahora recibe sus dependencias como parámetros
  2. Las dependencias son ahora interfaces en lugar de implementaciones concretas de las clases. Lo que facilita la creación de “mocks”.
  3. El método increaseSalary ahora devuelve códigos de estado para reflejar cuál fue el resultado de la ejecución

Los cambios realizados en la clase PayrollServiceImpl tienen varios beneficios: ahora podemos controlar cuáles son las dependencias de esta clase en nuestras pruebas unitarias, lo que nos permitirá usar “mocks” para controlar el funcionamiento de estas dependencias durante la prueba. Por ejemplo, podríamos pasar un “mock” de la interfaz NotificationService al constructor de clase de PayrollServiceImpl al inicializar nuestra prueba unitaria para así verificar si el método notifySalaryIncreased fue llamado o no. De manera similar, podríamos hacer que el “mock” del EmployeeRepository arroje una excepción al ser invocado para simular el caso en el que se pasa un employeeId no existente como parámetro.

Como podemos observar, escribir código “testeable” requiere algo de disciplina y un esfuerzo extra a la hora del diseño y desarrollo de nuestros componentes. Sin embargo, como recompensa, tenemos un código más fácil de probar, limpio, fácil de mantener y débilmente acoplado. Escribiendo pruebas unitarias adecuadas para nuestros componentes podremos incrementar la confianza que tenemos en el correcto funcionamiento de nuestro sistema.

En el siguiente post explicaremos como escribir dichas pruebas unitarias usando JUnit y mockito.

Anuncios

1 comentario »

Responder

Introduce tus datos o haz clic en un icono para iniciar sesión:

Logo de WordPress.com

Estás comentando usando tu cuenta de WordPress.com. Cerrar sesión /  Cambiar )

Google+ photo

Estás comentando usando tu cuenta de Google+. Cerrar sesión /  Cambiar )

Imagen de Twitter

Estás comentando usando tu cuenta de Twitter. Cerrar sesión /  Cambiar )

Foto de Facebook

Estás comentando usando tu cuenta de Facebook. Cerrar sesión /  Cambiar )

Conectando a %s