使用IdleTest进行TDD单元测试驱动开发演练(2)

时间:2022-07-05 22:58:42

【前言】

1. 有关上篇请参见《使用IdleTest进行TDD单元测试驱动开发演练(1)》,有关本篇用到Entity Framework Code First请参见《使用NuGet助您玩转代码生成数据————Entity Framework 之 Code First》,而用的个人类库参照IdleTest
2. 本文只用了简单的Entity Framework演练单元测试,着重于Testing,而不是实现,并不会涉及事务、效率等问题。

3. 回顾上一篇里面讲到的是针对业务层的测试,正如敏捷中厉行的多与用户沟通,在书《C# 测试驱动开发(Professional Test Driven Development with C#)》中作者就推荐TDD中单元测试的编写应有业务人员与需求人员参与,不是参与编码,而是参与单元测试的用例制定,当然了不涉及业务层面的代码也不需要如此。比如注册功能有多少种场景都可以在单元测试中体现出来,这时就要针对每种场景编写至少一个单元测试的方法,其命名也就尤为重要,因为要让他们看懂每个方法对应什么样的场景。以下就是我改造后的对UserService进行测试的代码,其中每个类对应一个功能模块,类中的每个方法则对应该功能的每一种场景,这样以便于与需求以及相关业务人员确定开发需求后再编码,减少了开发中的需求变更。

    public abstract class BaseUserServiceTest
{
protected UserTestHelper UserTestHelper = new UserTestHelper(); private IUserRepository userRepository; protected IList<UserModel> ExistedUsers; protected abstract IUserService UserService
{
get;
} /// <summary>
/// IUserRepository模拟对象
/// </summary>
public virtual IUserRepository UserRepository
{
get
{
if (this.userRepository == null)
{
StubIUserRepository stubUserRepository = new StubIUserRepository();
//模拟Get方法
stubUserRepository.GetExpressionOfFuncOfUserModelBooleanFuncOfIQueryableOfUserModelIOrderedQueryableOfUserModelString
= (x, y, z) =>
{
return this.ExistedUsers.Where<UserModel>(x.Compile());
}; //模拟GetSingle方法
stubUserRepository.GetSingleString = p => this.ExistedUsers.FirstOrDefault<UserModel>(o => o.LoginName == p); //模拟Insert方法
stubUserRepository.InsertUserModel = (p) => this.ExistedUsers.Add(p); this.userRepository = stubUserRepository;
} return this.userRepository;
}
} [TestInitialize]
public void InitUserList()
{
//每次测试前都初始化
this.ExistedUsers = new List<UserModel> { UserTestHelper.ExistedUser };
} #region Login Test
[TestCategory("登陆场景")]
public virtual void 当用户信息全部为空或账户为空或密码为空或账户错误或密码错误或账户密码均错误都登陆失败()
{
//验证登陆失败的场景
AssertCommon.AssertBoolean<UserModel>(
new UserModel[] {
null, new UserModel(),
new UserModel { LoginName = string.Empty, Password = UserTestHelper.ExistedPassword }, //账户为空
new UserModel { LoginName = UserTestHelper.ExistedLoginName, Password = string.Empty }, //密码为空
new UserModel {
LoginName = UserTestHelper.ExistedLoginName, Password = UserTestHelper.NotExistedPassword
}, //密码错误
new UserModel {
LoginName = UserTestHelper.NotExistedLoginName, Password = UserTestHelper.NotExistedPassword
}, //账户密码错误
new UserModel {
LoginName = UserTestHelper.NotExistedLoginName, Password = UserTestHelper.ExistedLoginName
} //账户错误
}, false, p => UserService.Login(p));
} [TestCategory("登陆场景")]
public virtual void 当账户密码全部正确时登陆成功()
{
//账户密码正确,验证成功,这里假设正确的账户密码是"zhangsan"、"123456"
UserModel model = new UserModel
{
LoginName = UserTestHelper.ExistedLoginName,
Password = UserTestHelper.ExistedPassword
};
AssertCommon.AssertBoolean(true, UserService.Login(model));
} #endregion #region RegisterTest
[TestCategory("注册场景")]
public virtual void 当用户信息全为空或账户为空或密码为空或账户已存在时注册失败()
{
//验证注册失败的场景
AssertCommon.AssertBoolean<UserModel>(
new UserModel[] {
null, new UserModel(),
new UserModel { LoginName = string.Empty, Password = UserTestHelper.NotExistedPassword }, //账户为空
new UserModel { LoginName = UserTestHelper.NotExistedLoginName, Password = string.Empty }, //密码为空
new UserModel {
LoginName = UserTestHelper.ExistedLoginName, Password = UserTestHelper.NotExistedPassword
}, //账户已存在
}, false, p => UserService.Register(p));
} [TestCategory("注册场景")]
public virtual void 当账号密码均不为空且账号未存在则注册成功并且注册后的用户信息与注册输入的保持完全一致()
{ //验证注册成功的场景
//密码与他人相同也可注册
UserModel register1 = new UserModel { LoginName = "register1", Password = UserTestHelper.ExistedPassword };
UserModel register2 = new UserModel { LoginName = "register2", Password = UserTestHelper.NotExistedPassword };
UserModel register3 = new UserModel
{
LoginName = "register3",
Password = UserTestHelper.NotExistedPassword,
Age =
}; AssertCommon.AssertBoolean<UserModel>(
new UserModel[] { register1, register2, register3 }, true, p => UserService.Register(p)); //获取用户且应与注册的信息保持一致
UserModel actualRegister1 = UserService.GetModel(register1.LoginName);
UserTestHelper.AssertEqual(register1, actualRegister1); UserModel actualRegister2 = UserService.GetModel(register2.LoginName);
UserTestHelper.AssertEqual(register2, actualRegister2); UserModel actualRegister3 = UserService.GetModel(register3.LoginName);
UserTestHelper.AssertEqual(register3, actualRegister3);
}
#endregion //该方法可不需要业务人员参与
public virtual void GetModelTest()
{
AssertCommon.AssertIsNull<string, UserModel>(TestCommon.GetEmptyStrings(), true, p => UserService.GetModel(p));
AssertCommon.AssertIsNull(true, UserService.GetModel(UserTestHelper.NotExistedLoginName)); UserModel actual = UserService.GetModel(UserTestHelper.ExistedLoginName);
UserTestHelper.AssertEqual(UserTestHelper.ExistedUser, actual);
}
}

