在这篇文章中,您将看到一个使用构建器模式来帮助对具有多个依赖项的服务进行单元测试的示例

您是否曾经需要对具有六个(或更多)依赖项的服务或外观进行单元测试?我会说——依赖注入很棒,但是测试具有太多依赖项的服务肯定会导致程序集的痛苦。让我给你举个例子。

public class UserFacade
{private readonly IApiService _apiService;private readonly IAuthService _authService;private readonly IDatabaseService _databaseService;private readonly IEmailService _emailService;private readonly IFileService _fileService;private readonly ISessionService _sessionService;public UserFacade(IApiService apiService,IAuthService authService,IDatabaseService databaseService,IEmailService emailService,IFileService fileService,ISessionService sessionService){_apiService = apiService;_authService = authService;_databaseService = databaseService;_emailService = emailService;_fileService = fileService;_sessionService = sessionService;}public void SignUp(string email, string password){var authUser = _authService.CreateUser(email, password);if (authUser == null)throw new Exception("Unable to create auth user");_apiService.CreateUserInApi(authUser);var welcomeEmailContents = _fileService.ReadFile("template.html");_emailService.SendEmail(email, "Welcome!", welcomeEmailContents);}public string SignIn(string email, string password){var authUser = _authService.GetUser(email, password);if (authUser == null)throw new UnauthorizedAccessException("User does not exist.");_databaseService.RecordLogin(authUser);return _sessionService.CreateSession(authUser);}
}

在这里,我们有一个相当简单的外观,允许用户注册或登录。它有六个依赖项,虽然不是微不足道的,但与您可能在野外看到的相比,这个数字并不大。现在,我们将如何模拟这些依赖项以便为SignUp和SignIn方法编写单元测试?

在线框级别,这可能看起来像这样:

public class UserFacadeTests
{private UserFacade _sut;[SetUp]public void Setup(){// Create mocksvar apiServiceMock = new Mock<IApiService>();var authServiceMock = new Mock<IAuthService>();var databaseServiceMock = new Mock<IDatabaseService>();var emailServiceMock = new Mock<IEmailService>();var fileServiceMock = new Mock<IFileService>();var sessionServiceMock = new Mock<ISessionService>();// Setup service under test_sut = new UserFacade(apiServiceMock.Object,authServiceMock.Object,databaseServiceMock.Object,emailServiceMock.Object,fileServiceMock.Object,sessionServiceMock.Object);}[Test]public void Test1(){Assert.Pass();}[Test]public void Test2(){Assert.Pass();}
}

如果这对您来说完全陌生,让我们快速回顾一些基础知识。

什么是模拟?

模拟是接口或抽象类的动态实现,您可以将它们传递到您的服务中进行测试。在这个例子中,我使用了一个名为Moq的模拟库。虽然此处未显示,但Moq允许您为接口上的属性和方法定义回调和返回值,这有助于验证被测试服务的行为。

这是什么业务?

sut仅表示被测服务(或系统)。由于您的测试类应该只测试一项服务,因此常见的约定是命名该服务sut(或_sut)以将其标识为正在测试中。

所以有什么问题?

首先,您可能有多个用于显着大小的门面的测试类——可能每个方法或不同行为有一个测试类。这意味着如果构造函数签名发生变化,您将有几个地方需要更新。这很烦人。

但其次,请注意并非所有依赖项都用于每个操作。该SignUp方法调用_authService.CreateUser,_apiService.CreateUserInApi,_fileService.ReadFile和_emailService.SendEmail。该SignIn方法,在另一方面,调用_authService.GetUser,_databaseService.RecordLogin和_sessionService.CreateSession。这意味着如果我的测试类只测试这两种方法之一,我实际上不需要提供所有服务依赖项。不过,进一步想象一下,如果我们有两个测试用例——一个用于当_authService.GetUser返回值,另一个用于返回值null(模拟未找到用户)。为了支持这一点,我们需要为每个测试用例创建一个全新的_sut实例,这意味着每个测试都有大量的样板代码。

这是我的意思的快速说明。

