Java >> Java tutorial >  >> Tag >> assert

Skrivning af rene tests - Erstat påstande med et domænespecifikt sprog

Automatiserede test er værdiløse, hvis de ikke hævder noget, men problemet med almindelige JUnit-påstande er, at de taler det forkerte sprog og bliver rodede, hvis vi skal skrive mange af dem.

Hvis vi vil skrive test, som både er nemme at forstå og vedligeholde, må vi finde ud af en bedre måde at skrive påstande på .

Dette blogindlæg identificerer problemerne med "standard" JUnit-påstandene og beskriver, hvordan vi løser disse problemer ved at erstatte disse påstande med et domænespecifikt sprog.

Data er ikke så vigtigt

I mit tidligere blogindlæg identificerede jeg to problemer forårsaget af datacentrerede tests. Selvom det blogindlæg talte om oprettelsen af ​​nye objekter, er disse problemer også gyldige for påstande.

Lad os genopfriske vores hukommelse og tage et kig på kildekoden til vores 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.

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 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, sikrer påstandene fundet fra vores enhedstest, at egenskabsværdierne for den returnerede Bruger objektet er korrekt. Vores påstande sikrer, at:

  • Værdien af ​​e-mailen egenskaben er korrekt.
  • Værdien af ​​fornavn egenskaben er korrekt.
  • Værdien af ​​efternavn egenskaben er korrekt.
  • Værdien af ​​signInProvider er korrekt.
  • Værdien af ​​rollen egenskaben er korrekt.
  • adgangskoden er nul.

Dette er selvfølgelig ret indlysende, men det er vigtigt at gentage disse påstande på denne måde, fordi det hjælper os med at identificere problemet med vores påstande. Vores påstande er datacentrerede og det betyder, at:

  • Læseren skal kende de forskellige tilstande for det returnerede objekt . For eksempel, hvis vi tænker på vores eksempel, skal læseren vide, at hvis e-mail , fornavn , efternavn og signInProvider egenskaber for returnerede RegistrationForm objekt har ikke-null værdier og værdien af ​​adgangskoden egenskaben er null, det betyder, at objektet er en registrering, som er foretaget ved hjælp af en social log-in-udbyder.
  • Hvis det oprettede objekt har mange egenskaber, fylder vores påstande kildekoden til vores tests. Vi skal huske, at selvom vi ønsker at sikre, at dataene for det returnerede objekt er korrekte, er det meget vigtigere, at vi beskriver tilstanden for det returnerede objekt .

Lad os se, hvordan vi kan forbedre vores påstande.

Forvandling af påstande til et domænespecifikt sprog

Du har måske bemærket, at udviklerne og domæneeksperterne ofte bruger forskellige udtryk for de samme ting. Med andre ord taler udviklere ikke det samme sprog som domæneeksperterne. Dette forårsager unødig forvirring og friktion mellem udviklerne og domæneeksperterne .

Domænedrevet design (DDD) giver én løsning på dette problem. Eric Evans introducerede udtrykket allestedsnærværende sprog i sin bog med titlen Domain-Driven Design.

Wikipedia specificerer allestedsnærværende sprog som følger:

Allestedsnærværende sprog er et sprog, der er struktureret omkring domænemodellen og bruges af alle teammedlemmer til at forbinde alle teamets aktiviteter med softwaren.

Hvis vi vil skrive påstande, der taler det "korrekte" sprog, er vi nødt til at bygge bro mellem udviklerne og domæneeksperterne. Med andre ord er vi nødt til at skabe et domænespecifikt sprog til at skrive påstande.

Implementering af vores domænespecifikke sprog

Før vi kan implementere vores domænespecifikke sprog, skal vi designe det. Når vi designer et domænespecifikt sprog til vores påstande, skal vi følge disse regler:

  1. Vi er nødt til at opgive den datacentrerede tilgang og tænke mere på den rigtige bruger, hvis information er fundet fra en Bruger objekt.
  2. Vi skal bruge det sprog, der tales af domæneeksperterne.

Hvis vi følger disse to regler, kan vi oprette følgende regler for vores domænespecifikke sprog:

  • En bruger har et fornavn, efternavn og e-mailadresse.
  • En bruger er en registreret bruger.
  • En bruger er registreret ved at bruge en social sign-udbyder, hvilket betyder, at denne bruger ikke har en adgangskode.

Nu hvor vi har specificeret reglerne for vores domænespecifikke sprog, er vi klar til at implementere det. Vi vil gøre dette ved at skabe en tilpasset AssertJ-påstand, som implementerer reglerne for vores domænespecifikke sprog.