BaseUserServiceTest

    [TestClass]
public class UserServiceTest : BaseUserServiceTest
{
protected override IUserService UserService
{
get { return new UserService(this.UserRepository); }
} [TestMethod]
public override void GetModelTest()
{
base.GetModelTest();
} [TestMethod]
public override void 当用户信息全部为空或账户为空或密码为空或账户错误或密码错误或账户密码均错误都登陆失败()
{
base.当用户信息全部为空或账户为空或密码为空或账户错误或密码错误或账户密码均错误都登陆失败();
} [TestMethod]
public override void 当账户密码全部正确时登陆成功()
{
base.当账户密码全部正确时登陆成功();
} [TestMethod]
public override void 当用户信息全为空或账户为空或密码为空或账户已存在时注册失败()
{
base.当用户信息全为空或账户为空或密码为空或账户已存在时注册失败();
} [TestMethod]
public override void 当账号密码均不为空且账号未存在则注册成功并且注册后的用户信息与注册输入的保持完全一致()
{
base.当账号密码均不为空且账号未存在则注册成功并且注册后的用户信息与注册输入的保持完全一致();
}
}

UserServiceTest

4. 这里我已经在上一篇的基础上进行了一些重构:

  在解决方案文件夹“Tests”下新建类库项目“IdleTest.TDDEntityFramework.TestUtilities”,并添加引用“IdleTest.dll”、“IdleTest.MSTest.dll” 
(参考上一篇)和“IdleTest.TDDEntityFramework.Models”。接着在项目下添加类“UserTestHelper”。

    public class UserTestHelper
{
public string ExistedLoginName = "zhangsan"; public string ExistedPassword = ""; public string NotExistedLoginName = "zhangsan1"; public string NotExistedPassword = ""; public UserModel ExistedUser
{
get { return new UserModel { LoginName = ExistedLoginName, Password = ExistedPassword }; }
} public UserModel NotExistedUser
{
get {
return new UserModel {
LoginName = NotExistedLoginName, Password = NotExistedPassword, Age = };
}
} public void AssertEqual(UserModel expected, UserModel actual)
{
AssertCommon.AssertIsNull(false, expected);
AssertCommon.AssertIsNull(false, actual); AssertCommon.AssertEqual<string>(expected.LoginName, actual.LoginName);
AssertCommon.AssertEqual<string>(expected.Password, actual.Password);
AssertCommon.AssertEqual<int>(expected.Age, actual.Age);
} }

