怎样编写测试类测试分支
很难为干净的代码找到一个好的定义,因为我们每个人都有自己的单词clean的定义。 但是,有一个似乎是通用的定义:
简洁的代码易于阅读。
这可能会让您感到有些惊讶,但我认为该定义也适用于测试代码。 使测试尽可能具有可读性是我们的最大利益,因为:
- 如果我们的测试易于阅读,那么很容易理解我们的代码是如何工作的。
- 如果我们的测试易于阅读,那么如果测试失败(不使用调试器),很容易发现问题。
编写干净的测试并不难,但是需要大量的实践,这就是为什么如此多的开发人员为此苦苦挣扎的原因。
我也为此感到挣扎,这就是为什么我决定与您分享我的发现的原因。
这是我教程的第四部分,描述了我们如何编写干净的测试。 这次我们将学习为什么不使用new关键字在测试方法中创建对象。 我们还将学习如何用工厂方法和测试数据构建器替换new关键字。
新不是新黑
在本教程中,我们一直在重构单元测试,以确保当使用唯一的电子邮件地址和社交登录提供者创建新用户帐户时, RepositoryUserService类的registerNewUserAccount(RegistrationForm userAccountData)方法能够按预期工作。
RegistrationForm类是一个数据传输对象(DTO) ,我们的单元测试使用setter方法设置其属性值。 我们的单元测试的源代码如下所示(相关代码突出显示):
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 = "john.smith@gmail.com";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;@Mockprivate PasswordEncoder passwordEncoder;@Mockprivate UserRepository repository;@Beforepublic void setUp() {registrationService = new RepositoryUserService(passwordEncoder, repository);}@Testpublic void registerNewUserAccount_SocialSignInAndUniqueEmail_ShouldCreateNewUserAccountAndSetSignInProvider() throws DuplicateEmailException {RegistrationForm registration = new RegistrationForm();registration.setEmail(REGISTRATION_EMAIL_ADDRESS);registration.setFirstName(REGISTRATION_FIRST_NAME);registration.setLastName(REGISTRATION_LAST_NAME);registration.setSignInProvider(SOCIAL_SIGN_IN_PROVIDER);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);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);}
}
那么,有什么问题呢? 我们的单元测试中突出显示的部分很短,而且相对容易阅读。 我认为,此代码的最大问题是它是以数据为中心的。 它创建了一个新的RegistrationForm对象并设置了创建对象的属性值,但没有描述这些属性值的含义。
如果我们使用new关键字在测试方法中创建新对象,则由于以下原因,我们的测试将变得难以阅读:
- 读者必须知道所创建对象的不同状态。 例如,如果我们考虑示例,读者必须知道,如果我们创建一个新的RegistrationForm对象并设置email , firstName , lastName和signInProvider属性的属性值,则意味着该对象是一个注册,即通过使用社交登录提供商进行。
- 如果创建的对象具有许多属性,则创建该对象的代码会乱码我们测试的源代码。 我们应该记住,即使我们在测试中需要这些对象,我们也应该专注于描述被测试方法/功能的行为。
尽管不能完全消除这些缺点是不现实的,但我们应尽最大努力将其影响降到最低,并使我们的测试尽可能易于阅读。
让我们找出如何使用工厂方法来做到这一点。
使用工厂方法
当我们使用工厂方法创建新对象时,我们应该以这种方式命名工厂方法及其方法参数,以使我们的代码更易于读写。 让我们看一下两种不同的工厂方法,看看它们对我们的单元测试的可读性有什么样的影响。
这些工厂方法通常添加到对象母类中,因为它们通常对多个测试类有用。 但是,由于我想保持简单,因此将它们直接添加到测试类中。
第一个工厂方法的名称是newRegistrationViaSocialSignIn() ,并且没有方法参数。 在将此工厂方法添加到测试类之后,单元测试的源如下所示(相关部分已突出显示):
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 = "john.smith@gmail.com";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;@Mockprivate PasswordEncoder passwordEncoder;@Mockprivate UserRepository repository;@Beforepublic void setUp() {registrationService = new RepositoryUserService(passwordEncoder, repository);}@Testpublic void registerNewUserAccount_SocialSignInAndUniqueEmail_ShouldCreateNewUserAccountAndSetSignInProvider() throws DuplicateEmailException {RegistrationForm registration = newRegistrationViaSocialSignIn();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);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);}private RegistrationForm newRegistrationViaSocialSignIn() {RegistrationForm registration = new RegistrationForm();registration.setEmail(REGISTRATION_EMAIL_ADDRESS);registration.setFirstName(REGISTRATION_FIRST_NAME);registration.setLastName(REGISTRATION_LAST_NAME);registration.setSignInProvider(SOCIAL_SIGN_IN_PROVIDER);return registration;}
}
第一种工厂方法具有以下后果:
- 我们测试方法的一部分,它创建了新的RegistrationForm对象,比以前干净得多,并且工厂方法的名称描述了所创建的RegistrationForm对象的状态。
- 我们的模拟对象的配置更难以阅读,因为email属性的值在我们的工厂方法中被“隐藏”了。
- 由于创建的RegistrationForm对象的属性值被“隐藏”在我们的工厂方法中,因此我们的断言更难以阅读。
如果使用对象母模式 ,则问题将更大,因为我们必须将相关的常量移至对象母类。
我认为可以说,尽管第一种工厂方法有其好处,但它也有严重的缺点。
让我们看看第二种工厂方法是否可以消除这些缺点。
第二个工厂方法的名称为newRegistrationViaSocialSignIn() ,并且它将电子邮件地址,名字,姓氏和提供程序中的社交符号作为方法参数。 在将此工厂方法添加到测试类之后,单元测试的源如下所示(相关部分已突出显示):
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 = "john.smith@gmail.com";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;@Mockprivate PasswordEncoder passwordEncoder;@Mockprivate UserRepository repository;@Beforepublic void setUp() {registrationService = new RepositoryUserService(passwordEncoder, repository);}@Testpublic void registerNewUserAccount_SocialSignInAndUniqueEmail_ShouldCreateNewUserAccountAndSetSignInProvider() throws DuplicateEmailException {RegistrationForm registration = newRegistrationViaSocialSignIn(REGISTRATION_EMAIL_ADDRESS,REGISTRATION_FIRST_NAME,REGISTRATION_LAST_NAME,SOCIAL_MEDIA_SERVICE);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);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);}private RegistrationForm newRegistrationViaSocialSignIn(String emailAddress, String firstName, String lastName, SocialMediaService signInProvider) {RegistrationForm registration = new RegistrationForm();registration.setEmail(emailAddress);registration.setFirstName(firstName);registration.setLastName(lastName);registration.setSignInProvider(signInProvider);return registration;}
}
第二种工厂方法具有以下后果:
- 我们的测试方法的一部分(创建新的RegistrationForm对象)比使用第一个工厂方法的相同代码稍微有些混乱。 但是,它仍然比原始代码干净,因为factory方法的名称描述了创建对象的状态。
- 似乎消除了第一个工厂方法的弊端,因为创建的对象的属性值未“隐藏”在工厂方法内部。
看起来很酷,对吧?
真的很容易想到天堂里一切都很好,但是事实并非如此。 尽管我们已经看到工厂方法可以使我们的测试更具可读性,但事实是,只有在满足以下条件时,它们才是一个不错的选择:
- 工厂方法没有太多的方法参数。 当方法参数的数量增加时,我们的测试将变得更加难以读写。 显而易见的问题是:工厂方法可以有多少个方法参数? 不幸的是,很难给出确切的答案,但是我认为,如果工厂方法只有少数方法参数,那么使用工厂方法是一个不错的选择。
- 测试数据没有太大的差异。 使用工厂方法的问题是单个工厂方法通常适用于一个用例。 如果我们需要支持N个用例,则需要N种工厂方法。 这是一个问题,因为随着时间的流逝,我们的工厂方法变得ated肿,混乱并且难以维护(尤其是如果使用对象母模式)。
让我们找出测试数据生成器是否可以解决其中一些问题。
使用测试数据构建器
测试数据构建器是使用构建器模式创建新对象的类。 Effective Java中描述的构建器模式有很多好处 ,但是我们的主要动机是提供一种流畅的API以创建测试中使用的对象。
我们可以按照以下步骤创建一个测试数据构建器类,该类创建新的RegistrationForm对象:
- 创建一个RegistrationFormBuilder类。
- 将RegistrationForm字段添加到创建的类。 该字段包含对创建对象的引用。
- 将默认构造函数添加到创建的类中,并通过创建新的RegistrationForm对象来实现它。
- 添加用于设置创建的RegistrationForm对象的属性值的方法。 每个方法都通过调用正确的setter方法来设置属性值,并返回对RegistrationFormBuilder对象的引用。 请记住,这些方法的方法名称可以建立或破坏我们的DSL 。
- 向所创建的类中添加一个build()方法,并通过返回所创建的RegistrationForm对象来实现它。
我们的测试数据构建器类的源代码如下所示:
public class RegistrationFormBuilder {private RegistrationForm registration;public RegistrationFormBuilder() {registration = new RegistrationForm();}public RegistrationFormBuilder email(String email) {registration.setEmail(email);return this;}public RegistrationFormBuilder firstName(String firstName) {registration.setFirstName(firstName);return this;}public RegistrationFormBuilder lastName(String lastName) {registration.setLastName(lastName);return this;}public RegistrationFormBuilder isSocialSignInViaSignInProvider(SocialMediaService signInProvider) {registration.setSignInProvider(signInProvider);return this;}public RegistrationForm build() {return registration;}
}
在修改了单元测试以使用新的测试数据构建器类之后,其源代码如下所示(相关部分已突出显示):
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 = "john.smith@gmail.com";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;@Mockprivate PasswordEncoder passwordEncoder;@Mockprivate UserRepository repository;@Beforepublic void setUp() {registrationService = new RepositoryUserService(passwordEncoder, repository);}@Testpublic 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>() {@Overridepublic 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);}
}
如我们所见,测试数据构建器具有以下优点:
- 创建新的RegistrationForm对象的代码易于阅读和编写。 我非常喜欢流畅的API,并且我认为这段代码既优美又优雅。
- 构建器模式可确保从我们的测试数据中发现的变化不再是问题,因为我们可以简单地将新方法添加到测试数据构建器类中。
- 模拟对象和断言的配置易于阅读,因为常量在我们的测试方法中可见,并且DSL强调每个属性值的含义。
那么,我们应该对所有内容使用构建器模式吗?
没有!
仅在有意义时,才应使用测试数据构建器。 换句话说,我们应该在以下情况下使用它们:
- 我们设置了许多属性值。
- 我们的测试数据有很大的差异。
如果满足以下条件之一,则构建器模式是一个完美的选择。 原因是我们可以通过命名builder类的setter-like方法来创建特定于域的语言 。 即使我们将创建许多不同的对象并设置许多属性值,这也使我们的测试易于读写。
那是建造者木匠的力量。
如果您想了解有关流利API的更多信息,则应阅读以下文章:
- 流利的界面
- Java Fluent API设计器速成课程
- 用Java构建流畅的API(内部DSL)
今天就这些。 让我们继续并总结从这篇博客文章中学到的知识。
摘要
我们了解了为什么使用new关键字在测试方法中创建对象不是一个好主意,并且我们学习了两种不同的方法来创建在测试中使用的对象。
更具体地说,这篇博客文章教会了我们三件事:
- 通过使用new关键字在测试方法中创建所需的对象是一个坏主意,因为它会使我们的测试混乱且难以阅读。
- 如果我们只需要设置少数几个属性值,而我们的测试数据没有太多变化,则应该使用工厂方法来创建所需的对象。
- 如果必须设置很多属性值和/或我们的测试数据有很多差异,则应该使用测试数据生成器来创建所需的对象。
翻译自: https://www.javacodegeeks.com/2014/05/writing-clean-tests-new-considered-harmful.html
怎样编写测试类测试分支