Java >> Tutorial de Java >  >> Tag >> new

Escribir pruebas limpias:nuevo considerado nocivo

La creación de nuevos objetos es una parte esencial de las pruebas automatizadas, y la forma más obvia de hacerlo es usar el nuevo palabra clave.

Sin embargo, esta no es la mejor manera de crear nuevos objetos en nuestros casos de prueba y usando el nuevo palabra clave hará que nuestras pruebas sean más difíciles de leer y mantener.

Esta publicación de blog identifica los problemas causados ​​por la nueva palabra clave y describe cómo podemos resolver estos problemas mediante el uso de métodos de fábrica y el patrón de creación.

Lo nuevo no es el nuevo negro

Durante este tutorial, hemos estado refactorizando una prueba unitaria que garantiza que registerNewUserAccount(RegistrationForm userAccountData) método del RepositoryUserService La clase funciona como se espera cuando se crea una nueva cuenta de usuario mediante una dirección de correo electrónico única y un proveedor de inicio de sesión social.

El Formulario de Registro La clase es un objeto de transferencia de datos (DTO), y nuestras pruebas unitarias establecen sus valores de propiedad mediante el uso de métodos setter. El código fuente de nuestra prueba unitaria tiene el siguiente aspecto (el código relevante está resaltado):

import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.Mock;
import org.mockito.invocation.InvocationOnMock;
import org.mockito.runners.MockitoJUnitRunner;
import org.mockito.stubbing.Answer;
import org.springframework.security.crypto.password.PasswordEncoder;

import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNull;
import static org.mockito.Matchers.isA;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.verifyNoMoreInteractions;
import static org.mockito.Mockito.verifyZeroInteractions;
import static org.mockito.Mockito.when;


@RunWith(MockitoJUnitRunner.class)
public class RepositoryUserServiceTest {

    private static final String REGISTRATION_EMAIL_ADDRESS = "[email protected]";
    private static final String REGISTRATION_FIRST_NAME = "John";
    private static final String REGISTRATION_LAST_NAME = "Smith";
    private static final Role ROLE_REGISTERED_USER = Role.ROLE_USER;
    private static final SocialMediaService SOCIAL_SIGN_IN_PROVIDER = SocialMediaService.TWITTER;

    private RepositoryUserService registrationService;

    @Mock
    private PasswordEncoder passwordEncoder;

    @Mock
    private UserRepository repository;

    @Before
    public void setUp() {
        registrationService = new RepositoryUserService(passwordEncoder, repository);
    }


    @Test
    public void registerNewUserAccount_SocialSignInAndUniqueEmail_ShouldCreateNewUserAccountAndSetSignInProvider() throws DuplicateEmailException       {
        RegistrationForm registration = new RegistrationForm();
        registration.setEmail(REGISTRATION_EMAIL_ADDRESS);
        registration.setFirstName(REGISTRATION_FIRST_NAME);
        registration.setLastName(REGISTRATION_LAST_NAME);
        registration.setSignInProvider(SOCIAL_SIGN_IN_PROVIDER);

        when(repository.findByEmail(REGISTRATION_EMAIL_ADDRESS)).thenReturn(null);

        when(repository.save(isA(User.class))).thenAnswer(new Answer<User>() {
            @Override
            public User answer(InvocationOnMock invocation) throws Throwable {
                Object[] arguments = invocation.getArguments();
                return (User) arguments[0];
            }
        });

        User createdUserAccount = registrationService.registerNewUserAccount(registration);

        assertEquals(REGISTRATION_EMAIL_ADDRESS, createdUserAccount.getEmail());
        assertEquals(REGISTRATION_FIRST_NAME, createdUserAccount.getFirstName());
        assertEquals(REGISTRATION_LAST_NAME, createdUserAccount.getLastName());
        assertEquals(SOCIAL_SIGN_IN_PROVIDER, createdUserAccount.getSignInProvider());
        assertEquals(ROLE_REGISTERED_USER, createdUserAccount.getRole());
        assertNull(createdUserAccount.getPassword());

        verify(repository, times(1)).findByEmail(REGISTRATION_EMAIL_ADDRESS);
        verify(repository, times(1)).save(createdUserAccount);
        verifyNoMoreInteractions(repository);
        verifyZeroInteractions(passwordEncoder);
    }
}