UserTestHelper

5. 再在项目“IdleTest.TDDEntityFramework.ServiceTest”引用刚添加的项目“IdleTest.TDDEntityFramework.TestUtilities”。

6. 接着生成并运行测试,在测试资源管理器中单击右键,滑动鼠标到“分组依据”后选中“特征”,如下图所示,此时便可以看到比较适合非开发人员的测试方法名。

使用IdleTest进行TDD单元测试驱动开发演练(2)

  使用“[TestCategory]”声明的测试方法可以在测试资源管理器中按照特征来排列。

  我这里为了简便,把一个细分功能只划分为成功与失败两个方法,其实应该还可以划分得更细些,比如账户名为空登陆失败、密码为空登陆失

败分为两个测试方法。当然了,如果在需求并不复杂的情况下,也可以不用这么划分,比如上述的登陆与注册需求就很简单,完全可以不用细化,这
里只作为演示下罢了。

【一】对Repository层做测试前准备

(本篇将使用与上篇类似的方式完成仓储层(Repository)开发)
7. 由于使用Entity Framework Code First,因而需对Model增加一些特性(Attribute)声明。在编写以下代码前需在项

目“IdleTest.TDDEntityFramework.Models”添加引用“System.ComponentModel.DataAnnotations”。

    [Table("UserInfo")]
public class UserModel
{
[Key]
[MaxLength()]
public string LoginName { get; set; } [MaxLength()]
public string Password { get; set; } public int Age { get; set; }
}

UserModel

8. 项目“IdleTest.TDDEntityFramework.Repositories”的变动:添加引用“IdleTest.TDDEntityFramework.Models”;打开程序包管理器控制台,如下图所示在默认项目选择“IdleTest.TDDEntityFramework.Repositories”,并在命令中输入“Install-Package EntityFramework”(PS 现在才发现Entity Framework已经到了6.0了,不过原有功能应该都还在);
使用IdleTest进行TDD单元测试驱动开发演练(2)

在项目下添加类“SqlFileContext”。

    public class SqlFileContext : DbContext
{
public DbSet<UserModel> Users { get; set; } public SqlFileContext() : base("DefaultConnectionString") { }
}

【二】、编写UserRepository的测试UserRepositoryTest

(由于以下的测试不需要业务人员参与,故我又可以按照我喜欢的方式来命名单元测试了)

9. 在解决方案文件夹“Tests”下创建单元测试项目“IdleTest.TDDEntityFramework.RepositoryTest”,添加引用 “IdleTest.TDDEntityFramework.TestUtilities”、“IdleTest.TDDEntityFramework.IRepositories”、“IdleTest.TDDEntityFramework.Models” 和 “IdleTest.TDDEntityFramework.Repositories” 以及 “IdleTest”、“IdleTest.MSTest”(类似上一篇);继续添加“EntityFramework.dll”的引用如下图所示。
使用IdleTest进行TDD单元测试驱动开发演练(2)

10. 对刚添加的“IdleTest.TDDEntityFramework.Repositories”与“EntityFramework”引用“添加Fakes程序集”。

11. 由于 “IdleTest.TDDEntityFramework.IRepositories” 有两个接口 “IUserRepository”、“IRepository”,因而我这里也创建两个对应的测试类“RepositoryTest”、“BaseUserRepositoryTest”。

    public abstract class RepositoryTest<TEntity, TKey> where TEntity : class
{
protected abstract IRepository<TEntity, TKey> Repository { get; } public virtual void GetSingleTest()
{
AssertCommon.AssertIsNull(true, Repository.GetSingle(default(TKey)));
} public virtual void InsertTest()
{
AssertCommon.ThrowException(true, () => Repository.Insert(default(TEntity)));
AssertCommon.ThrowException(true, () => Repository.Insert(null));
}
}

RepositoryTest

12. 限于篇幅,本文只对IRepository的“GetSingle”和“Insert”方法进行测试,其他方法类似,后续完成所有测试再将代码上传至http://idletest.codeplex.com/。

