验证码编写
在编写使用模拟对象的单元测试时,请遵循以下步骤:
- 配置我们的模拟对象的行为。
- 调用测试的方法。
- 验证是否已调用模拟对象的正确方法。
第三步的描述实际上有点误导,因为通常我们最终会验证是否调用了正确的方法以及未调用模拟对象的其他方法。
每个人都知道,如果我们要编写无错误的软件,我们必须验证这两种情况或不良情况的发生。
对?
让我们验证一切
让我们首先看一下用于向数据库添加新用户帐户的服务方法的实现。
此服务方法的要求是:
- 如果注册用户帐户的电子邮件地址不是唯一的,我们的服务方法必须抛出异常。
- 如果注册的用户帐户具有唯一的电子邮件地址,则我们的服务方法必须将新的用户帐户添加到数据库中。
- 如果注册的用户帐户具有唯一的电子邮件地址,并且是使用常规登录创建的,则我们的服务方法必须先对用户密码进行编码,然后再将其保存到数据库中。
- 如果注册的用户帐户具有唯一的电子邮件地址,并且是使用社交登录创建的,则我们的服务方法必须保存使用的社交登录提供商。
- 通过使用社交登录创建的用户帐户不得具有密码。
- 我们的服务方法必须返回创建的用户帐户的信息。
如果您想了解如何指定服务方法的要求,则应阅读以下博客文章:
- 从上到下:Web应用程序的TDD
- 从构思到代码:敏捷规范的生命周期
通过执行以下步骤来实现此服务方法:
- 服务方法检查是否从数据库中找不到用户提供的电子邮件地址。 它是通过调用UserRepository接口的findByEmail()方法来实现的。
- 如果找到User对象,则服务方法方法将抛出DuplicateEmailException 。
- 它创建一个新的User对象。 如果通过使用常规登录进行注册 (未设置RegistrationForm类的signInProvider属性),则service方法将对用户提供的密码进行编码,并将编码后的密码设置为创建的User对象。
- 服务方法将创建的User对象的信息保存到数据库中,并返回保存的User对象。
RepositoryUserService类的源代码如下所示:
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;@Autowiredpublic RepositoryUserService(PasswordEncoder passwordEncoder, UserRepository repository) {this.passwordEncoder = passwordEncoder;this.repository = repository;}@Transactional@Overridepublic 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 。
- 给定重复的电子邮件地址时,service方法不会将新帐户保存到数据库中。
- 如果提供重复的电子邮件地址,我们的服务方法不会对用户的密码进行编码。
- 当提供唯一的电子邮件地址时,我们的服务方法会检查电子邮件地址是否唯一。
- 当给出唯一的电子邮件地址时,我们的服务方法将创建一个包含正确信息的新User对象,并将创建的User对象的信息保存到数据库中。
- 当给出唯一的电子邮件地址时,我们的服务方法将返回创建的用户帐户的信息。
- 当提供唯一的电子邮件地址并使用社交登录名时,我们的服务方法不得设置已创建用户帐户的密码(或对其进行编码)。
我们的测试类的源代码如下所示:
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 = "john.smith@gmail.com";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;@Mockprivate PasswordEncoder passwordEncoder;@Mockprivate UserRepository repository;@Beforepublic void setUp() {registrationService = new RepositoryUserService(passwordEncoder, repository);}@Testpublic 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);}@Testpublic 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);}@Testpublic 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));}@Testpublic 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);}@Testpublic 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);}@Testpublic 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);}@Testpublic 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>() {@Overridepublic 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);}@Testpublic 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);}
}
这些单元测试是按照本教程前面部分中给出的说明编写的。
该课程有很多单元测试。 我们确定他们每个人都是真的必要吗?
或者可能不是
一个明显的问题是,我们编写了两个单元测试,两个单元测试都验证我们的服务方法检查了用户提供的电子邮件地址是否唯一。 我们可以通过将这些测试合并为一个单元测试来解决此问题。 毕竟,一项测试应该使我们确信,我们的服务方法会在创建新用户帐户之前验证用户提供的电子邮件地址是否唯一。
但是,如果这样做,我们将找不到更有趣的问题的答案。 这个问题是:
我们是否应该真的验证测试代码和模拟对象之间的每一次交互?
几个月前,我遇到了James Coplien撰写的标题为: 为什么大多数单元测试都是浪费的文章。 本文提出了几点要点,但其中之一非常适合这种情况。 詹姆斯·科普林(James Coplien)认为,对于测试套件中的每个测试,我们应该提出一个问题:
如果该测试失败,那么将损害哪些业务要求?
他还解释了为什么这是一个如此重要的问题:
在大多数情况下,答案是“我不知道”。 如果您不知道测试的价值,那么从理论上讲,测试的商业价值可能为零。 测试确实要付出代价:维护,计算时间,管理等等。 这意味着测试可能具有净负值。 这是要删除的第四类测试。
让我们找出使用此问题评估单元测试时会发生什么。
弹出问题
当问一个问题时:“如果该测试失败,将危及哪些业务需求?” 关于测试类的每个单元测试,我们得到以下答案:
- 当提供重复的电子邮件地址时,服务方法将检查电子邮件地址是否唯一。
- 用户必须具有唯一的电子邮件地址。
- 给定重复的电子邮件地址时,将引发DuplicateEmailException 。
- 用户必须具有唯一的电子邮件地址。
- 给定重复的电子邮件地址时,service方法不会将新帐户保存到数据库中。
- 用户必须具有唯一的电子邮件地址。
- 如果提供重复的电子邮件地址,我们的服务方法不会对用户的密码进行编码。
- –
- 当提供唯一的电子邮件地址时,我们的服务方法会检查电子邮件地址是否唯一。
- 用户必须具有唯一的电子邮件地址。
- 当给出唯一的电子邮件地址时,我们的服务方法将创建一个包含正确信息的新User对象,并将创建的User对象的信息保存到使用的数据库中。
- 如果注册的用户帐户具有唯一的电子邮件地址,则必须将其保存到数据库中。
- 当给出唯一的电子邮件地址时,我们的服务方法将返回创建的用户帐户的信息。
- 我们的服务方法必须返回创建的用户帐户的信息。
- 当提供唯一的电子邮件地址并使用社交登录名时,我们的服务方法不得设置已创建用户帐户的密码(或对其进行编码)。
- 使用社交登录创建的用户帐户没有密码。
乍一看,我们的测试类似乎只有一个没有业务价值(或可能有负净值)的单元测试。 此单元测试可确保当用户尝试使用重复的电子邮件地址创建新的用户帐户时,我们的代码与PasswordEncoder模拟之间没有任何交互。
显然,我们必须删除此单元测试,但这不是唯一必须删除的单元测试。
兔子洞比预期的要深
早些时候,我们注意到我们的测试类包含两个单元测试,两个单元测试都可以验证是否调用了UserRepository接口的findByEmail()方法。 当我们仔细查看测试的服务方法的实现时,我们注意到:
- 当UserRepository接口的findByEmail()方法返回User对象时,我们的服务方法将引发DuplicateEmailException 。
- 当UserRepository接口的findByEmail()方法返回null时,我们的服务方法将创建一个新的用户帐户。
经过测试的服务方法的相关部分如下所示:
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()方法是通过使用正确的method参数调用的。 尽管我们可以将这些测试用例链接到业务需求(用户的电子邮件地址必须是唯一的),但是我们不需要它们来验证该业务需求没有受到损害。
- 这些单元测试未记录我们服务方法的API。 他们记录了它的实现。 像这样的测试是有害的,因为它们使我们的测试套件杂乱无章,并且使重构变得更加困难。
如果我们不配置模拟对象,它们将返回“ nice”值。
Mockito常见问题解答指出:
为了透明和不引人注目,默认情况下,所有Mockito模拟都返回“ nice”值。 例如:零,假,空集合或空。 请参阅有关存根的javadocs,以查看确切地返回默认值。
这就是为什么我们应该始终配置相关的模拟对象的原因! 如果我们不这样做,我们的测试可能就没有用了。
让我们继续前进,清理这个烂摊子。
清理混乱
从测试类中删除这些单元测试后,其源代码如下所示:
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 = "john.smith@gmail.com";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;@Mockprivate PasswordEncoder passwordEncoder;@Mockprivate UserRepository repository;@Beforepublic void setUp() {registrationService = new RepositoryUserService(passwordEncoder, repository);}@Testpublic 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);}@Testpublic 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));}@Testpublic 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);}@Testpublic 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>() {@Overridepublic 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);}@Testpublic 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的单元测试,因为这些测试使我们的代码(和测试)更难以维护和重构。
- 如果发现现有的单元测试违反了这两个规则,则应将其删除。
在本教程中,我们取得了很多成就。 您认为可以使这些单元测试变得更好吗?
如果您想了解有关编写干净测试的更多信息,请阅读我的编写干净测试教程的所有部分 。
翻译自: https://www.javacodegeeks.com/2014/08/writing-clean-tests-to-verify-or-not-to-verify.html
验证码编写