.net持续集成测试篇之Nunit 测试配置

系列目录html

在开始以前咱们先看一个陷阱web

用到的Person类以下数据库

public class Person:IPerson
    {
        public string Name { get; set; }
        public int Age { get; set; }
        public DateTime BirthDay { get; set; }
        /// <summary>
        /// 判断Name是否包含字母B
        /// </summary>
        /// <returns></returns>
        public bool WhetherNameContainsB()
        {
            if (this.Name == null) throw new ArgumentNullException("参数不能为null");
            if (this.Name.Contains("B")) return true;
            return false;
        }
    }

这个类之前也用过,有三个属性和一个方法,其中方法用于判断Name字段是否包含大写字母B,若是包含返回true,不包含返回false,若是Name为null则抛出异常ide

测试类以下函数

[TestFixture]
    public class FirstUnitTest
    {
        private Person psn;
        public FirstUnitTest()
        {
         psn = new Person();
         }

        [Test]
        [Order(1)]
        public void SetPersonName()
        {
            psn.Name = "sto";
            Assert.IsNotEmpty(psn.Name);
        }
        [Test]
        [Order(2)]
        public void DemoTest()
        {
            Assert.Throws<ArgumentNullException>(() => psn.WhetherNameContainsB());
        }
       

    }

第一个测试给Name赋值,而后断言用户名不为空,这显然应该是经过的单元测试

第二个测试用于断言调用WhetherNameContainsB时会抛异常,因为这里Name并无赋值,因此会抛出异常,这里也应该能返回成功.测试

然而运行以上代码第二个测试返回的是失败!这是由于Nunit在运行测试类的时候会调用全部的测试方法,因为咱们显式指定的运行顺序(使用order注解)则第一个方法先于第二个方法前执行,因为第一个方法把Name设置为"sto",所以这时候全局psn的Name字段便有值了.因此第二个方法再调用psn的WhetherNameContainsB方法时,是不会抛出异常的(方法的逻辑是只有Name有值便不会抛出异常).this

若是不指定运行顺序,则第二个方法运行的结果是不肯定的,若是它先于第一个方法执行,则就会返回成功,若是晚于第一个方法则返回失败.设计

咱们前面说到,单元测试的结果应该是稳定的,然而这里倒是不肯定的,所以咱们要从新设计.日志

固然其实解决这个问题很简单,只要把对全局的变量移动到方法里面就好了,这样每一个方法的状态就不会被外部改变了.

改造后的测试类以下

[TestFixture]
    public class FirstUnitTest
    {
       
        public FirstUnitTest()
        {
        
         }

        [Test]
        [Order(1)]
        public void SetPersonName()
        {
            Person psn = new Person();
            psn.Name = "sto";
            Assert.IsNotEmpty(psn.Name);
        }
        [Test]
        [Order(2)]
        public void DemoTest()
        {
            Person psn = new Person();
            Assert.Throws<ArgumentNullException>(() => psn.WhetherNameContainsB());
        }
       

    }

咱们再运行,便都能经过了.

然而这样设计有一个问题,第一若是多个测试方法都要用到这个对象,则须要复制不少,第二若是多个方法之间共用的代码很是多,那么每一个方法里都要复制不少代码,咱们前面说过单元测试里的代码应力求简洁明了,而且复制一样的代码不利于维护.下面咱们介绍Nunit里的Setup

Setup注释

在单元测试类中若是把一个方法加上setup注解,则这个方法会先于其它未标的方法执行,而且每一个方法执行以前都会执行它,若是在setup注解的方法内初始化对象,则每一个方法运行以前都会运行这个被注解的方法,则每次变量都从新初始化,不会再有数据被共享形成的各类问题了.咱们用setup改造后的测试类以下

[TestFixture]
    public class FirstUnitTest
    {
        private Person psn;
        public FirstUnitTest()
        {
        
         }

        [SetUp]
        public void Setup()
        {
            psn = new Person();
        }
        [Test]
        [Order(1)]
        public void SetPersonName()
        {
          
            psn.Name = "sto";
            Assert.IsNotEmpty(psn.Name);
        }
        
        [Test]
        [Order(2)]
        public void DemoTest()
        {
           
            Assert.Throws<ArgumentNullException>(() => psn.WhetherNameContainsB());
        }
       

    }

咱们在标识为Setup的方法里初始化Person,这样测试就能经过了

被Setup注解的方法名可任意取,只要符合命名规范便可

Nunit并不限制一个测试类中有多个Setup方法,可是强烈不建议这么作.

OneTimeSetup注释

