.NET009-xUnit导读

191 阅读4分钟

基础

意义

  • 可靠:比人工测试要快要可靠
  • 可迭代:测试代码可根据实际生产代码需要进行迭代
  • 可自动化:可在任何时间任何环境(具备相关运行环境)进行测试且可频繁重复进行测试。

分类

纵轴代码测试的深度,横轴代表测试的广度(覆盖度)。

  • 单元测试(Unit Test):可以测试一个类或者一个类的某个功能,具有很好的深度,但对于整个应用或者功能没有很好的覆盖面。
  • 集成测试(Integration Test):没有单元测试那么细致,但具有相对较好的测试覆盖面。例如集成测试可用于测试功能的组合、数据库或文件系统的外部资源等。
  • 皮下测试(Subcutaneous Test):这种测试作用于UI层的下面一层,对整个引用有着很好的覆盖率,但是深度欠佳。
  • UI测试:测试覆盖范围最广,但是深度欠佳。

测试三阶段AAA

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

xUnit.NET

基本概念

github.com/xunit/xunit

xUnit是一个测试框架,可以针对.NET或.NET Core项目进行测试。测试项目需引用被测项目和xUnit库。测试用例编写好后,用Test Runner来运行测试。目前可用的Test Runner包括vs自带的Test Explorer或者dotnet test命令行,以及第三方工具(resharper)等。各参数的含义:docs.microsoft.com/zh-cn/dotne…

xUnit支持的平台:.net full, uwp,xamarin

dotnet test -用于执行单元测试的.NET测试驱动程序。
dotnet test [<PROJECT> | <SOLUTION> | <DIRECTORY> | <DLL>]
    [-a|--test-adapter-path <ADAPTER_PATH>] [--blame] [--blame-crash]
    [--blame-crash-dump-type <DUMP_TYPE>] [--blame-crash-collect-always]
    [--blame-hang] [--blame-hang-dump-type <DUMP_TYPE>]
    [--blame-hang-timeout <TIMESPAN>]
    [-c|--configuration <CONFIGURATION>]
    [--collect <DATA_COLLECTOR_NAME>]
    [-d|--diag <LOG_FILE>] [-f|--framework <FRAMEWORK>]
    [--filter <EXPRESSION>] [--interactive]
    [-l|--logger <LOGGER>] [--no-build]
    [--nologo] [--no-restore] [-o|--output <OUTPUT_DIRECTORY>]
    [-r|--results-directory <RESULTS_DIR>] [--runtime <RUNTIME_IDENTIFIER>]
    [-s|--settings <SETTINGS_FILE>] [-t|--list-tests]
    [-v|--verbosity <LEVEL>] [[--] <RunSettings arguments>]
dotnet test -h|--help

示例:

dotnet test 运行当前目录所含项目中的测试
dotnet test ~/projects/test1/test1.csproj 运行test1项目中的测试
dotnet test ~/projects/test1/bin/debug/test1.dll 运行test1.dll程序集中的测试
dotnet test --logger trx 在当前目录运行项目中的测试,并以trx格式生成测试结果文件
dotnet test --logger "console;verbosity=detailed" 记录详细的测试结果日志到控制台上
dotnet test --collect:"Code Coverage" 这个需要安装Coverlet并进行一定的配置,默认配置会生成名字为Code Coverage的.coverage文件,用于收集测试代码覆盖率。
dotnet test --blame 在当前目录下的项目中运行测试,并报告在测试主机发生故障时正在进行的测试
dotnet test --filter Method 运行FullyQualifiedName包含Method的测试
dotnet test --filter Name~TestMethod1 运行名称包含TestMethod1的测试
dotnet test --filter FullyQualifiedName!=MSTestNamespace.UnitTest1.TestMethod1 运行除 MSTestNamespace.UnitTest1.TestMethod1 之外的其他所有测试。
dotnet test --filter Category=CategoryA 运行含 [TestCategory("CategoryA")] 批注的测试。
dotnet test --filter "FullyQualifiedName=MyNamespace.MyTestsClass<ParameterType1%2CParameterType2>.MyTestMethod" 对于包含泛型类型参数的逗号的 FullyQualifiedName 值,请使用 %2C 来转义逗号。
using Microsoft.VisualStudio.TestTools.UnitTesting;

namespace MSTestNamespace
{
    [TestClass]
    public class UnitTest1
    {
        [TestMethod, Priority(1), TestCategory("CategoryA")]
        public void TestMethod1()
        {
        }

        [TestMethod, Priority(2)]
        public void TestMethod2()
        {
        }
    }
}

安装配置xUnit.NET

  • 创建一个albertxunit类库项目(dotnet new classlib -n albertxunit)
  • 创建xUnit Test项目(albertxunit.Test) (dotnet new xunit -n test_albertxunit)

(dotnet add reference ../albertxunit/albertxunit.csproj)

(dotnet sln add test_albertxunit\test_albertxunit.csproj)

