领域驱动设计之单元测试最佳实践(二)
领域驱动设计之单元测试最佳实践(一)
介绍完了DDD案例,我们终于可以进入主题了,本方案的测试代码基于Xunit编写,断言组件采用了FluentAssertions,类似的组件还有Shouldly。另外本案例使用了Code Contracts for .NET,如果不安装此插件,可能有个别测试不能正确Pass。
为了实现目标中的第二点:"尽量不Mock,包括数据库读取部分”,我尝试过3种方案:
1、测试代码连接真实数据库,只需要将测试数据库配置到测试项目中的web.config中,即可达到这一目标。但是该方案毕竟存在很多缺点,如:需要将测试库和正式库的更改保持同步,单元测试不利于集成在CI中,不利于团队协作等。
2、使用SQL Lite,但是由于SQL lite本身不支持一些Linq表达式如:Skip,另外还有一些功能也无法跟Sql server保持一致,最终放弃该方案。
3、使用测试组件Effort,可以很好的配合Entity framework使用,由于Effort内部使用了关系型内存数据库nmemory,所以非常适合运行单元测试。
当然我还是非常期待微软能够编写基于EF的单元测试组件。
我在《我眼中的领域驱动设计》一文中提到:不要使用数据库独有的技术,如存储过程和触发器等。一方面这些逻辑都应该是Domain逻辑,另一方面一旦使用了这些技术也就意味着我们无法为这些逻辑编写测试。
一、使用Effort
为了能够在Castle中使用基于Effort的DbContext,需要在Castle中注册Effort:
public class FakeDbContextInstaller:IWindsorInstaller{public const string DbConnectionKey = "FakeDbConnection";public const string FakeBookLibraryDbContextKey = "FakeBookLibraryDbContext";public void Install(IWindsorContainer container, IConfigurationStore store){container.Register(Component.For<DbConnection>().UsingFactoryMethod(DbConnectionFactory.CreateTransient).Named(DbConnectionKey).LifestylePerWebRequest());container.Register(Component.For<BookLibraryDbContext>().DependsOn(Dependency.OnComponent(typeof(DbConnection), DbConnectionKey)).Named(FakeBookLibraryDbContextKey).LifestylePerWebRequest().IsDefault());}}
二、为测试编写场景
为了复用测试数据,我们需要编写场景(Scenario),下面的文件组织结构描述了这一意图:
以用户注册为例,设计RegisterUserScenario:
public class RegisterUserScenario : ScenarioBase{public UserModel GivingModel { get; set; }public Guid Id { get; private set; }public RegisterUserScenario(IWindsorContainer container):base(container){GivingModel = new UserModel(){Name = "Lilei",Password = "Password1",Email = "lilei@google.com",};}public override void Execute(){var userService = Container.Resolve<IUserService>();Id = userService.Register(GivingModel);}}
场景总是提供了正确的数据,执行这样的场景总是能够得到正确的结果:
[Fact]public void When_RegisterUserWithValidData_Should_CreateUser(){//Arrangevar scenario=new RegisterUserScenario(Container);//Actscenario.Execute();//Assertvar user = UserService.GetUser(scenario.Id);user.Name.Should().Be(scenario.GivingModel.Name);user.Email.Should().Be(scenario.GivingModel.Email);}
测试的方法名很重要,我们在读完这个方法名之后就知道该测试是在干嘛。
为了得到失败的结果,我们需要重写Scenario中的数据,比如下面的测试:
[Fact]public void When_RegisterUserWithEmptyName_Should_ThrowException(){//Arrangevar scenario=new RegisterUserScenario(Container){GivingModel = new UserModel(){Name = string.Empty,Email = "lilei@google.com",Password = "Password1"}};//Actscenario.Invoking(s => s.Execute()).ShouldThrow<Exception>("invalid username");}
三、基于之前的场景编写新的场景,从而达到复用数据的目的
例如我们需要编写“用户登录”的测试,首先需要编写LoginScenario
public class LoginScenario:ScenarioBase{public string Email { get; set; }public string Password { get; set; }public bool Login { get; private set; }public Guid Id { get; private set; }public LoginScenario(IWindsorContainer container) : base(container){var registerScenario=new RegisterUserScenario(container);registerScenario.Execute();Id = registerScenario.Id;Email = registerScenario.GivingModel.Email;Password = registerScenario.GivingModel.Password;}public override void Execute(){var userService = Container.Resolve<IUserService>();Login=userService.Login(Email, Password);}}
在这个场景的构造函数中我们又执行了RegisterScenario,从而达到重复利用数据的目的。
为“用户登录”编写测试:
public class UserLoginTests:TestBase{[Fact]public void When_LoginWithInexistentEmail_Should_ThrowException(){//Arrangevar loginScenario=new LoginScenario(Container){Email = "other@google.com",};//ActloginScenario.Invoking(s => s.Execute()).ShouldThrow<ApplicationServiceException>("no such user");}[Fact]public void When_LoginWithWrongPassword_Should_ReturnFalse(){//Arrangevar loginScenario=new LoginScenario(Container){Password = "wrongPassword"};//ActloginScenario.Execute();//AssertloginScenario.Login.Should().BeFalse();} [Fact]public void When_LoginWithCorrectPassword_Should_ReturnTrue(){//Arrangevar loginScenario = new LoginScenario(Container);//ActloginScenario.Execute();//AssertloginScenario.Login.Should().BeTrue();}}
我们总是需要为新的业务逻辑编写新的场景,而新的场景总是基于之前编写好的场景,整个系统的任何功能都可以用真实的测试代码来覆盖。
由于我们在测试基类中为每个测试都开启了单独的scope,每一个测试结束都会dispose数据库。所以每一个测试无论运行多少遍都是相同的效果。缺点是这些测试不能并行运行,XUnit默认以不同的测试类为单位并行运行,我们通过在测试类上添加相同的[Collection("IntegrationTests")]标签,从而禁用XUnit的并行运行能力。
采用该方案覆盖完毕单元测试的系统,开发者每次提交代码并保证所有单元测是都是“passed”,开发者每一次代码提交都会信心满满。
高质量的单元测试不但能够确保系统的平稳运行,更是一种有效的文档,当你读完每一个场景的测试用例,你基本就能够对该业务非常熟悉了。
接近真实的单元测试还可以省去你Debug的时间,只要你编写的测试通过,基本就可以确保后台代码的可靠性。另外你可以在任何时候从这些测试代码中Debug进去,相比从前端界面Debug代码能够节省不少时间,一劳永逸。
更多具体细节请查看源码:https://git.oschina.net/richieyangs/BookLibrary.git
转载于:https://www.cnblogs.com/richieyang/p/5451604.html
领域驱动设计之单元测试最佳实践(二)相关推荐
- DDD(Domain Driven Design) 领域驱动设计从理论到实践 四
- 接上 SOA 架构 面向服务架构(Service Oriented Architecture,SOA)对于不同的人来说意思不同.这里梳理一下SOA原则: 服务契约 : 通过契约文档,服 ...
- 京东研发团队 - 领域驱动设计(DDD)实践
过去几年,通天塔一直处于快速的业务能力建设和架构完善的阶段,以应对不断增长的业务需求和容量.高可用等技术需求,现在通天塔平台已经能满足集团主站的大部分活动.频道搭建和运营能力,主流程的新需求越来越少, ...
- 京东平台研发:领域驱动设计(DDD)实践总结
点击上方"芋道源码",选择"设为星标" 管她前浪,还是后浪? 能浪的浪,才是好浪! 每天 8:55 更新文章,每天掉亿点点头发... 源码精品专栏 原创 | J ...
- 领域驱动设计--京东研发团队实践(六)
过去几年,通天塔一直处于快速的业务能力建设和架构完善的阶段,以应对不断增长的业务需求和容量.高可用等技术需求,现在通天塔平台已经能满足集团主站的大部分活动.频道搭建和运营能力,主流程的新需求越来越少, ...
- DDD(领域驱动设计)系列主题:领域驱动设计(DDD)实践
过去几年,通天塔一直处于快速的业务能力建设和架构完善的阶段,以应对不断增长的业务需求和容量.高可用等技术需求,现在通天塔平台已经能满足集团主站的大部分活动.频道搭建和运营能力,主流程的新需求越来越少, ...
- 领域驱动设计--领域驱动设计到数据建模实践(十)
----- 学习笔记 ----- 过去,系统的软件设计是以数据库设计为核心,当需求确定下来以后,团队首先开始进行数据库设计.因为数据库是各个模块唯一的接口,当整个团队将数据库设计确定下来以后,就可以按 ...
- nvme通用驱动_对领域驱动设计的理解与实践
领域驱动设计(Domain-Driven-Design)是一种针对大型复杂系统的领域建模与分析方法论. 2003 年,Eric Evans 发布<Domain-Driven Design: Ta ...
- 对领域驱动设计的理解与实践
前言 领域驱动设计(Domain-Driven-Design)是一种针对大型复杂系统的领域建模与分析方法论.2003 年,Eric Evans 发布<Domain-Driven Design: ...
- Re:从零开始的领域驱动设计
前言 认识领域驱动设计的意义 模式 smart ui是个反模式 模式 Entity与Value Object 模式 Repository 模式 聚合和聚合根 模式 包结构 其他模式 微服务之于领域驱动 ...
最新文章
- Ubuntu 调节屏幕亮度
- 羊车门问题python_python编程羊车门问题代码示例
- WPF的悬停工具栏实现方案
- python接口自动化21-下载excel文件(Content-Type:octets/stream)
- CEF使用的几个注意点
- Vmware虚拟机集群设置静态ip
- 范凯:对移动社交型app的一点思考
- 在线js调试工具JSbin、jsFiddle
- c语言 输入5为学生成绩求最高,求C语言小程序 输入5个学生成绩 输出最高成绩和其对应序号(0~5)若有并列则输出并列...
- Linux网络下载管理工具(lftp, ftp, lftpget, wget)
- MPQ文档布局分析[转帖]
- 基于avr atmega16单片机定时器的 pwm调宽调占空比以及调频率
- 平面设计师需要掌握哪些印刷知识
- java个人所得税if语句_java_计算个人所得税
- 【b站雅思笔记】Simon‘s IELTS Course - 听力部分
- Maven 上手指南
- 删除flash助手推荐广告
- 模块递归拆分法: 设计模式 设计原则,复杂层次设计举例。系统重构 装饰模式,门面模式,代理模式
- 终于找到了一款好用的录屏软件了
- java设计模式都有哪些?
热门文章
- 便携式办公套件LibreOffice Portable 4.0.1
- shell统计游戏活跃用户数之改进过程分析
- Oracle Net Services 配置:Xlib: connection to :0.0 refused by server
- 懒不是傻懒,如果你想少干,就要想出懒的方法。要懒出风格,懒出境界。
- C++11 处理时间和日期的处理,以及chrono库介绍
- 10许可证即将到期_食品经营许可证延续
- java 系列1:数据类型转换
- Uniswap V3 24小时交易量达12.9亿美元
- SAP License:GL显示行项目
- SAP License:谁能透露你的信息