工作三周了,深深理解了学校和工作中处理问题的不同,从武哥这里得到了四字经验:最佳实践,而这个最佳实践是先投入工作,而后获取结论,改变之前先理论后实践的做法。于是产生了写一系列的博文的想法,积累工作中的经验,希望以后少踩坑,本篇为开篇第一篇,涉及到我这两周真正有所实践的学习–单元测试。本系列最佳实践每一篇博文都长期更新,而且每一篇都会分两方面来讲:1,知识的通用性,2,知识在工作中的最佳实践。

单元测试

注意:本篇只针对C#,适用版本vs2015

定义

是指对软件中的最小可测试单元进行检查和验证。对于单元测试中单元的含义,Java里单元指一个类,C#中,一个方法,一个类,一个窗口的测试。即单元测试。单元就是人为规定的最小的被测功能模块。单元测试是在软件开发过程中 要进行的最低级别的测试活动,软件的独立单元将在与程序的其他部分相隔离的情况下进行测试。

一些常识

测试范围:单元测试一般针对公共方法,例如public修饰的方法
测试人:编写者自身
测试时机:在函数编写之前,并随着功能增加不断增加测试用例

通用操作

创建和运行

1,创建单元测试项目

2,依据要测试的项目,各级对应创建相应的文件夹

3,在文件夹下添加单元测试

4,注意待测方法前要加[Test Method],否则不能生成单元测试

5,第一次测试的时候要生成解决方案,然后才能运行或调试单元测试

常用测试方法

常用测试方法测试:

Assert.IsNull() 测试指定的对象是否为空引用,如果为空,则测试通过;

Assert.IsNotNull() 测试指定的对象是否为非空,如果不为空,则测试通过;

Assert.IsTrue() 测试指定的条件是否为True,如果为True,则测试通过;

Assert.AreEqual() 测试指定的值是否相等,如果相等,则测试通过;

对于不同返回类型需要做哪些测试

**对于int型:**Assert.AreEqual()方法来测试是否和预期值相等,一般测试是否为1

对于String类型的:先测是否为null---Assert.IsNull(),然后测是否为空串Assert.AreEqual(),最后测和预期值是否相符Assert.AreEqual()

对于list类型:先测是否为null–Assert.IsNull(),然后测list.Count>0是否为真Assert.IsTrue() ,最后测返回列表个数list.Count是否和预期相同Assert.AreEqual()

最佳实践

随着单元测试的深入,持续进行,会涉及到各个方面:首先,DAO层的单元测试最佳实践

最佳实践规则

1,单元测试原子性,没有switch,没有分支,总体来说,圈复杂度为1–ps(vs2015可安装CodeMetrics插件检测圈复杂度)

2,单一职责,指行为不是方法,类例如数据库里的增删改查,CRU和SafeCRU,一个整体行为,而不是多个方法,这样可以减少代码量,而且也对整个行为进行了测试

3,独立无耦合,单元测试间不能相互调用,否则有可能出现无限循环错误

4,属性值不共享,每个独立单元都要准备自己的数据,虽然复用低,但是能保证一致

5,测试驱动开发,单元测试越早越好,我之后写代码也要记住这一点

最佳实践结构

依照个人经验总结以下结构:

1,测试类内准备的公共数据

该测试准备数据修饰为private static
1,公共待测数据:公有的,各个方法都会使用到的测试数据

2,公共的预期值:公有的,约定好的各个待测方法的执行预期值

2,测试类内准备的继承自父类方法的测试

由于一些类可能会共用一些方法,所以可以抽象出一个公共父类,这样我们把所有公共父类所有的方法单独拿出来测试,这样每一个子类测试的时候测试到公有父类的方法可以减少错误的发生,甚至可以直接copy代码

该测试方法修饰为 public void 待测方法名()注意,一定要为public,否则测试无法进行

1,该方法私有的待测数据:私有的,该方法测试需要用到的数据 类型一般为var

2,该方法私有的业务测试:私有的,该方法核心功能的测试(例如一个更新操作函数,就不需要对其它诸如创建等做过多assert)

3,销毁为测试准备的所有数据(公有+私有)

3,测试类内准备的自身实现的方法的测试

不是从父类继承的公用方法,也主要包括三个步骤

该测试方法修饰为 public void 待测方法名()注意,一定要为public,否则测试无法进行

1,该方法私有的待测数据:私有的,该方法测试需要用到的数据 类型一般为var

2,该方法私有的业务测试:私有的,该方法核心功能的测试(例如一个更新操作函数,就不需要对其它诸如创建等做过多assert)

