Статьи

Написание чистых тестов — проверять или не проверять

Когда мы пишем модульные тесты, которые используют фиктивные объекты , мы выполняем следующие шаги:

  1. Настроить поведение наших фиктивных объектов.
  2. Вызвать проверенный метод.
  3. Убедитесь, что правильные методы наших фиктивных объектов были вызваны.

Описание третьего шага на самом деле немного вводит в заблуждение, потому что часто мы заканчиваем проверкой того, что были вызваны правильные методы И что другие методы наших фиктивных объектов не были вызваны.

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

Правильно?

Давайте проверим все

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

Требования этого метода обслуживания:

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

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

Этот метод обслуживания реализуется с помощью следующих шагов:

  1. Сервисный метод проверяет, что адрес электронной почты, указанный пользователем, не найден в базе данных. Это делается путем вызова метода findByEmail () интерфейса UserRepository .
  2. Если объект User найден, метод метода службы генерирует исключение DuplicateEmailException .
  3. Создает новый объект User . Если регистрация выполняется с помощью обычного входа (свойство signInProvider класса RegistrationForm не установлено), метод службы кодирует пароль, предоставленный пользователем, и устанавливает закодированный пароль для созданного объекта User .
  4. Методы службы сохраняют информацию о созданном объекте пользователя в базе данных и возвращают сохраненный объект пользователя .

Исходный код класса 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
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
 
@Service
public class RepositoryUserService implements UserService {
 
    private PasswordEncoder passwordEncoder;
 
    private UserRepository repository;
 
    @Autowired
    public RepositoryUserService(PasswordEncoder passwordEncoder, UserRepository repository) {
        this.passwordEncoder = passwordEncoder;
        this.repository = repository;
    }
 
    @Transactional
    @Override
    public User registerNewUserAccount(RegistrationForm userAccountData) throws DuplicateEmailException {
        if (emailExist(userAccountData.getEmail())) {
            throw new DuplicateEmailException("The email address: " + userAccountData.getEmail() + " is already in use.");
        }
 
        String encodedPassword = encodePassword(userAccountData);
 
        User registered = User.getBuilder()
                .email(userAccountData.getEmail())
                .firstName(userAccountData.getFirstName())
                .lastName(userAccountData.getLastName())
                .password(encodedPassword)
                .signInProvider(userAccountData.getSignInProvider())
                .build();
 
        return repository.save(registered);
    }
 
    private boolean emailExist(String email) {
        User user = repository.findByEmail(email);
 
        if (user != null) {
            return true;
        }
 
        return false;
    }
 
    private String encodePassword(RegistrationForm dto) {
        String encodedPassword = null;
 
        if (dto.isNormalRegistration()) {
            encodedPassword = passwordEncoder.encode(dto.getPassword());
        }
 
        return encodedPassword;
    }
}

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

Мы должны обеспечить, чтобы:

  • Методы службы проверяют, является ли адрес электронной почты уникальным, если указан повторяющийся адрес электронной почты.
  • DuplicateEmailException генерируется, когда дается повторяющийся адрес электронной почты.
  • Служебный метод не сохраняет новую учетную запись в базе данных при наличии дублированного адреса электронной почты.
  • Наш сервисный метод не кодирует пароль пользователя, если указан дублирующий адрес электронной почты.
  • Наш сервисный метод проверяет, является ли адрес электронной почты уникальным, если указан уникальный адрес электронной почты.
  • Когда указан уникальный адрес электронной почты, наш метод обслуживания создает новый объект User, который содержит правильную информацию, и сохраняет информацию о созданном объекте User в базу данных.
  • Когда указан уникальный адрес электронной почты, наш сервисный метод возвращает информацию о созданной учетной записи пользователя.
  • Если указан уникальный адрес электронной почты и используется социальный вход, наш метод обслуживания не должен устанавливать пароль созданной учетной записи пользователя (или кодировать его).

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

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
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
import net.petrikainulainen.spring.social.signinmvc.user.dto.RegistrationForm;
import net.petrikainulainen.spring.social.signinmvc.user.dto.RegistrationFormBuilder;
import net.petrikainulainen.spring.social.signinmvc.user.model.SocialMediaService;
import net.petrikainulainen.spring.social.signinmvc.user.model.User;
import net.petrikainulainen.spring.social.signinmvc.user.repository.UserRepository;
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 com.googlecode.catchexception.CatchException.catchException;
import static com.googlecode.catchexception.CatchException.caughtException;
import static net.petrikainulainen.spring.social.signinmvc.user.model.UserAssert.assertThatUser;
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.Matchers.isA;
import static org.mockito.Mockito.never;
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_SocialSignInAndDuplicateEmail_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(new User());
 
        catchException(registrationService).registerNewUserAccount(registration);
 
        verify(repository, times(1)).findByEmail(REGISTRATION_EMAIL_ADDRESS);
    }
 
    @Test
    public void registerNewUserAccount_SocialSignInAndDuplicateEmail_ShouldThrowException() 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(new User());
 
        catchException(registrationService).registerNewUserAccount(registration);
 
        assertThat(caughtException()).isExactlyInstanceOf(DuplicateEmailException.class);
    }
 
    @Test
    public void registerNewUserAccount_SocialSignInAndDuplicateEmail_ShouldNotSaveNewUserAccount() 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(new User());
 
        catchException(registrationService).registerNewUserAccount(registration);
 
        verify(repository, never()).save(isA(User.class));
    }
 
    @Test
    public void registerNewUserAccount_SocialSignInAndDuplicateEmail_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(new User());
 
        catchException(registrationService).registerNewUserAccount(registration);
 
        verifyZeroInteractions(passwordEncoder);
    }
 
    @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();
 
        assertThatUser(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);
 
        assertThatUser(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);
    }
}

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