¿Entonces, cuál es el problema? La parte resaltada de nuestra prueba unitaria es breve y relativamente fácil de leer. En mi opinión, el mayor problema de este código es que está centrado en los datos. Crea un nuevo RegistrationForm objeto y establece los valores de propiedad del objeto creado, pero no describe el significado de estos valores de propiedad.

Si creamos nuevos objetos en el método de prueba usando el nuevo palabra clave, nuestras pruebas se vuelven más difíciles de leer porque:

  1. El lector debe conocer los diferentes estados del objeto creado. Por ejemplo, si pensamos en nuestro ejemplo, el lector debe saber que si creamos un nuevo RegistrationForm objeto y establezca los valores de propiedad del correo electrónico , nombre , apellido y signInProvider properties, significa que el objeto es un registro que se realiza mediante un proveedor de inicio de sesión social.
  2. Si el objeto creado tiene muchas propiedades, el código que lo crea ensucia el código fuente de nuestras pruebas. Debemos recordar que aunque necesitamos estos objetos en nuestras pruebas, debemos centrarnos en describir el comportamiento del método/función probado.

Aunque no es realista suponer que podemos eliminar por completo estos inconvenientes, debemos hacer todo lo posible para minimizar su efecto y hacer que nuestras pruebas sean lo más fáciles de leer posible.

Averigüemos cómo podemos hacer esto usando métodos de fábrica.

Uso de métodos de fábrica

Cuando creamos nuevos objetos utilizando métodos de fábrica, debemos nombrar los métodos de fábrica y sus parámetros de método de tal manera que haga que nuestro código sea más fácil de leer y escribir. Echemos un vistazo a dos métodos de fábrica diferentes y veamos qué tipo de efecto tienen en la legibilidad de nuestra prueba unitaria.

El nombre del primer método de fábrica es newRegistrationViaSocialSignIn() y no tiene parámetros de método. Después de haber agregado este método de fábrica a nuestra clase de prueba, la fuente de nuestra prueba unitaria tiene el siguiente aspecto (las partes relevantes están resaltadas):

import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.Mock;
import org.mockito.invocation.InvocationOnMock;
import org.mockito.runners.MockitoJUnitRunner;
import org.mockito.stubbing.Answer;
import org.springframework.security.crypto.password.PasswordEncoder;

import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNull;
import static org.mockito.Matchers.isA;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.verifyNoMoreInteractions;
import static org.mockito.Mockito.verifyZeroInteractions;
import static org.mockito.Mockito.when;


@RunWith(MockitoJUnitRunner.class)
public class RepositoryUserServiceTest {

	private static final String REGISTRATION_EMAIL_ADDRESS = "[email protected]";
	private static final String REGISTRATION_FIRST_NAME = "John";
	private static final String REGISTRATION_LAST_NAME = "Smith";
	private static final Role ROLE_REGISTERED_USER = Role.ROLE_USER;
	private static final SocialMediaService SOCIAL_SIGN_IN_PROVIDER = SocialMediaService.TWITTER;

	private RepositoryUserService registrationService;

	@Mock
	private PasswordEncoder passwordEncoder;

	@Mock
	private UserRepository repository;

	@Before
	public void setUp() {
		registrationService = new RepositoryUserService(passwordEncoder, repository);
	}