OneTimeSetup也是在全部的测试方法运行以前运行,不一样的是它并不像SetUp同样每一个测试方法运行以前都会运行,而是在全部测试方法运行以前之运行一次.它适用这样场景:好比说咱们程序里的数据访问封闭类,这个类里面通常都是访问数据库的各类方法和一些私有的变量像链接字符串之类的,数据访问方法里只会去读取这些字段而不去修改它.最为重要的是每一个测试方法运行以前都去实体化一个这样的类会很耗费资源.像这种类型即可以放在OneTimeSetup方法里,在类建立的时候运行一次.

这个方法功能很像构造函数,它能作的工做通常构造函数也能作.

Teardown

Teardown和Setup用法同样,只是它是在测试方法运行以后才运行,若是咱们的测试方法里有须要释放的对象能够在这个方法里释放.

OneTimeTearDown

它是在全部的方法都运行完以后才运行一次,功能上至关于析构函数,用于在测试类全部方法都执行完之后释放掉类中使用的资源.

前面部分咱们讲了如何在所在单元测试运行以前以及在每个单元测试以前如何运行一个特定的方法.下面讲解如何在程序集运行以前和运行以后运行某一指定方法.

可能会有人怀疑这样作的意义,的确,大部分时候咱们可能不须要在程序集运行以前或者以后运行某一方法,可是特定的状况下这样作确实会给测试带来很大帮助.好比如下场景

  • 咱们想要统计一下全部测试方法的运行时间,这时候咱们能够在程序集以前启动StopWatch并在全部方法运行完以后得到运行时间,并写入日志.固然这样作可能显得有点傻.
  • 在Web项目中可能会大量使用ConfigurationManager.AppSetting[xxx]来获取web项目配置,这样作给测试带来难题

    因为单元测试的运行环境不少时候并不是在程序的输出目录,所以web项目使用到AppSetting配置的方法在web环境运行正常,可是在单元测试环境获得的值都是Null,这将会致使测试时大量业务覆盖不到.

    在测试的时候咱们很难经过传参来改变这个值,由于在程序中每每都是获取AppSetting里的值,而不是设置,所以它每每不包含在方法的参数里.也就无法经过传参来修改它.

    咱们若是在Setup里给AppSetting赋值,好比ConfigurationManager.AppSettings["user"] = "sto";这样在运行的时候咱们即可以获取到这个值了,可是AppSetting是全局的,可能程序中不少方法都用到了它,咱们在每一个测试方法里都写个Setup方法给它复制显然很是boring.

    这时候咱们能够在程序集运行以前运行一个方法,在这个方法里给AppSetting赋值,这样测试方法运行的时候使用到AppSetting的地方就能够获取到值了.

    要作到这一点,咱们须要新建一个类,并把类上加上SetUpFixture注解.而后方法上加上OneTimeSetUp和OneTimeTeardown注解.这样Nunit就会在程序集加载的时候扫描到这个类,而后对它处理.

    咱们看一下示例代码

    [SetUpFixture]
    public  class AssemblySetup
      {
          [OneTimeSetUp]
          public void RunBeforeEveryMethod()
          {
              ConfigurationManager.AppSettings["user"] = "sto";
              ConfigurationManager.AppSettings["age"] = "32";
          }
      }

    咱们新建这个类之后RunBeforeEveryMethod便会在程序集中全部代码运行以前运行了

咱们看运行结果
Avatar
咱们能够看到,在测试类中随便找一个方法里面去获取值,均可以获取到了.

前面咱们讲解了如何在方法运行先后,在测试类的全部方法运行先后以及如何在程序集,下面咱们讲一下如何自定义一个方法在测试方法运行以前/以后运行.

自定义方法的优点在于若是每一个测试类的setup里运行的代码基本相同,只是稍微有一点差别,这样就会致使代码重复的问题.好比咱们要在方法运行以前和以后记录一些日志,这样咱们就能够自定义一个方法实如今测试方法运行先后运行这个自定义方法,减小代码重复.

要实现自定义运行方法,咱们要继承TestactionAttribute
示例代码以下

public class MyTestAction:TestActionAttribute
    {
        public override void BeforeTest(ITest test)
        {
            Console.WriteLine("★★★★★★★★★★" + test.FullName);
        }
       
    }

咱们用Console.WriteLine模拟.

Itest对象由Nunit在运行时注入.

而后咱们要在运行这个自定义方法的类上加上MyTestAction注解便可.

自定义运行方法很是强大,还能够提供参数,这样会在大幅度减小类似代码的重复,提升可维护性,你们要之后的测试中慢慢体会.