Kildekoden for vores tilpassede påstandsklasse ser ud som følger:

import org.assertj.core.api.AbstractAssert;
import org.assertj.core.api.Assertions;

public class UserAssert extends AbstractAssert<UserAssert, User> {

    private UserAssert(User actual) {
        super(actual, UserAssert.class);
    }

    public static UserAssert assertThatUser(User actual) {
        return new UserAssert(actual);
    }

    public UserAssert hasEmail(String email) {
        isNotNull();

        Assertions.assertThat(actual.getEmail())
                .overridingErrorMessage( "Expected email to be <%s> but was <%s>",
                        email,
                        actual.getEmail()
                )
                .isEqualTo(email);

        return this;
    }

    public UserAssert hasFirstName(String firstName) {
        isNotNull();

        Assertions.assertThat(actual.getFirstName())
                .overridingErrorMessage("Expected first name to be <%s> but was <%s>",
                        firstName,
                        actual.getFirstName()
                )
                .isEqualTo(firstName);

        return this;
    }

    public UserAssert hasLastName(String lastName) {
        isNotNull();

        Assertions.assertThat(actual.getLastName())
                .overridingErrorMessage( "Expected last name to be <%s> but was <%s>",
                        lastName,
                        actual.getLastName()
                )
                .isEqualTo(lastName);

        return this;
    }

    public UserAssert isRegisteredByUsingSignInProvider(SocialMediaService signInProvider) {
        isNotNull();

        Assertions.assertThat(actual.getSignInProvider())
                .overridingErrorMessage( "Expected signInProvider to be <%s> but was <%s>",
                        signInProvider,
                        actual.getSignInProvider()
                )
                .isEqualTo(signInProvider);

        hasNoPassword();

        return this;
    }

    private void hasNoPassword() {
        isNotNull();

        Assertions.assertThat(actual.getPassword())
                .overridingErrorMessage("Expected password to be <null> but was <%s>",
                        actual.getPassword()
                )
                .isNull();
    }

    public UserAssert isRegisteredUser() {
        isNotNull();

        Assertions.assertThat(actual.getRole())
                .overridingErrorMessage( "Expected role to be <ROLE_USER> but was <%s>",
                        actual.getRole()
                )
                .isEqualTo(Role.ROLE_USER);

        return this;
    }
}

Vi har nu oprettet et domænespecifikt sprog til at skrive påstande til Bruger genstande. Vores næste skridt er at ændre vores enhedstest til at bruge vores nye domænespecifikke sprog.

Erstatning af JUnit-påstande med et domænespecifikt sprog

Efter at vi har omskrevet vores påstande til at bruge vores domænespecifikke sprog, ser kildekoden for vores enhedstest 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);

		assertThatUser(createdUserAccount)
			.hasEmail(REGISTRATION_EMAIL_ADDRESS)
			.hasFirstName(REGISTRATION_FIRST_NAME)
			.hasLastName(REGISTRATION_LAST_NAME)
			.isRegisteredUser()
			.isRegisteredByUsingSignInProvider(SOCIAL_SIGN_IN_PROVIDER);

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

Vores løsning har følgende fordele:

  • Vores påstande bruger det sprog, som domæneeksperterne forstår. Det betyder, at vores test er en eksekverbar specifikation, som er let at forstå og altid opdateret.
  • Vi behøver ikke at spilde tid på at finde ud af, hvorfor en test mislykkedes. Vores tilpassede fejlmeddelelser sikrer, at vi ved, hvorfor det mislykkedes.
  • Hvis API'et for Brugeren klasseændringer, behøver vi ikke rette enhver testmetode, der skriver påstande til Bruger genstande. Den eneste klasse, som vi skal ændre, er UserAssert klasse. Med andre ord, at flytte den faktiske påstandslogik væk fra vores testmetode gjorde vores test mindre skør og lettere at vedligeholde.

Lad os bruge et øjeblik på at opsummere, hvad vi lærte af dette blogindlæg.

Oversigt

Vi har nu transformeret vores påstande til et domænespecifikt sprog. Dette blogindlæg lærte os tre ting:

  • At følge den datacentrerede tilgang forårsager unødig forvirring og friktion mellem udviklerne og domæneeksperterne.
  • Oprettelse af et domænespecifikt sprog til vores påstande gør vores tests mindre skøre, fordi den faktiske påstandslogik flyttes til tilpassede påstandsklasser.
  • Hvis vi skriver påstande ved at bruge et domænespecifikt sprog, omdanner vi vores test til eksekverbare specifikationer, som er nemme at forstå og taler domæneeksperternes sprog.

Java tag