	@Test
	public void registerNewUserAccount_SocialSignInAndUniqueEmail_ShouldCreateNewUserAccountAndSetSignInProvider() throws DuplicateEmailException {
		RegistrationForm registration = newRegistrationViaSocialSignIn();

		when(repository.findByEmail(REGISTRATION_EMAIL_ADDRESS)).thenReturn(null);

		when(repository.save(isA(User.class))).thenAnswer(new Answer<User>() {
			@Override
			public User answer(InvocationOnMock invocation) throws Throwable {
				Object[] arguments = invocation.getArguments();
				return (User) arguments[0];
			}
		});

		User createdUserAccount = registrationService.registerNewUserAccount(registration);

		assertEquals(REGISTRATION_EMAIL_ADDRESS, createdUserAccount.getEmail());
		assertEquals(REGISTRATION_FIRST_NAME, createdUserAccount.getFirstName());
		assertEquals(REGISTRATION_LAST_NAME, createdUserAccount.getLastName());
		assertEquals(SOCIAL_SIGN_IN_PROVIDER, createdUserAccount.getSignInProvider());
		assertEquals(ROLE_REGISTERED_USER, createdUserAccount.getRole());
		assertNull(createdUserAccount.getPassword());

		verify(repository, times(1)).findByEmail(REGISTRATION_EMAIL_ADDRESS);
		verify(repository, times(1)).save(createdUserAccount);
		verifyNoMoreInteractions(repository);
		verifyZeroInteractions(passwordEncoder);
	}
	
	private RegistrationForm newRegistrationViaSocialSignIn() {
		RegistrationForm registration = new RegistrationForm();
	
		registration.setEmail(REGISTRATION_EMAIL_ADDRESS);
		registration.setFirstName(REGISTRATION_FIRST_NAME);
		registration.setLastName(REGISTRATION_LAST_NAME);
		registration.setSignInProvider(SOCIAL_SIGN_IN_PROVIDER);

		return registration;
	}
}

El primer método de fábrica tiene las siguientes consecuencias:

  • La parte de nuestro método de prueba, que crea el nuevo RegistrationForm objeto, es mucho más limpio que antes y el nombre del método de fábrica describe el estado del RegistrationForm creado objeto.
  • La configuración de nuestro objeto simulado es más difícil de leer porque el valor del correo electrónico La propiedad está "oculta" dentro de nuestro método de fábrica.
  • Nuestras afirmaciones son más difíciles de leer porque los valores de propiedad del RegistrationForm creado los objetos están "ocultos" dentro de nuestro método de fábrica.
Si usáramos el patrón objeto madre, el problema sería aún mayor porque tendríamos que mover las constantes relacionadas a la clase objeto madre.

Creo que es justo decir que aunque el primer método de fábrica tiene sus beneficios, también tiene serios inconvenientes.

Veamos si el segundo método de fábrica puede eliminar esos inconvenientes.

El nombre del segundo método de fábrica es newRegistrationViaSocialSignIn() y toma la dirección de correo electrónico, el nombre, el apellido y el proveedor de inicio de sesión social como parámetros del método. Después de haber agregado este método de fábrica a nuestra clase de prueba, la fuente de nuestra prueba unitaria tiene el siguiente aspecto (las partes relevantes están resaltadas):

import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.Mock;
import org.mockito.invocation.InvocationOnMock;
import org.mockito.runners.MockitoJUnitRunner;
import org.mockito.stubbing.Answer;
import org.springframework.security.crypto.password.PasswordEncoder;

import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNull;
import static org.mockito.Matchers.isA;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.verifyNoMoreInteractions;
import static org.mockito.Mockito.verifyZeroInteractions;
import static org.mockito.Mockito.when;


@RunWith(MockitoJUnitRunner.class)
public class RepositoryUserServiceTest {

	private static final String REGISTRATION_EMAIL_ADDRESS = "[email protected]";
	private static final String REGISTRATION_FIRST_NAME = "John";
	private static final String REGISTRATION_LAST_NAME = "Smith";
	private static final Role ROLE_REGISTERED_USER = Role.ROLE_USER;
	private static final SocialMediaService SOCIAL_SIGN_IN_PROVIDER = SocialMediaService.TWITTER;