       [Test]public void Test_SignIn_UserFound(){_authServiceMock.Setup(m => m.GetUser(It.IsAny<string>(), It.IsAny<string>())).Returns(new User(){// ...});var sut = new UserFacade(_apiServiceMock.Object, _authServiceMock.Object, _databaseServiceMock.Object,_emailServiceMock.Object, _fileServiceMock.Object, _sessionServiceMock.Object);var sessionId = sut.SignIn("john.doe@gmail.com", "password");Assert.IsNotNull(sessionId);}[Test]public void Test_SignIn_UserNotFound(){_authServiceMock.Setup(m => m.GetUser(It.IsAny<string>(), It.IsAny<string>())).Returns(null as User);var sut = new UserFacade(_apiServiceMock.Object, _authServiceMock.Object, _databaseServiceMock.Object,_emailServiceMock.Object, _fileServiceMock.Object, _sessionServiceMock.Object);var sessionId = sut.SignIn("john.doe@gmail.com", "password");Assert.IsNull(sessionId);}

请记住,这是一个人为的示例,仅包含一个模拟服务。在实际测试中,您可能需要模拟多个服务,不同的测试用例可能具有不同的行为。简而言之,测试很快就会变得非常丑陋。

好吧,爱因斯坦,你有什么建议?

首先,让我们添加一种通用的方式来动态存储依赖项。

    public class DependencyManager{private readonly Dictionary<Type, object> _dependencies = new Dictionary<Type, object>();public void AddDependency<T>(T dependency)where T : class{_dependencies[typeof(T)] = dependency;}public T GetDependency<T>()where T : class{if (_dependencies.ContainsKey(typeof(T))){return _dependencies[typeof(T)] as T;}return null;}}

我通常有一个Shared测试项目,它在一个规模很大的解决方案中的所有其他测试项目之间共享,我会在那里放置这样的东西。思路是,利用这个,我们可以在我们的测试Setup阶段注册一些常见的依赖,然后在测试用例中注册一些测试用例特定的依赖。可以多次注册相同的类型而不会出现问题,并且检索尚未设置的类型的依赖项将返回null而不是产生错误。这对于测试不需要特定依赖项的代码路径很有用,实际上可以是null。

接下来,让我们添加一种方法来逐步构建被测服务的实例(在本例中为UserFacade)。

public class UserFacadeBuilder
{private readonly  DependencyManager _dependencyManager = new DependencyManager();public UserFacadeBuilder AddDependency<T>(T value)where T : class{_dependencyManager.AddDependency(value);return this;}public UserFacade Build(){return new UserFacade(_dependencyManager.GetDependency<IApiService>(),_dependencyManager.GetDependency<IAuthService>(),_dependencyManager.GetDependency<IDatabaseService>(),_dependencyManager.GetDependency<IEmailService>(),_dependencyManager.GetDependency<IFileService>(),_dependencyManager.GetDependency<ISessionService>());}
}

该组件有两个目的。首先,它将被测服务的更新封装在一个位置,这样如果构造函数签名发生变化,我们只需要在一个地方进行更改。其次,它提供了一个流畅的API来注册被测服务的依赖关系。让我们看看我们的测试类现在的样子。

public class UserFacadeTests
{private UserFacadeBuilder _sutBuilder;[SetUp]public void Setup(){// Create common mocksvar apiServiceMock = new Mock<IApiService>();var databaseServiceMock = new Mock<IDatabaseService>();var emailServiceMock = new Mock<IEmailService>();var fileServiceMock = new Mock<IFileService>();var sessionServiceMock = new Mock<ISessionService>();_sutBuilder = new UserFacadeBuilder().AddDependency(apiServiceMock.Object).AddDependency(databaseServiceMock.Object).AddDependency(emailServiceMock.Object).AddDependency(fileServiceMock.Object).AddDependency(sessionServiceMock.Object);}[Test]public void Test_SignIn_UserFound(){var authServiceMock = new Mock<IAuthService>();authServiceMock.Setup(m => m.GetUser(It.IsAny<string>(), It.IsAny<string>())).Returns(new User(){// ...});var sut = _sutBuilder.AddDependency(authServiceMock.Object).Build();var sessionId = sut.SignIn("john.doe@gmail.com", "password");Assert.IsNotNull(sessionId);}[Test]public void Test_SignIn_UserNotFound(){var authServiceMock = new Mock<IAuthService>();authServiceMock.Setup(m => m.GetUser(It.IsAny<string>(), It.IsAny<string>())).Returns(null as User);var sut = _sutBuilder.AddDependency(authServiceMock.Object).Build();var sessionId = sut.SignIn("john.doe@gmail.com", "password");Assert.IsNull(sessionId);}
}

现在,每个测试用例只注册一个特定于测试的自定义模拟,而不是每次都必须注册所有模拟。我们还可以省略测试所关注的代码路径不需要的依赖项。例如,由于我们只测试SignIn方法,IEmailService,IApiService和IFileService可以安全地从建设者省略。

我发现这种模式非常有助于对实际大小的服务进行测试,并具有可管理的实际依赖项数量。

https://www.codeproject.com/Articles/5309312/Using-the-Builder-Pattern-to-Help-Your-Unit-Tests

使用构建器模式来帮助您的单元测试相关推荐

  1. 11Builder(构建器)模式

    技术交流QQ群:1027579432,欢迎你的加入! 1.Builder(构建器)模式动机 在软件系统中,有时候面临着一个复杂对象的创建工作,其通常由各个部分的子对象用一定的算法构成.由于需求的变化, ...

  2. 构建器设计模式_创新设计模式:构建器模式

    构建器设计模式 以前我们看过工厂和抽象工厂模式. 这些模式可以达到目的,并且确实有用,但是在某些用例中,我们必须创建一个非常复杂的对象,并且创建它需要不同的步骤,每个步骤都需要不同的操作. 在这种情况 ...

  3. 构建器模式_我喜欢构建器模式的三个原因

    构建器模式 有三种方法可以用Java编程语言创建新对象: 伸缩构造函数(反)模式 Javabeans模式 建造者模式 与其他两种方法相比,我更喜欢使用构建器模式. 为什么? Joshua Bloch描 ...

  4. java 构建者模式_Java方法中的参数太多,第3部分:构建器模式

    java 构建者模式 在我的前两篇文章中,我研究了如何通过自定义类型和参数对象减少构造函数或方法调用所需的参数数量. 在本文中,我将讨论如何使用构建器模式来减少构造器所需的参数数量,并讨论该模式如何甚 ...

  5. fusion构建器代码语法_构建器模式:适用于代码,适用于测试

    fusion构建器代码语法 我发现构建器设计模式偶尔在代码中有用,但在测试中经常有用. 本文简要概述了该模式,然后介绍了在测试中使用该模式的一个有效示例. 请参阅github中的代码. 生成器模式的背 ...

  6. 我喜欢构建器模式的三个原因

    有三种方法可以用Java编程语言创建新对象: 伸缩构造函数(反)模式 Javabeans模式 建造者模式 与其他两种方法相比,我更喜欢使用构建器模式. 为什么? Joshua Bloch描述了构建器模 ...

  7. Java方法中的参数太多,第3部分:构建器模式

    在我的前两篇文章中,我研究了如何通过自定义类型和参数对象减少构造函数或方法调用所需的参数数量. 在本文中,我将讨论如何使用构建器模式来减少构造器所需的参数数量,并讨论该模式如何甚至可以帮助采用过多参数 ...

  8. 构建器模式:适用于代码,适用于测试

    我发现生成器设计模式偶尔在代码中有用,但在测试中经常有用. 本文简要概述了该模式,然后介绍了在测试中使用该模式的一个有效示例. 请参阅github中的代码. 生成器模式的背景 根据GoF的书 ,构建器 ...

  9. Effective Java(一)———— 代替构造器和Setter的构建器模式

    引言 Java语言中的一部经典著作<Effective Java>,里面涵盖了78条我们应该熟练的Java编程技巧. 本篇博客是该书学习的系列笔记第一篇.本系列博客不会与书中的78条建议完 ...

最新文章

  1. 流媒体技术学习笔记之(十八)Ubuntu 16.04.3 如何编译 FFmpeg 记录
  2. 题目梳理(一)(2019.07.06~2019.07.20)
  3. 异常空格,ASCII (194,160)问题
  4. GDCM:gdcm::EncapsulatedDocument的测试程序
  5. C++11遍历map
  6. 前端学习(2116):为什么组件data必须是函数
  7. c语言 整数转二进制取位,C语言位运算--将整数转换成二进制串以及反转整数后N位...
  8. 一体打印机行业调研报告 - 市场现状分析与发展前景预测(2021-2027年)
  9. android 禁止软键盘弹出自动弹出,Android屏蔽软键盘自动弹出的解决方案
  10. 地理大数据下载网址推荐
  11. 创新工场和海豚浏览器宣讲会启示
  12. 语雀文章导入CSDN
  13. 智能AI短视频搬运剪辑小程序,可过原创检测
  14. Why That Big Meal You Just Ate Made You Hungry
  15. 行稳致远,进而有为——2020年 XAG发展展望
  16. AJAX_入门经典案例
  17. 软构习题课一内容总结
  18. 考考眼力,找下代码的bug
  19. 基于php二手书交易系统,二手书交易系统论文.doc
  20. Windows 7中如何安装卸载IE11或重装IE10

热门文章

  1. markdown 流程图js_在Markdown中用mermaid语法绘制图表
  2. ros发布节点信息python_ROS入门笔记(一): ROS简介
  3. php中 判断表中是否有重复,PHP:最常见的表中回显重复项的数量
  4. python数据包头_Python爬虫-请求响应包头
  5. python第三方库引用_Python入门:如何使用第三方库
  6. mysql 5.6.13-winx64_MySQL-5.6.13 zip解压版的安装与配置教程
  7. 什么?你的电商网页不够时尚?看这里
  8. MongoDB 教程 | 菜鸟教程
  9. 模式搜索的KMP算法详解与C语言代码实现
  10. C语言线性表之顺序表