Java >> Java tutorial >  >> Tag >> new

At skrive rene tests - Nyt betragtet som skadeligt

Oprettelse af nye objekter er en væsentlig del af automatiseret test, og den mest oplagte måde at gøre det på er at bruge den nye søgeord.

Men dette er ikke den bedste måde at skabe nye objekter på i vores testcases , og ved at bruge den nye søgeord vil gøre vores test sværere at læse og vedligeholde.

Dette blogindlæg identificerer de problemer, der er forårsaget af det nye søgeord, og beskriver, hvordan vi kan løse disse problemer ved at bruge fabriksmetoder og bygherremønsteret.

New Is Not the New Black

I løbet af denne øvelse har vi refaktoreret en enhedstest, som sikrer, at registerNewUserAccount(RegistrationForm userAccountData) metoden for RepositoryUserService klasse fungerer som forventet, når en ny brugerkonto oprettes ved at bruge en unik e-mailadresse og en social log-in-udbyder.

Registreringsformularen klasse er et dataoverførselsobjekt (DTO), og vores enhedstest sætter dens egenskabsværdier ved hjælp af setter-metoder. Kildekoden til vores enhedstest ser ud som følger (den relevante kode er fremhævet):

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

Så hvad er problemet? Den fremhævede del af vores enhedstest er kort, og den er forholdsvis let at læse. Efter min mening er det største problem ved denne kode, at den er datacentreret. Det opretter en ny RegistrationForm objekt og angiver egenskabsværdierne for det oprettede objekt, men det beskriver ikke betydningen af ​​disse egenskabsværdier.

Hvis vi opretter nye objekter i testmetoden ved at bruge ny søgeord, bliver vores test sværere at læse, fordi:

  1. Læseren skal kende de forskellige tilstande for det oprettede objekt. Hvis vi f.eks. tænker på vores eksempel, skal læseren vide, at hvis vi opretter en ny RegistrationForm objekt og indstil egenskabsværdierne for e-mail , fornavn , efternavn og signInProvider egenskaber, betyder det, at objektet er en registrering, som er foretaget ved at bruge en social log in-udbyder.
  2. Hvis det oprettede objekt har mange egenskaber, fylder koden, der skaber det, kildekoden til vores tests. Vi skal huske, at selvom vi har brug for disse objekter i vores test, bør vi fokusere på at beskrive adfærden af ​​den testede metode/funktion.

Selvom det ikke er realistisk at antage, at vi fuldstændigt kan eliminere disse ulemper, bør vi gøre vores bedste for at minimere deres effekt og gøre vores test så lette at læse som muligt.

Lad os finde ud af, hvordan vi kan gøre dette ved at bruge fabriksmetoder.

Ved brug af fabriksmetoder

Når vi opretter nye objekter ved at bruge fabriksmetoder, bør vi navngive fabriksmetoderne og deres metodeparametre på en sådan måde, at det gør vores kode nemmere at læse og skrive. Lad os tage et kig på to forskellige fabriksmetoder og se, hvilken effekt de har på læsbarheden af ​​vores enhedstest.

Navnet på den første fabriksmetode er newRegistrationViaSocialSignIn() , og den har ingen metodeparametre. Efter at vi har tilføjet denne fabriksmetode til vores testklasse, ser kilden til vores enhedstest ud som følger (de relevante dele er fremhævet):

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

Den første fabriksmetode har følgende konsekvenser:

  • Den del af vores testmetode, som skaber den nye RegistrationForm objekt, er meget renere end før, og navnet på fabriksmetoden beskriver tilstanden af ​​den oprettede RegistrationForm objekt.
  • Konfigurationen af ​​vores mock-objekt er sværere at læse, fordi værdien af ​​e-mail ejendom er "skjult" inde i vores fabriksmetode.
  • Vores påstande er sværere at læse, fordi egenskabsværdierne for den oprettede RegistrationForm objekt er "gemt" inde i vores fabriksmetode.
Hvis vi ville bruge objektets modermønster, ville problemet være endnu større, fordi vi skulle flytte de relaterede konstanter til objektets moderklasse.

Jeg synes, det er rimeligt at sige, at selvom den første fabriksmetode har sine fordele, har den også alvorlige ulemper.

Lad os se, om den anden fabriksmetode kan eliminere disse ulemper.

Navnet på den anden fabriksmetode er newRegistrationViaSocialSignIn() , og det tager e-mailadresse, fornavn, efternavn og social log-in-udbyder som metodeparametre. Efter at vi har tilføjet denne fabriksmetode til vores testklasse, ser kilden til vores enhedstest ud som følger (de relevante dele er fremhævet):

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

Den anden fabriksmetode har følgende konsekvenser:

  • Den del af vores testmetode, som skaber den nye RegistrationForm objekt, er en smule mere rodet end den samme kode, som bruger den første fabriksmetode. Den er dog stadig renere end den originale kode, fordi navnet på fabriksmetoden beskriver tilstanden for det oprettede objekt.
  • Det ser ud til at eliminere ulemperne ved den første fabriksmetode, fordi egenskabsværdierne for det oprettede objekt ikke er "gemt" inde i fabriksmetoden.

