领域驱动设计之单元测试最佳实践

发表于:2018-7-26 10:21

字体: | 上一篇 | 下一篇 | 我要投稿

 作者:richiezhang    来源:博客园

  一直以来,我试图找到一种有效的单元测试模式,使得“单元测试”真正能够在团队中流行起来,让单元测试不再是走过场,而是让单元测试切切实实成为提高代码质量的途径。
  本文将描述一种以EF Code First模式实现的领域驱动项目实施单元测试的方案。
  在描述这一方案之前,让我们看看这一最佳实践源于何种考虑和最终实现的目标:
  1、以MVC项目为例,如果将单元测试的重心放在如何测试一个Controller或Action将收效甚微,原因有二:
  从原则上讲Controller中不包含业务逻辑,理论上大部分代码都是ViewModel和DTO之间的赋值或者Service的调用,对这样的代码编写单元测试收效甚微,性价比极低。
  Controller的代码对UI的依赖度很高,也就意味着Controller的代码不够稳定,这将迫使单元测试的变化频率过高,容易给开发人员造成单元测试是一种负担的心理。
  基于这样的原因,我将不建议人手紧张的团队对Controller编写单元测试。
  2、一个软件项目真正需要测试的重心是业务逻辑,对一个领域驱动项目来说,领域逻辑才是重心。但是我们知道领域逻辑离不开数据的支撑,也就是说我们需要跟Repository打交道。
  对于这样的一个测试场景,大多数教程会提示你Mock Repository,从单元测试的角度来讲,这样的方案无疑是正确的,但是这样的方案存在两个问题:
  实际经验告诉我们这样的测试不能真实的反应出代码的问题,甚至出现单元测试是通过的,可是Debug起来却有问题。原因在于我们忽略了数据库部分,这一部分逻辑处于失控状态。
  需要Mock的数据太多,有时候为了测试一个逻辑,Mock的代码比测试还要多,给开发人员造成单元测试其实就是在玩Mock的错误认识。
  所以我心目中理想的单元测试应该具备以下条件:
  测试从Service->Repository->Domain一条线测试完毕,测试能够准确反应出代码是如何运行的。所以准确来讲我这个方案应该叫“领域驱动设计之集成测试”。
  尽量不Mock,包括读取数据库部分。
  测试需要的数据应该是可复用的,对测试“注册用户”、“搜索用户”这样的业务逻辑应该能够复用测试所提供的数据。
  任何测试都可以独立运行,同一个测试多次执行的效果应该是一致的,测试的执行速度尽可能快。
  为了能够尽可能的贴近这一目标,我实现了一个很简单的DDD案例用来做测试用,这一案例描述了两个重要的领域模型:User领域模型描述了“注册用户”、“更改密码”、“登录”等逻辑;BookManageProcess领域模型描述了“借书”、“归还图书”等逻辑,你可以理解为这是一个图书馆借书及还书的模型。
  为了能够理解此测试方案,我将对该测试案例做一个简单描述:
  该案例基于EF Code First和Castle实现的一个DDD案例,这一测试方案也是为DDD量身定制,并不适合于传统的三层架构。
  为什么说这一案例是一个领域驱动案例?
  以“用户注册”这一功能为例,我们来分析一下:
  1、从UserService这一入口来看:
  public class UserService : ApplicationService, IUserService
  {
  private readonly IUserRepository _userRepository;
  private readonly IEmailUniqueChecker _emailUniqueChecker;
  public UserService(IRepositoryContext context, IUserRepository userRepository,IEmailUniqueChecker emailUniqueChecker)
  : base(context)
  {
  _userRepository = userRepository;
  _emailUniqueChecker = emailUniqueChecker;
  }
  public Guid Register(UserModel userModel)
  {
  var user = User.Register(userModel,_emailUniqueChecker);
  _userRepository.Add(user);
  Context.Commit();
  return user.Id;
  }
  }
  Register()方法中几乎只是对领域模型User.Register()方法的调用,其余的代码都可以忽略不计,这说明了这样一个事实:Service层没有任何业务逻辑,所有的逻辑都应该在Domain。
  2、User领域模型中Register()方法的实现:
  public partial class User
  {
  public static User Register(UserModel userModel, IEmailUniqueChecker emailUniqueChecker)
  {
  Contract.Requires(!userModel.Name.IsNullOrEmpty(), "invalid username");
  if (emailUniqueChecker.IsExist(userModel.Email))
  {
  throw new DuplicateEmailException("email already exist, please input another one");
  }
  var password=new Password(userModel.Password);
  var user = new User()
  {
  Id = Guid.NewGuid(),
  Name = userModel.Name,
  Password = password.HashedPassword,
  Salt = password.Salt,
  Email = userModel.Email,
  RegisterDateTime = DateTime.Now,
  LastLoginDateTime = DateTime.Now
  };
  return user;
  }
  }


 上文内容不用于商业目的,如涉及知识产权问题,请权利人联系博为峰小编(021-64471599-8017),我们将立即处理。
21/212>
《2023软件测试行业现状调查报告》独家发布~

关注51Testing

联系我们

快捷面板 站点地图 联系我们 广告服务 关于我们 站长统计 发展历程

法律顾问:上海兰迪律师事务所 项棋律师
版权所有 上海博为峰软件技术股份有限公司 Copyright©51testing.com 2003-2024
投诉及意见反馈:webmaster@51testing.com; 业务联系:service@51testing.com 021-64471599-8017

沪ICP备05003035号

沪公网安备 31010102002173号