Статьи

Написание чистых тестов — разделяй и властвуй

Хороший юнит-тест должен провалиться только по одной причине. Это означает, что правильный модульный тест проверяет только одну логическую концепцию.

Если мы хотим написать чистые тесты, мы должны идентифицировать эти логические концепции и написать только один контрольный пример для каждой логической концепции.

В этом блоге описывается, как мы можем определить логические концепции, найденные в наших тестах, и разделить существующий модульный тест на несколько модульных тестов.

Довольно чисто не достаточно хорошо

Давайте начнем с рассмотрения исходного кода нашего модульного теста, который гарантирует, что метод registerNewUserAccount (RegistrationForm userAccountData) класса RepositoryUserService работает, как и ожидалось, когда создается новая учетная запись пользователя с использованием уникального адреса электронной почты и поставщика социальных сетей в поставщике. ,

Исходный код этого модульного теста выглядит следующим образом:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
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 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);
 
        assertThat(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);
    }
}

Этот юнит тест довольно чистый. В конце концов, наш тестовый класс, тестовый метод и локальные переменные, созданные внутри тестового метода, имеют описательные имена. Мы также заменили магические числа константами и создали предметно-ориентированные языки для создания новых объектов и написания утверждений.

И все же, мы можем сделать этот тест еще лучше .

Проблема этого модульного теста состоит в том, что он может потерпеть неудачу по нескольким причинам. Это может дать сбой, если:

  1. Наш метод обслуживания не проверяет, что адрес электронной почты, введенный в регистрационную форму, не найден в нашей базе данных.
  2. Информация сохраненного объекта User не совпадает с информацией, введенной в регистрационную форму.
  3. Информация о возвращенном объекте User неверна.
  4. Наш сервисный метод создает пароль для пользователя с помощью объекта PasswordEncoder .

Другими словами, этот модульный тест тестирует четыре различных логических понятия, и это вызывает следующие проблемы:

  • Если этот тест не пройден, мы не обязательно знаем, почему он не прошел. Это означает, что мы должны прочитать исходный код модульного теста.
  • Модульный тест немного длинный, что затрудняет его чтение.
  • Трудно описать ожидаемое поведение. Это означает, что очень трудно придумать отличные имена для наших методов тестирования.

Мы можем определить логические концепции, охватываемые одним модульным тестом, путем определения ситуаций, когда этот модульный тест не пройден.

Вот почему нам нужно разделить этот тест на четыре модульных теста.

Один тест, одна точка отказа

Наш следующий шаг — разделить наш модульный тест на четыре новых модульных теста и убедиться, что каждый из них тестирует единую логическую концепцию. Мы можем сделать это, написав следующие юнит-тесты:

  1. Мы должны убедиться, что наш метод обслуживания проверяет, является ли адрес электронной почты, указанный пользователем, уникальным.
  2. Нам нужно убедиться, что информация о постоянном объекте User верна.
  3. Нам нужно убедиться, что информация о возвращенном объекте User верна.
  4. Нам нужно убедиться, что наш метод обслуживания не создает зашифрованный пароль для пользователя, который использует провайдер социальной сети.

После того, как мы написали эти модульные тесты, исходный код нашего тестового класса выглядит следующим образом:

001
002
003
004
005
006
007
008
009
010
011
012
013
014
015
016
017
018
019
020
021
022
023
024
025
026
027
028
029
030
031
032
033
034
035
036
037
038
039
040
041
042
043
044
045
046
047
048
049
050
051
052
053
054
055
056
057
058
059
060
061
062
063
064
065
066
067
068
069
070
071
072
073
074
075
076
077
078
079
080
081
082
083
084
085
086
087
088
089
090
091
092
093
094
095
096
097
098
099
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.ArgumentCaptor;
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 net.petrikainulainen.spring.social.signinmvc.user.model.UserAssert.assertThat;
import static org.mockito.Matchers.isA;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
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 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_ShouldCheckThatEmailIsUnique() 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);
 
        registrationService.registerNewUserAccount(registration);
 
        verify(repository, times(1)).findByEmail(REGISTRATION_EMAIL_ADDRESS);
    }
 
    @Test
    public void registerNewUserAccount_SocialSignInAndUniqueEmail_ShouldSaveNewUserAccountAndSetSignInProvider() 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);
 
        registrationService.registerNewUserAccount(registration);
 
        ArgumentCaptor<User> userAccountArgument = ArgumentCaptor.forClass(User.class);
        verify(repository, times(1)).save(userAccountArgument.capture());
 
        User createdUserAccount = userAccountArgument.getValue();
 
        assertThat(createdUserAccount)
                .hasEmail(REGISTRATION_EMAIL_ADDRESS)
                .hasFirstName(REGISTRATION_FIRST_NAME)
                .hasLastName(REGISTRATION_LAST_NAME)
                .isRegisteredUser()
                .isRegisteredByUsingSignInProvider(SOCIAL_SIGN_IN_PROVIDER);
    }
 
 
    @Test
    public void registerNewUserAccount_SocialSignInAndUniqueEmail_ShouldReturnCreatedUserAccount() 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);
 
        assertThat(createdUserAccount)
                .hasEmail(REGISTRATION_EMAIL_ADDRESS)
                .hasFirstName(REGISTRATION_FIRST_NAME)
                .hasLastName(REGISTRATION_LAST_NAME)
                .isRegisteredUser()
                .isRegisteredByUsingSignInProvider(SOCIAL_SIGN_IN_PROVIDER);
    }
 
    @Test
    public void registerNewUserAccount_SocialSignInAnUniqueEmail_ShouldNotCreateEncodedPasswordForUser() 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);
 
        registrationService.registerNewUserAccount(registration);
 
        verifyZeroInteractions(passwordEncoder);
    }
}

Очевидное преимущество написания модульных тестов, которые проверяют только одну логическую концепцию, состоит в том, что легко понять, почему тест не прошел. Однако у этого подхода есть и два других преимущества:

  • Легко указать ожидаемое поведение. Это означает, что легче определить хорошие имена для наших методов тестирования.
  • Поскольку эти модульные тесты значительно короче, чем исходные модульные тесты, легче определить требования к тестируемому методу / компоненту. Это помогает нам преобразовывать наши тесты в исполняемые спецификации.

Давайте продолжим и подведем итог тому, что мы узнали из этого блога.

Резюме

Теперь мы успешно разбили наш модульный тест на четыре меньших модульных теста, которые тестируют единую логическую концепцию. Этот пост научил нас двум вещам:

  • Мы узнали, что можем идентифицировать логические концепции, охватываемые одним модульным тестом, путем определения ситуаций, когда этот тест не пройден.
  • Мы узнали, что написание модульных тестов, которые проверяют только одну логическую концепцию, помогает нам преобразовывать наши тестовые примеры в исполняемые спецификации, которые идентифицируют требования тестируемого метода / компонента.