13. 继续编写类 “BaseUserRepositoryTest”,它与上一篇的 “BaseUserServiceTest” 非常相似。

 public abstract class BaseUserRepositoryTest : RepositoryTest<UserModel, string>
{
protected UserTestHelper UserTestHelper = new UserTestHelper(); protected abstract IUserRepository UserRepository { get;} protected IList<UserModel> ExistedUsers; [TestInitialize]
public virtual void Init()
{
this.ExistedUsers = new List<UserModel> { UserTestHelper.ExistedUser };
} public override void GetSingleTest()
{
base.GetSingleTest(); AssertCommon.AssertIsNull<string, UserModel>(
TestCommon.GetEmptyStrings(), true, p => UserRepository.GetSingle(p));
AssertCommon.AssertIsNull(true, UserRepository.GetSingle(UserTestHelper.NotExistedLoginName)); UserModel actual = UserRepository.GetSingle(UserTestHelper.ExistedLoginName);
UserTestHelper.AssertEqual(UserTestHelper.ExistedUser, actual);
} public override void InsertTest()
{
base.InsertTest(); //验证添加成功的场景
//密码与他人相同也可添加
UserModel register1 = new UserModel { LoginName = "register1", Password = UserTestHelper.ExistedPassword };
UserModel register2 = UserTestHelper.NotExistedUser;
AssertCommon.ThrowException<UserModel>(
new UserModel[] { register1, register2 }, false, p => UserRepository.Insert(p)); //获取用户且应与注册的信息保持一致
UserModel actualRegister1 = UserRepository.GetSingle(register1.LoginName);
UserTestHelper.AssertEqual(register1, actualRegister1); UserModel actualRegister2 = UserRepository.GetSingle(register2.LoginName);
UserTestHelper.AssertEqual(register2, actualRegister2); //验证添加失败的场景,使用ThrowException来验证添加
AssertCommon.ThrowException<UserModel>(
new UserModel[] {
register1, //不能重复添加
//由于LoginName对应数据库字段为主键,故不能为空
new UserModel { LoginName = string.Empty, Password = UserTestHelper.NotExistedPassword },
}, true, p => UserRepository.Insert(p));
}
}

BaseUserRepositoryTest

14. 在项目 “IdleTest.TDDEntityFramework.RepositoryTest” 下添加类“UserRepositoryTest”

    [TestClass]
public class UserRepositoryTest : BaseUserRepositoryTest
{
protected SqlFileContext dbContext; protected IDisposable TestContext; private IUserRepository userRepository
{
get { return new UserRepository(dbContext); }
} protected override IRepository<UserModel, string> Repository
{
get { return userRepository; }
} protected override IUserRepository UserRepository
{
get { return userRepository; }
} [TestInitialize]
public override void Init()
{
base.Init(); if (dbContext == null)
{
TestContext = ShimsContext.Create(); //注意使用shim时必须先调用此方法(非全局可使用using) ShimSqlFileContext context = new ShimSqlFileContext();
ShimDbSet<UserModel> shimDbSet = new ShimDbSet<UserModel>(); shimDbSet.AddT0 = p =>
{
if (this.ExistedUsers.Select(o => o.LoginName).Contains(p.LoginName)
|| string.IsNullOrEmpty(p.LoginName))
{
throw new Exception();
} this.ExistedUsers.Add(p);
return p;
}; shimDbSet.FindObjectArray = p =>
{
if (p != null && p.Length > )
{
return this.ExistedUsers.FirstOrDefault(o => o.LoginName.Equals(p[]));
} return null;
}; context.UsersGet = () => shimDbSet;
dbContext = context;
}
} [TestCleanup]
public virtual void Dispose()
{
this.TestContext.Dispose();
} [TestMethod]
public override void InsertTest()
{
base.InsertTest();
} [TestMethod]
public override void GetSingleTest()
{
base.GetSingleTest();
}
}

UserRepositoryTest