	private RepositoryUserService registrationService;

	@Mock
	private PasswordEncoder passwordEncoder;

	@Mock
	private UserRepository repository;

	@Before
	public void setUp() {
		registrationService = new RepositoryUserService(passwordEncoder, repository);
	}


	@Test
	public void registerNewUserAccount_SocialSignInAndUniqueEmail_ShouldCreateNewUserAccountAndSetSignInProvider() throws DuplicateEmailException {
		RegistrationForm registration = newRegistrationViaSocialSignIn(REGISTRATION_EMAIL_ADDRESS,
																REGISTRATION_FIRST_NAME,
																REGISTRATION_LAST_NAME,
																SOCIAL_MEDIA_SERVICE
		);

		when(repository.findByEmail(REGISTRATION_EMAIL_ADDRESS)).thenReturn(null);

		when(repository.save(isA(User.class))).thenAnswer(new Answer<User>() {
			@Override
			public User answer(InvocationOnMock invocation) throws Throwable {
				Object[] arguments = invocation.getArguments();
				return (User) arguments[0];
			}
		});

		User createdUserAccount = registrationService.registerNewUserAccount(registration);

		assertEquals(REGISTRATION_EMAIL_ADDRESS, createdUserAccount.getEmail());
		assertEquals(REGISTRATION_FIRST_NAME, createdUserAccount.getFirstName());
		assertEquals(REGISTRATION_LAST_NAME, createdUserAccount.getLastName());
		assertEquals(SOCIAL_SIGN_IN_PROVIDER, createdUserAccount.getSignInProvider());
		assertEquals(ROLE_REGISTERED_USER, createdUserAccount.getRole());
		assertNull(createdUserAccount.getPassword());

		verify(repository, times(1)).findByEmail(REGISTRATION_EMAIL_ADDRESS);
		verify(repository, times(1)).save(createdUserAccount);
		verifyNoMoreInteractions(repository);
		verifyZeroInteractions(passwordEncoder);
	}
	
	private RegistrationForm newRegistrationViaSocialSignIn(String emailAddress, String firstName, String lastName, SocialMediaService signInProvider) {
		RegistrationForm registration = new RegistrationForm();
	
		registration.setEmail(emailAddress);
		registration.setFirstName(firstName);
		registration.setLastName(lastName);
		registration.setSignInProvider(signInProvider);

		return registration;
	}
}

El segundo método de fábrica tiene las siguientes consecuencias:

  • La parte de nuestro método de prueba, que crea el nuevo RegistrationForm objeto, es un poco más complicado que el mismo código que usa el primer método de fábrica. Sin embargo, sigue siendo más limpio que el código original porque el nombre del método de fábrica describe el estado del objeto creado.
  • Parece eliminar los inconvenientes del primer método de fábrica porque los valores de propiedad del objeto creado no están "ocultos" dentro del método de fábrica.

Parece genial, ¿verdad?

Sería muy fácil pensar que todo está bien en el paraíso, pero no es así. Aunque hemos visto que los métodos de fábrica pueden hacer que nuestras pruebas sean más legibles, la cuestión es que son una buena opción solo cuando se cumplen las siguientes condiciones:

  1. El método de fábrica no tiene demasiados parámetros de método. Cuando crece el número de parámetros de método, nuestras pruebas se vuelven más difíciles de escribir y leer. La pregunta obvia es:¿cuántos parámetros de método puede tener un método de fábrica? Desafortunadamente, es difícil dar una respuesta exacta a esa pregunta, pero creo que usar un método de fábrica es una buena opción si el método de fábrica solo tiene un puñado de parámetros de método.
  2. Los datos de prueba no tienen demasiada variación. El problema de usar métodos de fábrica es que un único método de fábrica suele ser adecuado para un caso de uso. Si necesitamos soportar N casos de uso, necesitamos tener N métodos de fábrica. Esto es un problema porque con el tiempo nuestros métodos de fábrica se vuelven inflados, desordenados y difíciles de mantener (especialmente si usamos el patrón principal del objeto).