3,销毁为测试准备的所有数据(公有+私有)

最佳实践操作

tip:还可以做代码覆盖率检测哦。利用vs自带的代码覆盖率检测可以看到有哪些方法还没有被测试

DAO层进行的最佳实践

dao层一般用于和数据库的存储过程进行交互,操作无外乎增删改查,且大多数的增删都是通过id来操作,而更新和删除则是通过id和一些相应的参数来实现。

公有的准备数据

对应于准备数据的最佳实践:一般先不做公共准备数据,而是先写各个方法自己存在的准备数据,最后发现统一的可以抽象成公共数据。经过实践,我们可以这么准备数据

  #region 公共的准备数据

        // *************************************************************公共的准备数据
        private static int age= 2345;   //例如下边的方法用到了age,所以抽象出来

        /// <summary>
        /// 创建方法,如果每个测试方法都用到了Create,则把它抽象出来实现复用(但不能再将其视为单元测试方法)
        /// </summary>
        /// <returns></returns>
        private int Create(Guid id)
        {
            var model = Build(id);
            return UserDao.Instance.Create(model);
        }

        /// <summary>
        /// 实例构造方法
        /// </summary>
        /// <returns></returns>
        private User Build(Guid id)
        {
            return new User()
            {
                ID = id,
                UserName = "tianmaolin",
                Age=age
             };
        }

        #endregion 公共的准备数据

        #region 公共的预期值

        //*************************************************************公共的预期参数值
        private static int checkDataRight = 1;

        #endregion 公共的预期值

对方法进行单元测试


        #region 继承自父类的方法进行的单元测试

        //********************继承自父类的方法进行的单元测试***********************************
        /// <summary>
        /// 基于id测试用户的增删改查
        /// <param name="id"/>
        /// </summary>
        [TestMethod]
        public void UserCRUD()
        {
            //准备数据==========================================================SetUp
            var id = Guid.NewGuid();
            var model = Build(id);
            var create = Create(id);
            //准备好的更新状态
            var modelupdate = new User()
            {
                ID = id,
                UserName = "tttttttt",

            };
            //测试创建============================================================ C
            //测试是否创建成功
            Assert.AreEqual(checkDataRight, create);

            //测试是否查询成功=====================================================R
            var read = UserDao.Instance.GetById(id);
            Assert.IsNotNull(read);  //非空测试
            Assert.IsTrue(read.ID == id);//Guid一致性测试
            Assert.IsTrue(read.UserName == "tianmaolin");//创建者姓名一致 
            Assert.IsTrue(read.Age == age);//创建者年龄一致性测试


            //测试是否更新成功(依据新model更新) =================================== U
            var update = UserDao.Instance.Update(modelupdate);
            Assert.AreEqual(checkDataRight, update);//更新成功测试
            var readupdate = UserDao.Instance.GetById(id);//更新后获取
            Assert.IsTrue(read.UserName == "tttttttt"),创建者姓名更新测试      


            //测试删除=============================================================D
            var delete = UserDao.Instance.Delete(id);
            Assert.AreEqual(checkDataRight, delete);//是否删除成功
            var readagain = UserDao.Instance.GetById(id);//再次查询
            Assert.IsNull(readagain);//为空则删除成功
            //====================================================================TearDown
        }

        #endregion 继承自父类的方法进行的单元测试

踩过的坑

1,如果两张表有主外键关联关系,对从表的数据进行单元测试的时候先要创建主表的数据,最后还要记得销毁主表里的数据,例如student的插入依赖class。

2,写单元测试之前,一定要看代码的具体实现,我写代码发现存储过程里的操作之前有查重操作,然而自己并没有看好,导致每次都插入失败,血淋淋的教训,一定要先看好待测函数的功能实现,避免采坑!!!,尤其是涉及到模糊查询的时候,创建第二条数据的时候一定要改变要求非重复的值,否则会创建失败。

3,涉及到更新的单元测试,提前准备好更新数据,还得注意哪些数据无需更新,不是所有传入的参数都是要更新的,不要导致一直报错

建议测试过程

1,先看函数要实现什么功能,一定要理解他要的输入和期望的输出,如果有分支流程还得注意准备两套数据,如果有更新要提前准备更新数据

2,再看存储过程需要什么参数,存储过程里是否有查重等条件限定,要创建满足条件限定的准备数据

3,然后要准备好数据,发现多个方法有公有数据,统一拿出来,创建这种通用操作抽离出来

4,测试之后一定要记得销毁数据

5,出错要耐心调试,注意表间关系