15. 编写测试类“UserRepositoryTest”时使用自动生成类生成“UserRepository”,并修改相应代码使编译通过

    public class UserRepository : IUserRepository
{
public IEnumerable<UserModel> Get(
Expression<Func<Models.UserModel,
bool>> filter = null,
Func<IQueryable<Models.UserModel>,
IOrderedQueryable<Models.UserModel>> orderBy = null,
string includeProperties = "")
{
throw new NotImplementedException();
} public UserModel GetSingle(string id)
{
throw new NotImplementedException();
} public void Insert(UserModel entity)
{
throw new NotImplementedException();
} public void Update(UserModel entityToUpdate)
{
throw new NotImplementedException();
} public void Delete(string id)
{
throw new NotImplementedException();
} public void Delete(UserModel entityToDelete)
{
throw new NotImplementedException();
}
}

UserRepository

16. 继续修改直至测试通过(前面说过这里只对其中两个方法进行测试)。然后按照上一篇文中的做法,再将UserRepository.cs文件移动到项目 “IdleTest.TDDEntityFramework.Repositories”并添加引用“IdleTest.TDDEntityFramework.IRepositories”,记得要修改命名空间是解决方案编译通过。

    public class UserRepository : IUserRepository, IDisposable
{
private SqlFileContext dbContext;
private DbSet<UserModel> UserModelSet; public UserRepository(SqlFileContext dbContext)
{
this.dbContext = dbContext;
this.UserModelSet = this.dbContext.Users;
} public IEnumerable<UserModel> Get(
Expression<Func<UserModel,
bool>> filter = null,
Func<IQueryable<UserModel>,
IOrderedQueryable<UserModel>> orderBy = null,
string includeProperties = "")
{
IQueryable<UserModel> query = UserModelSet; if (filter != null)
{
query = query.Where(filter);
} foreach (var includeProperty in includeProperties.Split
(new char[] { ',' }, StringSplitOptions.RemoveEmptyEntries))
{
query = query.Include(includeProperty);
} if (orderBy != null)
{
return orderBy(query).ToList();
}
else
{
return query.ToList();
}
} public UserModel GetSingle(string id)
{
return this.UserModelSet.Find(id);
} public void Insert(UserModel entity)
{
this.UserModelSet.Add(entity);
this.dbContext.SaveChanges();
} public void Update(UserModel entityToUpdate)
{
UserModelSet.Attach(entityToUpdate);
dbContext.Entry(entityToUpdate).State = EntityState.Modified;
this.dbContext.SaveChanges();
} public void Delete(string id)
{
var entityToDelete = GetSingle(id);
Delete(entityToDelete);
} public void Delete(UserModel entityToDelete)
{
if (dbContext.Entry(entityToDelete).State == EntityState.Detached)
{
UserModelSet.Attach(entityToDelete);
}
UserModelSet.Remove(entityToDelete);
this.dbContext.SaveChanges();
} public void Dispose()
{
if (this.dbContext != null)
{
this.dbContext.Dispose();
}
}
}

UserRepository

【总结】

  本文啰啰嗦嗦写了一大堆,其重点在于编写服务层(业务层)的测试时通过改变一些编码习惯以便于业务人员的参与;其次则是UserRepositoryTest中的Init方法,对DbContext和DbSet进行了模拟,而我自己编写的继承DbContext的SqlFileContext类将不会被测试。

  其实再写本文前我也没有编写类似的单元测试,算是个人边实践边做的笔记,感觉对数据仓储(或者说数据访问层)的测试做到面面俱到仍然
还是有难度。甚至我认为这种只对Entity Framework框架提供的操作进行封装的测试可能不太有必要。