Averigüemos si los creadores de datos de prueba pueden resolver algunos de estos problemas.

Uso de generadores de datos de prueba

Un generador de datos de prueba es una clase que crea nuevos objetos utilizando el patrón del generador. El patrón de construcción descrito en Java efectivo tiene muchos beneficios, pero nuestra principal motivación es proporcionar una API fluida para crear los objetos que se usan en nuestras pruebas.

Podemos crear una clase de generador de datos de prueba que crea un nuevo RegistrationForm objetos siguiendo estos pasos:

  1. Cree un Creador de formularios de registro clase.
  2. Agregar un Formulario de registro campo a la clase creada. Este campo contiene una referencia al objeto creado.
  3. Agregue un constructor predeterminado a la clase creada e impleméntelo creando un nuevo RegistrationForm objeto.
  4. Agregue métodos que se utilizan para establecer los valores de propiedad del RegistrationForm creado objeto. Cada método establece el valor de la propiedad llamando al método de establecimiento correcto y devuelve una referencia al RegistrationFormBuilder objeto. Recuerde que los nombres de los métodos de estos métodos pueden hacer o deshacer nuestro DSL .
  5. Agregue un método build() a la clase creada e impleméntelo devolviendo el RegistrationForm creado objeto.

El código fuente de nuestra clase de generador de datos de prueba tiene el siguiente aspecto:

public class RegistrationFormBuilder {

    private RegistrationForm registration;

    public RegistrationFormBuilder() {
        registration = new RegistrationForm();
    }

    public RegistrationFormBuilder email(String email) {
        registration.setEmail(email);
        return this;
    }

    public RegistrationFormBuilder firstName(String firstName) {
        registration.setFirstName(firstName);
        return this;
    }

    public RegistrationFormBuilder lastName(String lastName) {
        registration.setLastName(lastName);
        return this;
    }

    public RegistrationFormBuilder isSocialSignInViaSignInProvider(SocialMediaService signInProvider) {
        registration.setSignInProvider(signInProvider);
        return this;
    }

    public RegistrationForm build() {
        return registration;
    }
}

Después de que hayamos modificado nuestra prueba de unidad para usar la nueva clase de generador de datos de prueba, su código fuente se ve de la siguiente manera (la parte relevante está resaltada):

import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.Mock;
import org.mockito.invocation.InvocationOnMock;
import org.mockito.runners.MockitoJUnitRunner;
import org.mockito.stubbing.Answer;
import org.springframework.security.crypto.password.PasswordEncoder;

import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNull;
import static org.mockito.Matchers.isA;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.verifyNoMoreInteractions;
import static org.mockito.Mockito.verifyZeroInteractions;
import static org.mockito.Mockito.when;

@RunWith(MockitoJUnitRunner.class)
public class RepositoryUserServiceTest {

	private static final String REGISTRATION_EMAIL_ADDRESS = "[email protected]";
	private static final String REGISTRATION_FIRST_NAME = "John";
	private static final String REGISTRATION_LAST_NAME = "Smith";
	private static final Role ROLE_REGISTERED_USER = Role.ROLE_USER;
	private static final SocialMediaService SOCIAL_SIGN_IN_PROVIDER = SocialMediaService.TWITTER;

	private RepositoryUserService registrationService;

	@Mock
	private PasswordEncoder passwordEncoder;

	@Mock
	private UserRepository repository;

	@Before
	public void setUp() {
		registrationService = new RepositoryUserService(passwordEncoder, repository);
	}