Det virker fedt, ikke?

Det ville være rigtig nemt at tro, at alt er godt i paradiset, men det er ikke tilfældet. Selvom vi har set, at fabriksmetoder kan gøre vores tests mere læsbare, er sagen, at de kun er et godt valg, når følgende betingelser er opfyldt:

  1. Fabriksmetoden har ikke for mange metodeparametre. Når antallet af metodeparametre vokser, bliver vores test sværere at skrive og læse. Det åbenlyse spørgsmål er:hvor mange metodeparametre kan en fabriksmetode have? Desværre er det svært at give et præcist svar på det spørgsmål, men jeg tror, ​​at brug af en fabriksmetode er et godt valg, hvis fabriksmetoden kun har en håndfuld metodeparametre.
  2. Testdataene har ikke for stor variation. Problemet med at bruge fabriksmetoder er, at en enkelt fabriksmetode typisk er velegnet til én use case. Hvis vi skal understøtte N use cases, skal vi have N fabriksmetoder. Dette er et problem, fordi vores fabriksmetoder med tiden bliver oppustede, rodede og svære at vedligeholde (især hvis vi bruger objektets modermønster).

Lad os finde ud af, om testdatabyggere kan løse nogle af disse problemer.

Brug af Test Data Builders

En testdatabuilder er en klasse, som opretter nye objekter ved at bruge buildermønsteret. Builder-mønsteret beskrevet i Effektiv Java har mange fordele, men vores primære motivation er at levere en flydende API til at skabe de objekter, der bruges i vores test.

Vi kan oprette en testdatabuilderklasse, som opretter ny RegistrationForm objekter ved at følge disse trin:

  1. Opret en RegistrationFormBuilder klasse.
  2. Tilføj en Registreringsformular felt til den oprettede klasse. Dette felt indeholder en reference til det oprettede objekt.
  3. Tilføj en standardkonstruktør til den oprettede klasse og implementer den ved at oprette en ny Registreringsform objekt.
  4. Tilføj metoder, der bruges til at indstille egenskabsværdierne for den oprettede RegistrationForm objekt. Hver metode indstiller egenskabsværdien ved at kalde den korrekte setter-metode og returnerer en reference til RegistrationFormBuilder objekt. Husk, at metodenavnene på disse metoder enten kan lave eller ødelægge vores DSL .
  5. Tilføj en build()-metode til den oprettede klasse og implementer den ved at returnere den oprettede RegistrationForm objekt.

Kildekoden for vores testdatabuilderklasse ser ud som følger:

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

Efter at vi har ændret vores enhedstest til at bruge den nye testdatabuilderklasse, ser dens kildekode ud som følger (den relevante del er fremhævet):

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

Som vi kan se, har testdatabyggere følgende fordele:

  • Koden, der opretter nye RegistrationForm-objekter, er både nem at læse og skrive. Jeg er en stor fan af flydende API'er, og jeg synes, at denne kode er både smuk og elegant.
  • Buildermønsteret sikrer, at variationen fundet fra vores testdata ikke længere er et problem, fordi vi blot kan tilføje nye metoder til testdatabuilderklassen.
  • Konfigurationen af ​​vores falske objekt og vores påstande er nemme at læse, fordi konstanterne er synlige i vores testmetode, og vores DSL understreger betydningen af ​​hver egenskabsværdi.

Så skal vi bruge builder-mønsteret til alt?

NEJ!

Vi bør kun bruge testdatabyggere, når det giver mening. Med andre ord bør vi bruge dem, når

  1. Vi har fastsat mere end en håndfuld ejendomsværdier.
  2. Vores testdata har mange variationer.

Builder-mønsteret er et perfekt valg, hvis en af ​​disse forhold er sand. Grunden til dette er, at vi kan skabe et domænespecifikt sprog ved at navngive de setter-lignende metoder i builder-klassen. Dette gør vores test nemme at læse og skrive, selvom vi ville have oprettet en masse forskellige objekter og sat en masse egenskabsværdier.

Det er kraften i bygherren patten.

Det var alt for i dag. Lad os gå videre og opsummere, hvad vi lærte af dette blogindlæg.

Oversigt

Vi lærte, hvorfor det er en dårlig idé at oprette objekter i testmetoden ved at bruge den nye nøgleord, og vi lærte to forskellige måder at skabe de objekter, som bruges i vores test.

For at være mere specifik har dette blogindlæg lært os tre ting:

  • Det er en dårlig idé at oprette de nødvendige objekter i testmetoden ved at bruge den nye søgeord, fordi det gør vores test rodet og svære at læse.
  • Hvis vi kun skal indstille en håndfuld egenskabsværdier, og vores testdata ikke har meget variation, bør vi oprette det påkrævede objekt ved at bruge en fabriksmetode.
  • Hvis vi skal indstille en masse egenskabsværdier og/eller vores testdata har mange variationer, bør vi oprette det påkrævede objekt ved at bruge en testdatabuilder.

Java tag