В этом классе много юнит-тестов. Мы уверены, что каждый из них действительно необходим?

А может и нет

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

Однако, если мы сделаем это, мы не найдем ответ на гораздо более интересный вопрос. Этот вопрос:

Должны ли мы действительно проверять каждое взаимодействие между тестируемым кодом и нашими фиктивными объектами?

Несколько месяцев назад я натолкнулся на статью Джеймса Коплина «Почему большинство модульных тестов — отходы» . Эта статья имеет несколько хороших моментов, но один из них очень хорошо подошел в этой ситуации. Джеймс Коплиен утверждал, что мы должны задать один вопрос о каждом тесте в нашем наборе тестов:

Если этот тест не пройден, какое бизнес-требование скомпрометировано?

Он также объясняет, почему это такой важный вопрос:

В большинстве случаев ответ таков: «Я не знаю». Если вы не знаете значение теста, то теоретически тест может иметь нулевую ценность для бизнеса. Тест имеет свои затраты: обслуживание, вычислительное время, администрирование и так далее. Это означает, что тест может иметь отрицательное значение. Это четвертая категория тестов для удаления.

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

Всплывающий вопрос

Когда задают вопрос: «Если этот тест не пройден, какое бизнес-требование скомпрометировано?» о каждом модульном тесте нашего тестового класса мы получаем следующие ответы:

  • Служебный метод проверяет, является ли адрес электронной почты уникальным, если указан повторяющийся адрес электронной почты.
    • У пользователя должен быть уникальный адрес электронной почты.
  • DuplicateEmailException генерируется, когда дается повторяющийся адрес электронной почты.
    • У пользователя должен быть уникальный адрес электронной почты.
  • Служебный метод не сохраняет новую учетную запись в базе данных при наличии дублированного адреса электронной почты.
    • У пользователя должен быть уникальный адрес электронной почты.
  • Наш сервисный метод не кодирует пароль пользователя, если указан дублирующий адрес электронной почты.
  • Наш сервисный метод проверяет, является ли адрес электронной почты уникальным, если указан уникальный адрес электронной почты.
    • У пользователя должен быть уникальный адрес электронной почты.
  • Когда указан уникальный адрес электронной почты, наш метод обслуживания создает новый объект User, который содержит правильную информацию, и сохраняет информацию о созданном объекте User в используемую базу данных.
    • Если у зарегистрированной учетной записи пользователя есть уникальный адрес электронной почты, он должен быть сохранен в базе данных.
    • Если учетная запись зарегистрированного пользователя создается с помощью входа в социальную сеть, наш метод обслуживания должен сохранить поставщика использованной социальной учетной записи.
  • Когда указан уникальный адрес электронной почты, наш сервисный метод возвращает информацию о созданной учетной записи пользователя.
    • Наш сервисный метод должен возвращать информацию о созданной учетной записи пользователя.
  • Если указан уникальный адрес электронной почты и используется социальный вход, наш метод обслуживания не должен устанавливать пароль созданной учетной записи пользователя (или кодировать его).
    • Учетная запись пользователя, созданная с помощью входа в систему, не имеет пароля.