	@Test
	public void registerNewUserAccount_SocialSignInAndUniqueEmail_ShouldCreateNewUserAccountAndSetSignInProvider() throws DuplicateEmailException {
		RegistrationForm registration = new RegistrationFormBuilder()
			.email(REGISTRATION_EMAIL_ADDRESS)
			.firstName(REGISTRATION_FIRST_NAME)
			.lastName(REGISTRATION_LAST_NAME)
			.isSocialSignInViaSignInProvider(SOCIAL_SIGN_IN_PROVIDER)
			.build();

		when(repository.findByEmail(REGISTRATION_EMAIL_ADDRESS)).thenReturn(null);

		when(repository.save(isA(User.class))).thenAnswer(new Answer<User>() {
			@Override
			public User answer(InvocationOnMock invocation) throws Throwable {
				Object[] arguments = invocation.getArguments();
				return (User) arguments[0];
			}
		});

		User createdUserAccount = registrationService.registerNewUserAccount(registration);

		assertEquals(REGISTRATION_EMAIL_ADDRESS, createdUserAccount.getEmail());
		assertEquals(REGISTRATION_FIRST_NAME, createdUserAccount.getFirstName());
		assertEquals(REGISTRATION_LAST_NAME, createdUserAccount.getLastName());
		assertEquals(SOCIAL_SIGN_IN_PROVIDER, createdUserAccount.getSignInProvider());
		assertEquals(ROLE_REGISTERED_USER, createdUserAccount.getRole());
		assertNull(createdUserAccount.getPassword());

		verify(repository, times(1)).findByEmail(REGISTRATION_EMAIL_ADDRESS);
		verify(repository, times(1)).save(createdUserAccount);
		verifyNoMoreInteractions(repository);
		verifyZeroInteractions(passwordEncoder);
	}
}

Como podemos ver, los generadores de datos de prueba tienen los siguientes beneficios:

  • El código que crea nuevos objetos RegistrationForm es fácil de leer y escribir. Soy un gran admirador de las API fluidas y creo que este código es hermoso y elegante.
  • El patrón del generador asegura que la variación encontrada de nuestros datos de prueba ya no sea un problema porque simplemente podemos agregar nuevos métodos a la clase del generador de datos de prueba.
  • La configuración de nuestro objeto simulado y nuestras afirmaciones son fáciles de leer porque las constantes son visibles en nuestro método de prueba y nuestro DSL enfatiza el significado de cada valor de propiedad.

Entonces, ¿deberíamos usar el patrón de construcción para todo?

¡NO!

Deberíamos usar generadores de datos de prueba solo cuando tenga sentido. En otras palabras, deberíamos usarlos cuando

  1. Hemos establecido más de un puñado de valores de propiedad.
  2. Nuestros datos de prueba tienen mucha variación.

El patrón constructor es una elección perfecta si se cumple una de estas condiciones. La razón de esto es que podemos crear un lenguaje específico de dominio nombrando los métodos tipo setter de la clase constructora. Esto hace que nuestras pruebas sean fáciles de leer y escribir incluso si hubiéramos creado muchos objetos diferentes y establecido muchos valores de propiedad.

Ese es el poder del patrón constructor.

Eso es todo por hoy. Avancemos y resumamos lo que aprendimos de esta publicación de blog.

Resumen

Aprendimos por qué es una mala idea crear objetos en el método de prueba usando el nuevo palabra clave, y aprendimos dos formas diferentes de crear los objetos que se utilizan en nuestras pruebas.

Para ser más específicos, esta publicación de blog nos ha enseñado tres cosas:

  • Es una mala idea crear los objetos requeridos en el método de prueba usando el nuevo palabra clave porque hace que nuestras pruebas sean complicadas y difíciles de leer.
  • Si tenemos que establecer solo un puñado de valores de propiedad y nuestros datos de prueba no tienen mucha variación, debemos crear el objeto requerido usando un método de fábrica.
  • Si tenemos que establecer muchos valores de propiedad y/o nuestros datos de prueba tienen mucha variación, debemos crear el objeto requerido usando un generador de datos de prueba.

Etiqueta Java