使用IdleTest进行TDD单元测试驱动开发演练(2)的更多相关文章

  1. 使用IdleTest进行TDD单元测试驱动开发演练(3) 之 ASP&period;NET MVC

    一.[前言] (1)本文将用到IOC框架Unity,可参照<Unity V3 初步使用 —— 为我的.NET项目从简单三层架构转到IOC做准备>(2)本文的解决方案是基于前述<使用I ...

  2. 使用IdleTest进行TDD单元测试驱动开发演练(1)

    [前言] 开发工具:Visual Studio 2012 测试库:Visual Studio 2012自带的MSTest DI框架:Unity 数据持久层:Entity Framework 前端UI: ...

  3. TDD单元测试驱动

    使用IdleTest进行TDD单元测试驱动开发演练(2)   [前言] 1. 有关上篇请参见<使用IdleTest进行TDD单元测试驱动开发演练(1)>,有关本篇用到Entity Fram ...

  4. TDD测试驱动开发

    TDD测试驱动开发 一.概念 TDD故名思意就是用测试的方法驱动开发,简单说就是先写测试代码,再写开发代码.传统的方式是先写代码,再测试,它的开发方式与之正好相反. TDD是极限编程的一个最重要的设计 ...

  5. 我看TDD测试驱动开发

    今天在实验室给大家介绍了一下TDD和Docker,大家对TDD都比较感兴趣,包括老板,也问了一些问题. 还是从头来说TDD吧,TDD作为敏捷开发领域的领头军,充满魅力,同时也充满争议.一切从三大军规说 ...

  6. 行为驱动开发BDD和Cucunber简介

    测试驱动开发(TDD) 1.测试驱动开发,即Test-Driven Development(TDD),测试驱动开发是敏捷开发中的一项核心实践和技术,也是一种设计方法论.TDD的原理是在开发功能代码之前 ...

  7. 行为驱动开发BDD概要

    BDD脱胎于TDD 行为驱动开发(Behavior-Driven Development,简称BDD),是在测试驱动开发(Test-Driven Development,TDD)基础上发展而来的一种软 ...

  8. 测试计划驱动开发模式 TPDD:一种比 TDD 更友好的开发模式

    相信大部分开发团队都在使用TDD,并且还有很多开发团队都 对外声明 在使用 TDD 开发模式. 之所以说是“对外声明”,是因为很多开发团队虽然号称使用的是 TDD 开发模式,实际开发过程中却无法满足 ...

  9. Scrum敏捷软件开发之技术实践——测试驱动开发TDD

    重复无聊的定义 测试驱动开发,英文全称Test-Driven Development,简称TDD,是一种不同于传统软件开发流程的新型的开发方法.它要求在编写某个功能的代码之前先编写测试代码,然后只编写 ...

随机推荐

  1. 为什么要学习java?

    前面说了什么是java java只是一门语言,中文,英语,c++,c#等等数之不尽的语言 java的应用领域: 1)安卓应用 2)金融业服务器的应用 3)Java Web应用 4)软件工具 5)交易应 ...

  2. error while loading shared libraries&colon; libXXX&period;so&period;x&colon; cannot open shared object file&colon; No such file or directory &period;

    转载:http://www.eefocus.com/pengwr/blog/2012-02/235057_baf52.html 此时你可以locate libXXX.so.x 一下,查看系统里是否有该 ...

  3. iotop命令

    简介: iotop – simple top-like I/O monitor iotop是一个用来监视磁盘I/O使用状况的 top 类工具,可监测到哪一个程序使用的磁盘IO的信息(requires ...

  4. 基于bootstrap的图片轮播效果展示

    <!DOCTYPE html><html lang="zh-CN"> <head> <meta charset="utf-8&q ...

  5. 条款22 template method 模式

    template method 模式,模板方法模式 其实他和C++模板没有关系. 前者是提供的为派生类设计者提供清晰指示的一种方法,这个事实表示"如何去实现基类所规定的契约" 基类 ...

  6. MYSQL判断某个表是否已经存在

    方法一.You don't need to count anything. SELECT 1 FROM testtable LIMIT 1; If there's no error, table ex ...

  7. java 反射,注解,泛型,内省(高级知识点)

     Java反射 1.Java反射是Java被视为动态(或准动态)语言的一个关键性质.这个机制允许程序在运行时透过Reflection APIs    取得任何一个已知名称的class的内部信息, 包括 ...

  8. Android OpenGL ES(八)绘制点Point &period;&period;

    上一篇介绍了OpenGL ES能够绘制的几种基本几何图形:点,线,三角形.将分别介绍这几种基本几何图形的例子.为方便起见,暂时在同一平面上绘制这些几何图形,在后面介绍完OpenGL ES的坐标系统和坐 ...

  9. uvalive 3135 Argus

    https://vjudge.net/problem/UVALive-3135 题意: 有一个系统有多个指令,每个指令产生一个编号为qnum的时间,每个指令的触发间隔不相同,现在给出若干个指令,现在的 ...

  10. Maven 通过maven对项目进行拆分、聚合(重点)

    对现在已有maven ssh项目进行拆分,拆分思路:将dao层的代码已经配置文件全体提取出来到一个表现上独立的工程中.同样service.action拆分. ssh-parent: 父工程 ssh-d ...