Сначала кажется, что у нашего тестового класса есть только один модульный тест, который не имеет деловой ценности (или может иметь отрицательную чистую стоимость). Этот модульный тест гарантирует отсутствие взаимодействия между нашим кодом и макетом PasswordEncoder, когда пользователь пытается создать новую учетную запись пользователя с использованием дублированного адреса электронной почты.

Ясно, что мы должны удалить этот модульный тест, но это не единственный модульный тест, который необходимо удалить.

Кроличья нора глубже, чем ожидалось

Ранее мы заметили, что наш тестовый класс содержит два модульных теста, которые оба проверяют, что вызывается метод findByEmail () интерфейса UserRepository . Когда мы более подробно рассмотрим реализацию протестированного сервисного метода, мы заметим, что:

  • Наш сервисный метод генерирует исключение DuplicateEmailException, когда метод findByEmail () интерфейса UserRepository возвращает объект User .
  • Наш сервисный метод создает новую учетную запись пользователя, когда метод findByEmail () интерфейса UserRepository возвращает значение NULL.

Соответствующая часть проверенного метода обслуживания выглядит следующим образом:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
public User registerNewUserAccount(RegistrationForm userAccountData) throws DuplicateEmailException {
    if (emailExist(userAccountData.getEmail())) {
        //If the PersonRepository returns a Person object, an exception is thrown.
        throw new DuplicateEmailException("The email address: " + userAccountData.getEmail() + " is already in use.");
    }
 
    //If the PersonRepository returns null, the execution of this method continues.
}
 
private boolean emailExist(String email) {
    User user = repository.findByEmail(email);
 
    if (user != null) {
        return true;
    }
 
    return false;
}

Я утверждаю, что мы должны удалить оба этих модульных теста по двум причинам:

  • Пока мы правильно настроили макет PersonRepository , мы знаем, что его метод findByEmail () был вызван с использованием правильного параметра метода. Хотя мы можем связать эти тестовые примеры с бизнес-требованиями (адрес электронной почты пользователя должен быть уникальным), нам не нужно, чтобы они проверяли, что это бизнес-требование не скомпрометировано.
  • Эти модульные тесты не документируют API нашего сервисного метода. Они документируют его реализацию. Подобные тесты вредны, потому что они засоряют наш набор тестов ненужными тестами и затрудняют рефакторинг.

Если мы не настраиваем наши фиктивные объекты, они возвращают «хорошие» значения.
В FAQ по Mockito говорится, что:

Чтобы быть прозрачными и ненавязчивыми, все mockito mock по умолчанию возвращают ‘nice’ значения. Например: нули, ложные значения, пустые коллекции или нули. Обратитесь к javadocs о заглушке, чтобы увидеть, какие именно значения возвращаются по умолчанию.

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

Давайте двигаться дальше и навести порядок в этом беспорядке.

Навести порядок

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

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
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
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 com.googlecode.catchexception.CatchException.catchException;
import static com.googlecode.catchexception.CatchException.caughtException;
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.Matchers.isA;
import static org.mockito.Mockito.never;
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_SocialSignInAndDuplicateEmail_ShouldThrowException() 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(new User());
 
        catchException(registrationService).registerNewUserAccount(registration);
 
        assertThat(caughtException()).isExactlyInstanceOf(DuplicateEmailException.class);
    }
 
    @Test
    public void registerNewUserAccount_SocialSignInAndDuplicateEmail_ShouldNotSaveNewUserAccount() 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(new User());
 
        catchException(registrationService).registerNewUserAccount(registration);
 
        verify(repository, never()).save(isA(User.class));
    }
 
    @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();
 
        assertThatUser(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);
 
        assertThatUser(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);
    }
}

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

  • Наш тестовый класс имеет меньше юнит-тестов . Это может показаться странным преимуществом, потому что часто нам рекомендуется писать как можно больше юнит-тестов. Однако, если мы думаем об этом, иметь меньше юнит-тестов имеет смысл, потому что у нас меньше тестов для обслуживания. Это и тот факт, что каждый модуль тестирует только одну вещь, делает наш код проще в обслуживании и рефакторинге.
  • Мы улучшили качество нашей документации . Удаленные модульные тесты не документировали публичный API протестированного метода сервиса. Они задокументировали его реализацию. Поскольку эти тесты были удалены, легче определить требования к тестируемому методу обслуживания.

Резюме

Этот пост научил нас трем вещам:

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

Мы многого достигли в этом уроке . Как вы думаете, можно ли сделать эти юнит-тесты еще лучше?

Если вы хотите узнать больше о написании чистых тестов, прочитайте все части моего учебника Writing Clean Tests .