• .net、C#单元测试xUnit


    xUnit单元测试

    测试的分类

    • 单元测试:对某个类或者某个方法进行测试

    • 集成测试:可使用Web资源、数据库数据进行测试

    • 皮下测试:在Web中对controller下的节点测试

    • UI测试:对界面的功能进行测试

      程序员主要关注单元测试集成测试

    xUnit

    xUnit是一个可对.net进行测试的框架,支持.Net Framework、.Net Core、.Net Standard、UWP、Xamarin。

    • 支持多平台
    • 并行测试
    • 数据驱动测试
    • 可扩展

    测试一般针对Public方法进行测试,如果是私有方法需要改变修饰符才能进行测试。同时测试项目需添加对被测项目的引用,同时测试项目需要引入xUnit框架库。

    最简单的测试

    1. 创建一个.net core类库:Demo,添加一个Calculator类

      namespace Demo
      {
          public class Calculator
          {
              public int Add(int x,int y)
              {
                  return x + y;
              }
          }
      }
      
      • 1
      • 2
      • 3
      • 4
      • 5
      • 6
      • 7
      • 8
      • 9
      • 10
    2. 在同一解决方案,创建一个xUnit测试项目:DemoTest

      命名规则:一般是项目名+Test命名测试项目。创建一个类:CalculatorTests:

      public class CalculatorTests
      {
      	[Fact]
      	public void ShouldAddEquals5() //注意命名规范
      	{
      	    //Arrange 
      	    var sut = new Calculator(); //sut-system under test,通用命名
      	    //Act
      	    var result = sut.Add(3, 2);
      	    //Assert
      	    Assert.Equal(5, result);
      	}
      }
      
      • 1
      • 2
      • 3
      • 4
      • 5
      • 6
      • 7
      • 8
      • 9
      • 10
      • 11
      • 12
      • 13
    3. 运行测试,s自带的测试资源管理器,找到测试项目,选择运行

    测试的三个阶段

    • Arrange: 在这里做一些准备。例如创建对象实例,数据,输入等。
    • Act: 在这里执行生产代码并返回结果。例如调用方法或者设置属性。
    • Assert:在这里检查结果,会产生测试通过或者失败两种结果。

    Assert详解

    xUnit提供了以下类型的Assert

    类型行为
    boolTrue/False
    string是否相等、空、以什么开始/结束、是否包含、是否匹配正则
    数值是否相等、是否在范围内、浮点的精度
    集合内容是否相等、是否包含、是否包含某种条件的元素、每个元素是否满足条件
    事件自定义事件、.net事件
    Object是否为某种类型、是否继承某类型

    实例

    创建一个类库

     public class Patient : Person, INotifyPropertyChanged
        {
            public Patient()
            {
                IsNew = true;
                BloodSugar = 4.900003f;
                History = new List<string>();
                //throw new InvalidOperationException("not able to create"); 测试异常使用
            }
    
            public string FullName => $"{FirstName} {LastName}";
            public int HeartBeatRate { get; set; }
    
            public bool IsNew { get; set; }
    
            public float BloodSugar { get; set; }
            public List<string> History { get; set; }
    
            /// 事件
            public event EventHandler<EventArgs> PatientSlept;
    
    
            public void OnPatientSleep()
            {
                PatientSlept?.Invoke(this, EventArgs.Empty);
            }
    
            public void Sleep()
            {
                OnPatientSleep();
            }
    
            public void IncreaseHeartBeatRate()
            {
                HeartBeatRate = CalculateHeartBeatRate() + 2;
                OnPropertyChanged(nameof(HeartBeatRate));
            }
    
            private int CalculateHeartBeatRate()
            {
                var random = new Random();
                return random.Next(1, 100);
            }
    
            public event PropertyChangedEventHandler PropertyChanged;
    
            protected virtual void OnPropertyChanged([CallerMemberName] string propertyName = null)
            {
                PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
            }
        }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51

    Bool类型测试

    [Fact] //必须有这个特性
    public void BeNewWhenCreated()
    {
        // Arrange
        var patient = new Patient();
        // Act
        var result = patient.IsNew;
        // Assert
        Assert.True(result);
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10

    String测试

    [Fact]
    public void HaveCorrectFullName()
    {
        var patient = new Patient();
        patient.FirstName = "Nick";
        patient.LastName = "Carter";
        var fullName = _patient.FullName;
        Assert.Equal("Nick Carter", fullName); //相等
        Assert.StartsWith("Nick", fullName);//以开头
        Assert.EndsWith("Carter", fullName);//以结尾
        Assert.Contains("Carter", fullName);//包含
        Assert.Contains("Car", fullName);
        Assert.NotEqual("CAR", fullName);//不相等
        Assert.Matches(@"^[A-Z][a-z]*\s[A-Z][a-z]*", fullName);//正则表达式
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15

    数值测试

    [Fact]
    public void HaveDefaultBloodSugarWhenCreated()
    {
        var p = new Patient();
        var bloodSugar = p.BloodSugar;
        Assert.Equal(4.9f, bloodSugar,5); //判断是否相等,最后一个是精度,很重要
        Assert.InRange(bloodSugar, 3.9, 6.1);//判断是否在某一范围内
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8

    空值判断

    [Fact]
    public void HaveNoNameWhenCreated()
    {
        var p = new Patient();
        Assert.Null(p.FirstName);
        Assert.NotNull(_patient);
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    集合测试

    [Fact]
    public void HaveHadAColdBefore()
    {
    	//Arrange
        var _patient = new Patient();
        
    	//Act
    	var diseases = new List<string>
        {
            "感冒",
            "发烧",
            "水痘",
            "腹泻"
        };
        _patient.History.Add("发烧");
        _patient.History.Add("感冒");
        _patient.History.Add("水痘");
        _patient.History.Add("腹泻");
        
    	//Assert
    	//判断集合是否含有或者不含有某个元素
        Assert.Contains("感冒",_patient.History);
        Assert.DoesNotContain("心脏病", _patient.History);
    
        //判断p.History至少有一个元素,该元素以水开头
        Assert.Contains(_patient.History, x => x.StartsWith("水"));
    	//判断集合的长度
        Assert.All(_patient.History, x => Assert.True(x.Length >= 2));
    
        //判断集合是否相等,这里测试通过,说明是比较集合元素的值,而不是比较引用
        Assert.Equal(diseases, _patient.History);
    
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33

    object测试

    [Fact]
    public void BeAPerson()
    {
        var p = new Patient();
        var p2 = new Patient();
        Assert.IsNotType<Person>(p); //测试对象是否相等,注意这里为false
        Assert.IsType<Patient>(p);
    
        Assert.IsAssignableFrom<Person>(p);//判断对象是否继承自Person,true
    
        //判断是否为同一个实例
        Assert.NotSame(p, p2);
        //Assert.Same(p, p2);
    
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15

    异常测试

    [Fact]
    public void ThrowException() 
    {
        var p = new Patient();
        //判断是否返回指定类型的异常
        var ex = Assert.Throws<InvalidOperationException>(()=> { p.NotAllowed(); });
        //判断异常信息是否相等
        Assert.Equal("not able to create", ex.Message);
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9

    判断是否触发事件

    [Fact]
    public void RaizeSleepEvent()
    {
        var p = new Patient();
        Assert.Raises<EventArgs>(
            handler=>p.PatientSlept+=handler,
            handler=>p.PatientSlept -= handler,
            () => p.Sleep());
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9

    判断属性改变是否触发事件

    [Fact]
    public void RaisePropertyChangedEvent()
    {
        var p = new Patient();
        Assert.PropertyChanged(p, nameof(p.HeartBeatRate),
                               () => p.IncreaseHeartBeatRate());
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    分组

    使用trait特性,对测试进行分组:[Trait(“GroupName”,“Name”)] 可以作用于方法级和Class级别。相同的分组使用相同的特性。

    [Fact]
    [Trait("Category","New")]//凡是使用这个特性且组名一样,则分到一个组中
    public void BeNewWhenCreated()
    {...}
    
    • 1
    • 2
    • 3
    • 4

    忽略测试

    在测试方法上加上特性[Fact(Skip="不跑这个测试")]

    自定义输出内容

    使用ITestOutputHelper可以自定义在测试时的输出内容

    public class PatientShould:IClassFixture<LongTimeFixture>,IDisposable
    {
        private readonly ITestOutputHelper _output;
        public PatientShould(ITestOutputHelper output,LongTimeFixture fixture)
        {
            this._output = output;
        }
    
        [Fact]
        public void BeNewWhenCreated()
        {
            _output.WriteLine("第一个测试");
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14

    常用技巧

    • 减少new对象,减少new对象,可以在构造函数中new,在方法中使用。
    • 测试类实现IDispose接口,测试完释放资源,注意每个测试结束后都会调用Dispose方法。

    共享上下文

    某个方法需要执行很长时间,而在构造函数中new时,每个测试跑的时候都会new对象或者执行方法,这是导致测试很慢。解决方法:

    模拟运行长时间的任务

    public class LongTimeTask
    {
        public LongTimeTask()
        {
            Thread.Sleep(2000);
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    相同测试类

    1. 创建一个类:
    public class LongTimeFixture
        {
            public LongTimeTask Task { get; }
            public LongTimeFixture()
            {
                Task = new LongTimeTask();
            }
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    1. 测试类实现IClassFixture接口,并在构造函数中使用依赖注入的方式获取方法
    public class PatientShould:IClassFixture<LongTimeFixture>,IDisposable
    {
        private readonly Patient _patient;
        private readonly LongTimeTask _task;
        public PatientShould(ITestOutputHelper output,LongTimeFixture fixture)
        {
            this._output = output;
            _task = fixture.Task;//获取方法
        }
    }
    //这样的话其实只有一个LongTimeTask实例,所以要保证该实例不能有副作用
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11

    不同测试类

    如果多个测试类都要用到相同的耗时任务,则可以这样用

    1. 添加一个类
        [CollectionDefinition("Lone Time Task Collection")]
        public class TaskCollection:ICollectionFixture<LongTimeFixture>
        {
        }
    
    • 1
    • 2
    • 3
    • 4
    1. 在使用的类上加上[CollectionDefinition("Lone Time Task Collection")]注意里面的字符串要相同
    [Collection("Lone Time Task Collection")]
    public class AAAShould:IClassFixture<LongTimeFixture>,IDisposable
    {
        private readonly LongTimeTask _task;
        public PatientShould(LongTimeFixture fixture)
        {
            _task = fixture.Task;//获取方法
        }
    }
    [Collection("Lone Time Task Collection")]
    public class BBBShould:IClassFixture<LongTimeFixture>,IDisposable
    {
        private readonly LongTimeTask _task;
        public BBBShould(LongTimeFixture fixture)
        {
            _task = fixture.Task;//获取方法
        }
    }
    //此时这两个类中测试方法都会共享一个LongTimeFixture实例
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19

    数据共享

    1.[Theory]

    可以写有构造参数的测试方法,使用InlineData传递数据

    [Theory]
    [InlineData(1,2,3)]
    [InlineData(2,2,4)]
    [InlineData(3,3,6)]
    public void ShouldAddEquals(int operand1,int operand2,int expected)
    {
        //Arrange
        var sut = new Calculator(); //sut-system under test
        //Act
        var result = sut.Add(operand1, operand2);
        //Assert
        Assert.Equal(expected, result);
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13

    2.[MemberData]

    可以在多个测试中使用

    1. 创建一个类
        public  class CalculatorTestData
        {
            private static readonly List<object[]> Data = new List<object[]>
            {
                new object[]{ 1,2,3},
                new object[]{ 1,3,4},
                new object[]{ 2,4,6},
                new object[]{ 0,1,1},
            };
    
            public static IEnumerable<object[]> TestData => Data;
        }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    1. 使用MemberData
    [Theory]
    [MemberData(nameof(CalculatorTestData.TestData),MemberType =typeof(CalculatorTestData))]
    public void ShouldAddEquals2(int operand1, int operand2, int expected)
    {
        //Arrange
        var sut = new Calculator(); //sut-system under test
        //Act
        var result = sut.Add(operand1, operand2);
        //Assert
        Assert.Equal(expected, result);
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11

    3.使用外部数据

    1. 读取外部集合类
    //    读取文件并返回数据集合 必须是IEnumerable
    public class CalculatorCsvData
    {
            public static IEnumerable<object[]> TestData
            {
                get
                {
    	            //把csv文件中的数据读出来,转换
                    string[] csvLines = File.ReadAllLines("Data\\TestData.csv");
                    var testCases = new List<object[]>();
                    foreach (var csvLine in csvLines)
                    {
                        IEnumerable<int> values = csvLine.Trim().Split(',').Select(int.Parse);
                        object[] testCase = values.Cast<object>().ToArray();
                        testCases.Add(testCase);
                    }
                    return testCases;
                }
            }
        }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    1. 使用
    [Theory]
    [MemberData(nameof(CalculatorCsvData.TestData), MemberType = typeof(CalculatorCsvData))]
    public void ShouldAddEquals3(int operand1, int operand2, int expected)
    {
        //Arrange
        var sut = new Calculator(); //sut-system under test
        //Act
        var result = sut.Add(operand1, operand2);
        //Assert
        Assert.Equal(expected, result);
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11

    4.DataAttribute

    1. 自定义特性
        public class CalculatorDataAttribute : DataAttribute
        {
            public override IEnumerable<object[]> GetData(MethodInfo testMethod)
            {
                yield return new object[] { 0, 100, 100 };
                yield return new object[] { 1, 99, 100 };
                yield return new object[] { 2, 98, 100 };
                yield return new object[] { 3, 97, 100 };
            }
        }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    1. 使用
    [Theory]
    [CalculatorDataAttribute]
    public void ShouldAddEquals4(int operand1, int operand2, int expected)
    {
        //Arrange
        var sut = new Calculator(); //sut-system under test
        //Act
        var result = sut.Add(operand1, operand2);
        //Assert
        Assert.Equal(expected, result);
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
  • 相关阅读:
    数字化转型过程中,企业如何搭建好数据安全?
    layui几种加载方式
    环境管理体系EMS基础知识试卷
    Win7 easy connect 提示:选路连接失败,可能当前连接网络异常,请稍后重试
    ServletContext
    开源集群管理系统对比分析:Kubernetes 与 Apache Mesos
    爬虫及词云总结
    使用凌鲨进行聚合搜索
    Pandas数据分析24——pandas时间重采样聚合
    蜂鸟E203学习(一)--RISC的前世今生
  • 原文地址:https://blog.csdn.net/weixin_44064908/article/details/128138551