分而治之

好的单元测试应该仅出于一个原因而失败。 这意味着适当的单元测试仅测试一个逻辑概念。

如果我们要编写干净的测试,则必须识别那些逻辑概念,并且每个逻辑概念只编写一个测试用例。

这篇博客文章描述了我们如何识别从测试中发现的逻辑概念,以及如何将现有的单元测试分成多个单元测试。

干净还不够好

让我们先看一下单元测试的源代码,该源代码确保当使用唯一的电子邮件地址和社交登录提供者创建新用户帐户时, RepositoryUserService类的registerNewUserAccount(RegistrationForm userAccountData)方法能够按预期工作。

该单元测试的源代码如下所示:

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 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);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. 我们需要验证我们的服务方法没有为使用社交登录提供商的用户创建编码密码。

编写完这些单元测试后,测试类的源代码如下所示:

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 = "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_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();assertThat(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);assertThat(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);}
}

编写只测试一个逻辑概念的单元测试的明显好处是,很容易知道为什么测试失败。 但是,此方法还有其他两个好处:

  • 指定期望的行为很容易。 这意味着更容易为我们的测试方法找出好名字。
  • 因为这些单元测试比原始单元测试要短得多,所以更容易弄清测试方法/组件的要求。 这有助于我们将测试转换为可执行规范。

让我们继续并总结从这篇博客文章中学到的知识。

摘要

现在,我们已经成功地将单元测试分为四个较小的单元测试,它们测试了一个逻辑概念。 这篇博客文章教会了我们两件事:

  • 我们了解到,通过确定测试失败的情况,我们可以确定单个单元测试所涵盖的逻辑概念。
  • 我们了解到,编写仅测试一个逻辑概念的单元测试有助于我们将测试用例编写成可执行的规范,从而确定测试方法/组件的要求。

翻译自: https://www.javacodegeeks.com/2014/06/writing-clean-tests-divide-and-conquer.html

分而治之

分而治之_编写干净的测试–分而治之相关推荐

  1. 编写干净的测试–分而治之

    好的单元测试应该仅出于一个原因而失败. 这意味着适当的单元测试仅测试一个逻辑概念. 如果我们要编写干净的测试,则必须识别这些逻辑概念,并且每个逻辑概念仅编写一个测试用例. 这篇博客文章描述了我们如何识 ...

  2. 怎样编写测试类测试分支_编写干净的测试–天堂中的麻烦

    怎样编写测试类测试分支 如果我们的代码有明显的错误,我们很有动力对其进行改进. 但是,在某些时候,我们认为我们的代码"足够好"并继续前进. 通常,当我们认为改进现有代码的好处小于所 ...

  3. 怎样编写测试类测试分支_编写干净的测试-被认为有害的新内容

    怎样编写测试类测试分支 很难为干净的代码找到一个好的定义,因为我们每个人都有自己的单词clean的定义. 但是,有一个似乎是通用的定义: 简洁的代码易于阅读. 这可能会让您感到有些惊讶,但我认为该定义 ...

  4. 怎样编写测试类测试分支_编写干净的测试–从配置开始

    怎样编写测试类测试分支 很难为干净的代码找到一个好的定义,因为我们每个人都有自己的单词clean的定义. 但是,有一个似乎是通用的定义: 简洁的代码易于阅读. 这可能会让您感到有些惊讶,但我认为该定义 ...

  5. 断言工具的编写_编写干净的测试–用特定领域的语言替换断言

    断言工具的编写 很难为干净的代码找到一个好的定义,因为我们每个人都有自己的单词clean的定义. 但是,有一个似乎是通用的定义: 简洁的代码易于阅读. 这可能会让您感到有些惊讶,但我认为该定义也适用于 ...

  6. c++返回指针时候注意提防_编写干净的测试–提防魔术

    c++返回指针时候注意提防 很难为干净的代码找到一个好的定义,因为我们每个人都有自己的单词clean的定义. 但是,有一个似乎是通用的定义: 简洁的代码易于阅读. 这可能会让您感到有些惊讶,但我认为该 ...

  7. 编写干净的测试–天堂中的麻烦

    如果我们的代码有明显的错误,我们很有动力进行改进. 但是,在某些时候,我们认为我们的代码"足够好"并继续前进. 通常,当我们认为改进现有代码的好处小于所需的工作时,就会发生这种情况 ...

  8. 编写干净的测试-被认为有害的新内容

    很难为干净的代码找到一个好的定义,因为我们每个人都有自己的单词clean的定义. 但是,有一个似乎是通用的定义: 干净的代码易于阅读. 这可能会让您感到有些惊讶,但是我认为该定义也适用于测试代码. 使 ...

  9. 编写干净的测试–从配置开始

    很难为干净的代码找到一个好的定义,因为我们每个人都有自己的单词clean的定义. 但是,有一个似乎是通用的定义: 干净的代码易于阅读. 这可能会让您感到有些惊讶,但是我认为该定义也适用于测试代码. 使 ...

最新文章

  1. C编译器、链接器、加载器详解
  2. jedis取存 数据库查到的对象_Mysql数据库索引BAT面试必问
  3. dqn在训练过程中loss越来越大_[动手学强化学习] 2.DQN解决CartPole-v0问题
  4. WinForm 数据库无限填充树目录 treeView
  5. 使用Python+turtle绘制同心圆
  6. Business Connectivity Services 团队博客简介
  7. java web filter 入口_springboot 通过@WebFilter(urlPatterns )配置Filter过滤路径
  8. Asp.net 中 IHttpHandlerFactory接口 对应web.config 中的节点
  9. 全网首发:跟踪分析This parser does not support specification “null“ version “null“
  10. java python混合编程_python+java混合编程
  11. linux ac97声卡驱动下载,AC97声卡如何在Linux操作系统中进行驱动
  12. 什么软件可以查手机卡的imsi_怎么查看手机的IMSI?
  13. 氚云后台代码-创建、更新子表以及发送消息
  14. [离散数学]命题逻辑P_2:命题联结词
  15. iSCSI网络SCSI接口
  16. python实现千牛客服自动回复语_千牛自动回复语大全
  17. 无人驾驶车辆纵向速度PID控制
  18. ISP(图像信号处理)学习笔记-DPC坏点校正
  19. forestploter包,超赞的森林图绘制新R包
  20. 史上最详细LRW数据集、LRW-1000数据集、LRS2数据集、LRS3-TED数据集、OuluVS2数据集介绍

热门文章

  1. 【做题记录】 [JLOI2011]不等式组
  2. 【线段树】蝴蝶与花(P6859)
  3. 【线段树】扇形面积并(P3997)
  4. 【结论】立体井字棋(jzoj 2124)
  5. 9、play中缓存的使用
  6. 汇编语言(十二)之统计小于平均数的个数
  7. 扫盲,为什么分布式一定要有Redis?
  8. 这些代码优化的方法,你都用过吗
  9. 深入并发包-ConcurrentHashMap
  10. vue实现下拉列表远程搜索示例(根据关键词模糊搜索)