创建完成后,默认引用了这四个Packages,其中coverlet.collector是用于收集代码测试覆盖率的。(dotnet test --collect:"Code Coverage" 这个需要安装Coverlet并进行一定的配置,默认配置会生成名字为Code Coverage的.coverage文件,用于收集测试代码覆盖率。)

  • 引用待测项目,从Test Explorer可以看到待测项目

Assert

Assert(断言)是基于代码的返回值、对象的最终状态、事件是否发生等情况来评估测试的结果。

xUnit提供了以下类型的Assert:

  • boolen:True/False
  • String:相等/不等, 是否为空,以..开始/结束,是否包含子字符串,匹配正则表达式
  • 数值型:相等/不等,是否在某个范围内,浮点的精度
  • Collection:内容是否相等,是否包含某个元素,是否包含满足某种条件(predicate)的元素,是否所有的元素都满足某个assert
  • Raised events:Custom events,Framework events(例如:PropertyChanged)
  • Object Type:是否是某种类型,是否某种类型或继承与某种类型

Tips一般每个Test方法中只有一个assert, 如果一个test里面有多个asserts,只要这些assert都是针对同一个行为的即可。

测试案列1-boolen

public class Patient
    {
        public Patient()
        {
            IsNew = true;
        }

        public string FirstName { get; set; }
        public string LastName { get; set; }
        public string FullName => $"{FirstName} {LastName}";
        public int HeartBeatRate { get; set; }
        public bool IsNew { get; set; }

        public void IncreaseHeartBeatRate()
        {
            HeartBeatRate = CalculateHeartBeatRate() + 2;
        }

        private int CalculateHeartBeatRate()
        {
            var random = new Random();
            return random.Next(1, 100);
        }
    }
public class PatientShould
    {
        [Fact]
        public void HaveHeartBeatWhenNew()
        {
            var patient = new Patient();

            Assert.True(patient.IsNew);
        }
    }

运行测试,进入到项目目录下,dotnet test运行整个测试

只运行单个方法 dotnet test --filter TestIsNew

生成测试日志 dotnet test --logger trx

使用命令行将详细日志信息打印出来 dotnet test --logger "console;verbosity=detailed" detailed/minimal

生成测试覆盖率文件 dotnet test --collect "Code Coverage"

测试案例2-string

using albertxunit;
using Xunit;

namespace Test_albertxunit
{
    public class PatientTest
    {
        [Fact]
        public void TestIsNew()
        {
            Patient patient = new Patient();
            Assert.True(patient.IsNew);
        }

        [Fact]
        public void CalFullName()
        {
            Patient p = new Patient();
            p.FirstName = "albert";
            p.LastName = "zhao";
            Assert.Equal("albert zhao",p.FullName);
        }
        
        //正则表达式,匹配首字母是否是大写
         [Fact]
        public void CalculcateFullNameWithTitleCase()
        {
            var p = new Patient
            {
                FirstName = "Nick",
                LastName = "Carter"
            };
            Assert.Matches("[A-Z]{1}{a-z}+ [A-Z]{1}[a-z]+", p.FullName);
        }
    }
}

dotnet build之后,执行dotnet test --filter CalFullName,测试结果

测试案例3:数值

   public void HaveDinner()
        {
            var random = new Random();
            _bloodSugar += (float)random.Next(1, 1000) / 100; //  应该是1000
        }
[Fact]
        public void BloodSugarIncreaseAfterDinner()
        {
            var p = new Patient();
            p.HaveDinner();
            // Assert.InRange<float>(p.BloodSugar, 5, 6);
            Assert.InRange(p.BloodSugar, 5, 6);
        }

//小数点精度三位,precision
[Fact]
        public void HaveCorrectSalary()
        {
            var plumber = new Plumber();
            Assert.Equal(66.667, plumber.Salary, precision: 3);
        }

测试案例4:NULL值

[Fact]
        public void NotHaveNameByDefault()
        {
            var plumber = new Plumber();
            Assert.Null(plumber.Name);
        }

        [Fact]
        public void HaveNameValue()
        {
            var plumber = new Plumber
            {
                Name = "Brian"
            };
            Assert.NotNull(plumber.Name);
        }

测试案例5:Collection Assert Contains/NotContains/Equal/All

[Fact]
        public void HaveScrewdriver()
        {
            var plumber = new Plumber();
            Assert.Contains("螺丝刀", plumber.Tools);
        }

[Fact]
        public void NotHaveKeyboard()
        {
            var plumber = new Plumber();
            Assert.DoesNotContain("键盘", plumber.Tools);
        }

//两个集合比较
[Fact]
        public void HaveAllTools()
        {
            var plumber = new Plumber();
            var expectedTools = new []
            {
                "螺丝刀",
                "扳子",
                "钳子"
            };
            Assert.Equal(expectedTools, plumber.Tools);
        }

