我们已经知道, “干净的”单元测试可能没有我们想象的那么干净 。
我们已尽力使我们的单元测试尽可能干净。 我们的测试格式正确,使用特定于域的语言,并避免了过多的嘲笑。
但是,我们的单元测试并不干净,因为:
- 当我们更改测试代码时,大多数现有的单元测试在运行它们时都不会编译或失败。 修复这些单元测试缓慢而令人沮丧。
- 当我们向被测试的类中添加新方法时,我们意识到编写新的单元测试要慢得多。
如果是这样,我们的单元测试很可能会遇到以下常见问题:
- 我们的测试方法的方法名称太长。 如果测试失败,则方法名称不一定说明发生了什么问题。 另外,很难对我们的测试涵盖的情况进行简要概述。 这意味着我们可能会多次测试相同的情况。
- 我们的测试方法包含重复的代码,这些代码配置了模拟对象并创建了我们的测试中使用的其他对象。 这意味着我们的测试很难读取,编写和维护。
- 因为没有干净的方法可以仅使用几种测试方法来共享通用配置,所以我们必须将所有常量放在测试类的开头。 你们中的某些人可能声称这是一个小问题,您是对的,但是它仍然使我们的测试类比应有的更混乱。
让我们找出如何解决所有这些问题。
如果您尚未阅读我的博客文章: 写干净的测试–天堂中的麻烦 , 则应在继续阅读此博客文章之前阅读它 。 它以更多详细信息描述了这些问题,并提供了有助于您理解此博客文章的其他信息。
抢救的嵌套配置
如果我们想解决单元测试中发现的问题,我们必须
- 以不需要长方法名的方式描述被测方法和被测状态。
- 寻找一种将通用配置从测试方法转移到设置方法的方法。
- 为测试方法创建一个公共上下文,并使设置方法和常量仅对属于创建的上下文的测试方法可见。
有一个JUnit运行器可以帮助我们实现这些目标。 它称为NestedRunner ,它使我们可以运行放在嵌套内部类中的测试方法。
在开始使用NestedRunner解决问题之前,我们必须将NestedRunner依赖项添加到Maven构建中,并确保NestedRunner类调用我们的测试方法。
首先 ,我们需要在pom.xml文件中添加以下依赖项声明:
<dependency>
<groupId>com.nitorcreations</groupId>
<artifactId>junit-runners</artifactId>
<version>1.2</version>
<scope>test</scope>
</dependency>
其次 ,我们需要对RepositoryUserServiceTest类进行以下更改:
- 确保从NestedRunner类调用从RepositoryUserServiceTest类找到的测试方法。
- 从passwordEncoder和存储库字段中删除@Mock批注。
- 通过调用Mockito类的静态嘲笑()方法来创建所需的嘲笑对象,并将其插入passwordEncoder和存储库字段。
您还可以将@Mock批注保留在passwordEncoder和存储库字段中,并通过调用MockitoAnnotations类的静态initMocks()方法来创建模拟对象。 我决定在这里使用手动方法,因为它更加简单。
RepositoryUserServiceTest类的源代码如下所示:
import com.nitorcreations.junit.runners.NestedRunner
import org.junit.Before;
import org.junit.runner.RunWith;
import org.springframework.security.crypto.password.PasswordEncoder;
import static org.mockito.Mockito.mock;
@RunWith(NestedRunner.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;
private PasswordEncoder passwordEncoder;
private UserRepository repository;
@Before
public void setUp() {
passwordEncoder = mock(PasswordEncoder.class);
repository = mock(UserRepository.class);
registrationService = new RepositoryUserService(passwordEncoder, repository);
}
}
补充阅读:
现在,我们已经配置了NestedRunner,可以开始解决从单元测试中发现的问题。 让我们先用嵌套的类层次结构替换长方法名。
用嵌套的类层次结构替换长测试方法名称
在用嵌套的类层次结构替换长测试方法名称之前,我们需要弄清楚单元测试所涵盖的情况。 如果看一下我们的测试类 ,我们会注意到从RepositoryUserServiceTest类找到的单元测试确保:
- 如果已经有一个具有相同电子邮件地址的用户帐户,则我们的代码应
- 引发异常。
- 如果没有用户帐户具有相同的电子邮件地址,则我们的代码应
- 保存一个新的用户帐户。
现在,我们可以通过将测试方法替换为BDD样式类层次结构来消除长测试方法名称。 我们的想法是:
- 每个测试方法创建一个内部类。 此类可以包含设置方法,测试方法和其他内部类。 在我们的例子中,此内部类的名称为RegisterNewUserAccount 。
- 创建描述被测状态的类层次结构。 我们可以通过向RegisterNewUserAccount类(及其内部类)添加内部类来实现。 我们可以使用以下语法来命名这些内部类: When [StateUnderTest] 。 我们可以按照以下步骤将此类层次结构添加到我们的测试类中:
- 因为用户正在使用社交登录来注册用户帐户,所以我们必须将WhenUserUsesSocialSignIn类添加到RegisterNewUserAccount类。
- 因为必须涵盖两种不同的情况,所以我们必须将两个内部类( WhenUserAccountIsFoundWithEmailAddress和WhenEmailAddressIsUnique )添加到WhileUserUsesSocialSignIn类。
- 将实际的测试方法添加到正确的内部类中。 因为类层次结构描述了测试方法和测试状态,所以每个单元测试的名称都必须仅描述预期的行为。 一种执行此方法的方法是使用前缀: should来命名每个测试方法。
创建类层次结构后,测试类的源代码如下所示:
import com.nitorcreations.junit.runners.NestedRunner
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.mock;
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(NestedRunner.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;
private PasswordEncoder passwordEncoder;
private UserRepository repository;
@Before
public void setUp() {
passwordEncoder = mock(PasswordEncoder.class);
repository = mock(UserRepository.class);
registrationService = new RepositoryUserService(passwordEncoder, repository);
}
public class RegisterNewUserAccount {
public class WhenUserUsesSocialSignIn {
public class WhenUserAccountIsFoundWithEmailAddress {
@Test
public void 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 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));
}
}
public class WhenEmailAddressIsUnique {
@Test
public void shouldSaveNewUserAccount() 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(isA(User.class));
}
@Test
public void shouldSetCorrectEmailAddress() 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);
}
@Test
public void shouldSetCorrectFirstAndLastName() 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)
.hasFirstName(REGISTRATION_FIRST_NAME)
.hasLastName(REGISTRATION_LAST_NAME)
}
@Test
public void shouldCreateRegisteredUser() 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)
.isRegisteredUser()
}
@Test
public void shouldSetSignInProvider() 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)
.isRegisteredByUsingSignInProvider(SOCIAL_SIGN_IN_PROVIDER);
}
@Test
public void 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);
}
@Test
public void 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 returnedUserAccount = registrationService.registerNewUserAccount(registration);
ArgumentCaptor<User> userAccountArgument = ArgumentCaptor.forClass(User.class);
verify(repository, times(1)).save(userAccountArgument.capture());
User createdUserAccount = userAccountArgument.getValue();
assertThat(returnedUserAccount)
.isEqualTo(createdUserAccount);
}
}
}
}
}
现在,我们用嵌套的类层次结构代替了长测试方法的名称,但是此解决方案的缺点是我们添加了很多重复的代码。 让我们摆脱该代码。
删除重复的代码
通过将其移至放置在“正确”内部类中的设置方法,我们可以从测试类中删除所有重复的代码。 在确定“正确的”内部类之前,我们必须了解设置和测试方法的执行顺序。 理解这一点的最好方法是使用一个简单的示例:
@RunWith(NestedRunner.class)
public class TestClass {
/**
* This setup method is invoked before the test and setup methods
* found from the inner classes of this class.
* This is a good place for configuration that is shared by all
* test methods found from this test class.
*/
@Before
public void setUpTestClass() {}
public class MethodA {
/**
* This setup method is invoked before the test methods found from
* this class and before the test and setup methods found from the
* inner classes of this class.
*
* This is a good place for configuration that is shared by all test
* methods which ensure that the methodA() is working correctly.
*/
@Before
public void setUpMethodA() {}
@Test
public void shouldFooBar() {}
public class WhenFoo {
/**
* This setup method is invoked before the test methods found from
* this class and before the test and setup methods found from the
* inner classes of this class.
*
* This is a good place for configuration which ensures that the methodA()
* working correctly when foo is 'true'.
*/
@Before
public void setUpWhenFoo() {}
@Test
public void shouldBar() {}
}
public class WhenBar {
@Test
public shouldFoo() {}
}
}
}
换句话说,在调用测试方法之前,NestedRunner通过从类层次结构的根类导航到测试方法并调用所有设置方法来调用设置方法。 让我们看一下从示例中找到的测试方法:
- 在调用测试方法shouldFooBar()之前,NestedRunner会调用setUpTestClass()和setUpMethodA()方法。
- 在调用测试方法shouldBar()之前,NestedRunner会调用setUpTestClass() , setUpMethodA()和setUpWhenFoo()方法。
- 在调用测试方法shouldFoo()之前,NestedRunner会调用setUpTestClass()和setUpMethodA()方法。
现在,我们可以按照以下步骤对RepositoryUserServiceTest类进行必要的修改:
- 将一个setUp()方法添加到WhenUserUsesSocialSignIn类,并通过创建一个新的RegistrationForm对象来实现它。 这是执行此操作的正确位置,因为所有单元测试都将RegistrationForm对象作为已测试方法的输入。
- 将一个setUp()方法添加到WhileUserAccountIsFoundWithEmailAddress类,并配置我们的存储库模拟,以使用输入到注册表单中的电子邮件地址在调用它的findByEmail()方法时返回一个User对象。 在此代码的正确位置,因为从WhenUserAccountIsFoundWithEmailAddress类找到的每个单元测试都假定注册过程中提供的电子邮件地址不是唯一的。
- 将setUp()方法添加到WhileEmailAddressIsUnique类,并将我们的存储库模拟配置为1)当使用输入到注册表单中的电子邮件地址调用其findByEmail()方法时,返回null ; 2)返回以a表示的User对象调用其save()方法时的method参数。 在此代码的正确位置,因为从WhenEmailAddressIsUnique类找到的每个单元测试都假定注册期间给定的电子邮件地址是唯一的,并且返回了所创建用户帐户的信息。
完成这些更改后,测试类的源代码如下所示:
import com.nitorcreations.junit.runners.NestedRunner
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.BDDMockito.given;
import static org.mockito.Matchers.isA;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.verifyZeroInteractions;
@RunWith(NestedRunner.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;
private PasswordEncoder passwordEncoder;
private UserRepository repository;
@Before
public void setUp() {
passwordEncoder = mock(PasswordEncoder.class);
repository = mock(UserRepository.class);
registrationService = new RepositoryUserService(passwordEncoder, repository);
}
public class RegisterNewUserAccount {
public class WhenUserUsesSocialSignIn {
private RegistrationForm registration;
@Before
public void setUp() {
RegistrationForm registration = new RegistrationFormBuilder()
.email(REGISTRATION_EMAIL_ADDRESS)
.firstName(REGISTRATION_FIRST_NAME)
.lastName(REGISTRATION_LAST_NAME)
.isSocialSignInViaSignInProvider(SOCIAL_SIGN_IN_PROVIDER)
.build();
}
public class WhenUserAccountIsFoundWithEmailAddress {
@Before
public void setUp() {
given(repository.findByEmail(REGISTRATION_EMAIL_ADDRESS)).willReturn(new User());
}
@Test
public void shouldThrowException() throws DuplicateEmailException {
catchException(registrationService).registerNewUserAccount(registration);
assertThat(caughtException()).isExactlyInstanceOf(DuplicateEmailException.class);
}
@Test
public void shouldNotSaveNewUserAccount() throws DuplicateEmailException {
catchException(registrationService).registerNewUserAccount(registration);
verify(repository, never()).save(isA(User.class));
}
}
public class WhenEmailAddressIsUnique {
@Before
public void setUp() {
given(repository.findByEmail(REGISTRATION_EMAIL_ADDRESS)).willReturn(null);
given(repository.save(isA(User.class))).willAnswer(new Answer<User>() {
@Override
public User answer(InvocationOnMock invocation) throws Throwable {
Object[] arguments = invocation.getArguments();
return (User) arguments[0];
}
});
}
@Test
public void shouldSaveNewUserAccount() throws DuplicateEmailException {
registrationService.registerNewUserAccount(registration);
ArgumentCaptor<User> userAccountArgument = ArgumentCaptor.forClass(User.class);
verify(repository, times(1)).save(isA(User.class));
}
@Test
public void shouldSetCorrectEmailAddress() throws DuplicateEmailException {
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);
}
@Test
public void shouldSetCorrectFirstAndLastName() throws DuplicateEmailException {
registrationService.registerNewUserAccount(registration);
ArgumentCaptor<User> userAccountArgument = ArgumentCaptor.forClass(User.class);
verify(repository, times(1)).save(userAccountArgument.capture());
User createdUserAccount = userAccountArgument.getValue();
assertThatUser(createdUserAccount)
.hasFirstName(REGISTRATION_FIRST_NAME)
.hasLastName(REGISTRATION_LAST_NAME)
}
@Test
public void shouldCreateRegisteredUser() throws DuplicateEmailException {
registrationService.registerNewUserAccount(registration);
ArgumentCaptor<User> userAccountArgument = ArgumentCaptor.forClass(User.class);
verify(repository, times(1)).save(userAccountArgument.capture());
User createdUserAccount = userAccountArgument.getValue();
assertThatUser(createdUserAccount)
.isRegisteredUser()
}
@Test
public void shouldSetSignInProvider() throws DuplicateEmailException {
registrationService.registerNewUserAccount(registration);
ArgumentCaptor<User> userAccountArgument = ArgumentCaptor.forClass(User.class);
verify(repository, times(1)).save(userAccountArgument.capture());
User createdUserAccount = userAccountArgument.getValue();
assertThatUser(createdUserAccount)
.isRegisteredByUsingSignInProvider(SOCIAL_SIGN_IN_PROVIDER);
}
@Test
public void shouldNotCreateEncodedPasswordForUser() throws DuplicateEmailException {
registrationService.registerNewUserAccount(registration);
verifyZeroInteractions(passwordEncoder);
}
@Test
public void shouldReturnCreatedUserAccount() throws DuplicateEmailException {
User returnedUserAccount = registrationService.registerNewUserAccount(registration);
ArgumentCaptor<User> userAccountArgument = ArgumentCaptor.forClass(User.class);
verify(repository, times(1)).save(userAccountArgument.capture());
User createdUserAccount = userAccountArgument.getValue();
assertThat(returnedUserAccount)
.isEqualTo(createdUserAccount);
}
}
}
}
}
笔记:
- 该测试类使用BDDMockito ,但是您也可以使用“标准” Mockito API。
- 我们的测试类仍然有一些重复的代码,这些代码用于捕获作为UserRepository的save()方法的方法参数提供的User对象。 我将此代码留给测试方法,因为我不想对原始源代码进行太多更改。 此代码应移至正确命名的私有方法。
我们的测试课程看起来很干净,但是我们仍然可以使其更加干净。 让我们找出如何做到这一点。
将常数与测试方法链接
用常量替换魔术数字时,我们面临的一个问题是,必须将这些常量添加到测试类的开头。 这意味着很难将这些常量与使用它们的测试用例链接起来。
如果看一下单元测试类,我们会注意到在创建新的RegistrationForm对象时会使用常量。 因为这是在RegisterNewUserAccount类的setUp()方法中发生的,所以我们可以通过将常量从RepositoryUserServiceTest类的开头移动到RegisterNewUserAccount类的开头来解决问题。
完成此操作后,我们的测试类如下所示:
import com.nitorcreations.junit.runners.NestedRunner
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.BDDMockito.given;
import static org.mockito.Matchers.isA;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.verifyZeroInteractions;
@RunWith(NestedRunner.class)
public class RepositoryUserServiceTest {
private RepositoryUserService registrationService;
private PasswordEncoder passwordEncoder;
private UserRepository repository;
@Before
public void setUp() {
passwordEncoder = mock(PasswordEncoder.class);
repository = mock(UserRepository.class);
registrationService = new RepositoryUserService(passwordEncoder, repository);
}
public class RegisterNewUserAccount {
private final String REGISTRATION_EMAIL_ADDRESS = "john.smith@gmail.com";
private final String REGISTRATION_FIRST_NAME = "John";
private final String REGISTRATION_LAST_NAME = "Smith";
private final SocialMediaService SOCIAL_SIGN_IN_PROVIDER = SocialMediaService.TWITTER;
public class WhenUserUsesSocialSignIn {
private RegistrationForm registration;
@Before
public void setUp() {
RegistrationForm registration = new RegistrationFormBuilder()
.email(REGISTRATION_EMAIL_ADDRESS)
.firstName(REGISTRATION_FIRST_NAME)
.lastName(REGISTRATION_LAST_NAME)
.isSocialSignInViaSignInProvider(SOCIAL_SIGN_IN_PROVIDER)
.build();
}
public class WhenUserAccountIsFoundWithEmailAddress {
@Before
public void setUp() {
given(repository.findByEmail(REGISTRATION_EMAIL_ADDRESS)).willReturn(new User());
}
@Test
public void shouldThrowException() throws DuplicateEmailException {
catchException(registrationService).registerNewUserAccount(registration);
assertThat(caughtException()).isExactlyInstanceOf(DuplicateEmailException.class);
}
@Test
public void shouldNotSaveNewUserAccount() throws DuplicateEmailException {
catchException(registrationService).registerNewUserAccount(registration);
verify(repository, never()).save(isA(User.class));
}
}
public class WhenEmailAddressIsUnique {
@Before
public void setUp() {
given(repository.findByEmail(REGISTRATION_EMAIL_ADDRESS)).willReturn(null);
given(repository.save(isA(User.class))).willAnswer(new Answer<User>() {
@Override
public User answer(InvocationOnMock invocation) throws Throwable {
Object[] arguments = invocation.getArguments();
return (User) arguments[0];
}
});
}
@Test
public void shouldSaveNewUserAccount() throws DuplicateEmailException {
registrationService.registerNewUserAccount(registration);
ArgumentCaptor<User> userAccountArgument = ArgumentCaptor.forClass(User.class);
verify(repository, times(1)).save(isA(User.class));
}
@Test
public void shouldSetCorrectEmailAddress() throws DuplicateEmailException {
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);
}
@Test
public void shouldSetCorrectFirstAndLastName() throws DuplicateEmailException {
registrationService.registerNewUserAccount(registration);
ArgumentCaptor<User> userAccountArgument = ArgumentCaptor.forClass(User.class);
verify(repository, times(1)).save(userAccountArgument.capture());
User createdUserAccount = userAccountArgument.getValue();
assertThatUser(createdUserAccount)
.hasFirstName(REGISTRATION_FIRST_NAME)
.hasLastName(REGISTRATION_LAST_NAME)
}
@Test
public void shouldCreateRegisteredUser() throws DuplicateEmailException {
registrationService.registerNewUserAccount(registration);
ArgumentCaptor<User> userAccountArgument = ArgumentCaptor.forClass(User.class);
verify(repository, times(1)).save(userAccountArgument.capture());
User createdUserAccount = userAccountArgument.getValue();
assertThatUser(createdUserAccount)
.isRegisteredUser()
}
@Test
public void shouldSetSignInProvider() throws DuplicateEmailException {
registrationService.registerNewUserAccount(registration);
ArgumentCaptor<User> userAccountArgument = ArgumentCaptor.forClass(User.class);
verify(repository, times(1)).save(userAccountArgument.capture());
User createdUserAccount = userAccountArgument.getValue();
assertThatUser(createdUserAccount)
.isRegisteredByUsingSignInProvider(SOCIAL_SIGN_IN_PROVIDER);
}
@Test
public void shouldNotCreateEncodedPasswordForUser() throws DuplicateEmailException {
registrationService.registerNewUserAccount(registration);
verifyZeroInteractions(passwordEncoder);
}
@Test
public void shouldReturnCreatedUserAccount() throws DuplicateEmailException {
User returnedUserAccount = registrationService.registerNewUserAccount(registration);
ArgumentCaptor<User> userAccountArgument = ArgumentCaptor.forClass(User.class);
verify(repository, times(1)).save(userAccountArgument.capture());
User createdUserAccount = userAccountArgument.getValue();
assertThat(returnedUserAccount)
.isEqualTo(createdUserAccount);
}
}
}
}
}
现在很明显,这些常量与从RegisterNewUserAccount内部类及其内部类中找到的单元测试相关。 这似乎是一个小的调整,但是我已经注意到,小事情可以产生很大的变化。
让我们继续并总结从这篇博客文章中学到的知识。
摘要
这篇博客文章告诉我们
- 我们可以将长方法名称替换为BDD样式类层次结构。
- 我们可以通过将代码移至设置方法并将这些方法置于正确的内部类中来删除重复的代码。
- 通过在正确的内部类中声明常量,我们可以将常量与使用它们的测试用例链接起来。
PS我建议您阅读博客文章,标题为: 通过TDD进行代码质量的三个步骤 。 这是一篇很棒的博客文章,即使您不使用TDD,也可以使用它的课程。
如果要编写干净的测试,则应阅读我的“ 编写干净的测试”教程。
翻译自: https://www.javacodegeeks.com/2015/04/writing-clean-tests-small-is-beautiful.html