//集合每个元素比较
[Fact]
        public void HaveNoEmptyDefaultTools()
        {
            var plumber = new Plumber();
            Assert.All(plumber.Tools, t => Assert.False(string.IsNullOrEmpty(t)));
        }

测试案例6:Object类型

IsType(xxx) IsAssignableFrom<父类>(xxx) NotSame(a,b)是否不是同一实例 Same是同一实例

namespace Hospital.Tests
{
    public class WorkerShould
    {
        [Fact]
        public void CreatePlumberByDefault()
        {
            var factory = new WorkerFactory();
            Worker worker = factory.Create("Nick");
            Assert.IsType<Plumber>(worker);
        }
        
         [Fact]
        public void TestIsAssignalbeFrom()
        {
            var factory = new WorkerFactory();
            var p1 = factory.Create("Nike", true);
            Assert.IsAssignableFrom<Worker>(p1);
        }
        
        [Fact]
        public void SameInstance()
        {
            var factory = new WorkerFactory();
            var p1 = factory.Create("Nike");
            var p2 = factory.Create("Jack");
            Assert.NotSame(p1, p2);
            //Assert.Same(p1, p2);
        }
    }
}Ty

测试案例7:异常

namespace Hospital
{
    public class WorkerFactory
    {
        public Worker Create(string name, bool isProgrammer = false)
        {
            if (name == null)
            {
                throw new ArgumentNullException(nameof(name));
            }
            if (isProgrammer)
            {
                return new Programmer { Name = name };
            }
            return new Plumber { Name = name };
        }
    }
    
     public abstract class Worker
    {
        public string Name { get; set; }

        public abstract double TotalReward { get; }
        public abstract double Hours { get; }
        public double Salary => TotalReward / Hours;

        public List<string> Tools { get; set; }
    }

    public class Plumber : Worker
    {
        public Plumber()
        {
            Tools = new List<string>()
            {
                "螺丝刀",
                "扳子",
                "钳子"
            };
        }

        public override double TotalReward => 200;
        public override double Hours => 3;
    }
    
     public class Programmer : Worker
    {
        public override double TotalReward => 1000;
        public override double Hours => 3.5;
    }
}
[Fact]
        public void NotAllowNullName()
        {
            var factory = new WorkerFactory();            // var p = factory.Create(null); // 这个会失败
            Assert.Throws<ArgumentNullException>(() => factory.Create(null));
        }

[Fact]
        public void NotAllowNullName()
        {
            var factory = new WorkerFactory();
            // Assert.Throws<ArgumentNullException>(() => factory.Create(null));
            Assert.Throws<ArgumentNullException>("name", () => factory.Create(null));
        }

[Fact]
        public void NotAllowNullNameAndUseReturnedException()
        {
            var factory = new WorkerFactory();
            ArgumentNullException ex = Assert.Throws<ArgumentNullException>(() => factory.Create(null));
            Assert.Equal("name", ex.ParamName);
        }

测试案例8:Events(Is raised)

public void Sleep()
        {
            OnPatientSlept();
        }

        public event EventHandler<EventArgs> PatientSlept;

        protected virtual void OnPatientSlept()
        {
            PatientSlept?.Invoke(this, EventArgs.Empty);
        }
[Fact]
        public void RaiseSleptEvent()
        {
            var p = new Patient();
            Assert.Raises<EventArgs>(
                handler => p.PatientSlept += handler, 
                handler => p.PatientSlept -= handler, 
                () => p.Sleep());
        }
 namespace Hospital
{
    public class Patient: INotifyPropertyChanged
    {
        public Patient()
        {
            IsNew = true;
            _bloodSugar = 5.0f;
        }

        public string FirstName { get; set; }
        public string LastName { get; set; }
        public string FullName => $"{FirstName} {LastName}";
        public int HeartBeatRate { get; set; }
        public bool IsNew { get; set; }

        private float _bloodSugar;
        public float BloodSugar
        {
            get => _bloodSugar;
            set => _bloodSugar = value;
        }

        public void HaveDinner()
        {
            var random = new Random();
            _bloodSugar += (float)random.Next(1, 1000) / 1000;
            OnPropertyChanged(nameof(BloodSugar));
        }

        public void IncreaseHeartBeatRate()
        {
            HeartBeatRate = CalculateHeartBeatRate() + 2;
        }

        private int CalculateHeartBeatRate()
        {
            var random = new Random();
            return random.Next(1, 100);
        }

        public void Sleep()
        {
            OnPatientSlept();
        }

        public event EventHandler<EventArgs> PatientSlept;

        protected virtual void OnPatientSlept()
        {
            PatientSlept?.Invoke(this, EventArgs.Empty);
        }

        public event PropertyChangedEventHandler PropertyChanged;

        protected virtual void OnPropertyChanged([CallerMemberName] string propertyName = null)
        {
            PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
        }
    }
}
[Fact]
        public void RaisePropertyChangedEvent()
        {
            var p = new Patient();
            Assert.PropertyChanged(p, "BloodSugar", () => p.HaveDinner());
        }