基础

意义

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

    分类

    01 导读 - 图1
    纵轴代码测试的深度,横轴代表测试的广度(覆盖度)。

  • 单元测试(Unit Test):可以测试一个类或者一个类的某个功能,具有很好的深度,但对于整个应用或者功能没有很好的覆盖面。

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

    测试三阶段AAA

  • Arrange: 这里做一些先决的设定,例如创建对象实例、数据、输入等。

  • Act:在这里执行生产代码并返回结果,例如调用方法或设置属性。
  • Assert:在这里检查结果,测试通过或失败。

    xUnit.NET

    基本概念

    https://github.com/xunit/xunit
    image.png
    xUnit是一个测试框架,可以针对.NET或.NET Core项目进行测试。测试项目需引用被测项目和xUnit库。测试用例编写好后,用Test Runner来运行测试。目前可用的Test Runner包括vs自带的Test Explorer或者dotnet test命令行,以及第三方工具(resharper)等。各参数的含义:https://docs.microsoft.com/zh-cn/dotnet/core/tools/dotnet-test
    xUnit支持的平台:.net full, uwp,xamarin
    1. dotnet test -用于执行单元测试的.NET测试驱动程序。
    2. dotnet test [<PROJECT> | <SOLUTION> | <DIRECTORY> | <DLL>]
    3. [-a|--test-adapter-path <ADAPTER_PATH>] [--blame] [--blame-crash]
    4. [--blame-crash-dump-type <DUMP_TYPE>] [--blame-crash-collect-always]
    5. [--blame-hang] [--blame-hang-dump-type <DUMP_TYPE>]
    6. [--blame-hang-timeout <TIMESPAN>]
    7. [-c|--configuration <CONFIGURATION>]
    8. [--collect <DATA_COLLECTOR_NAME>]
    9. [-d|--diag <LOG_FILE>] [-f|--framework <FRAMEWORK>]
    10. [--filter <EXPRESSION>] [--interactive]
    11. [-l|--logger <LOGGER>] [--no-build]
    12. [--nologo] [--no-restore] [-o|--output <OUTPUT_DIRECTORY>]
    13. [-r|--results-directory <RESULTS_DIR>] [--runtime <RUNTIME_IDENTIFIER>]
    14. [-s|--settings <SETTINGS_FILE>] [-t|--list-tests]
    15. [-v|--verbosity <LEVEL>] [[--] <RunSettings arguments>]
    16. dotnet test -h|--help
    示例:
    1. dotnet test 运行当前目录所含项目中的测试
    2. dotnet test ~/projects/test1/test1.csproj 运行test1项目中的测试
    3. dotnet test ~/projects/test1/bin/debug/test1.dll 运行test1.dll程序集中的测试
    4. dotnet test --logger trx 在当前目录运行项目中的测试,并以trx格式生成测试结果文件
    5. dotnet test --logger "console;verbosity=detailed" 记录详细的测试结果日志到控制台上
    6. dotnet test --collect:"Code Coverage" 这个需要安装Coverlet并进行一定的配置,默认配置会生成名字为Code Coverage的.coverage文件,用于收集测试代码覆盖率。
    7. dotnet test --blame 在当前目录下的项目中运行测试,并报告在测试主机发生故障时正在进行的测试
    8. dotnet test --filter Method 运行FullyQualifiedName包含Method的测试
    9. dotnet test --filter Name~TestMethod1 运行名称包含TestMethod1的测试
    10. dotnet test --filter FullyQualifiedName!=MSTestNamespace.UnitTest1.TestMethod1 运行除 MSTestNamespace.UnitTest1.TestMethod1 之外的其他所有测试。
    11. dotnet test --filter Category=CategoryA 运行含 [TestCategory("CategoryA")] 批注的测试。
    12. 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() { }

  1. [TestMethod, Priority(2)]
  2. public void TestMethod2()
  3. {
  4. }
  5. }

}

  1. <a name="FUFK7"></a>
  2. ## 安装配置xUnit.NET
  3. - 创建一个albertxunit类库项目(dotnet new classlib -n albertxunit)
  4. - 创建xUnit Test项目(albertxunit.Test) (dotnet new xunit -n test_albertxunit)
  5. (dotnet add reference ../albertxunit/albertxunit.csproj)<br />(dotnet sln add test_albertxunit\test_albertxunit.csproj)<br />![image.png](https://cdn.nlark.com/yuque/0/2022/png/957395/1647850566235-a17d6c9e-3452-4abe-8a77-f306b04b61f2.png#clientId=u54e1043e-0ed3-4&crop=0&crop=0&crop=1&crop=1&from=paste&height=711&id=ub959b8fa&margin=%5Bobject%20Object%5D&name=image.png&originHeight=1422&originWidth=2120&originalType=binary&ratio=1&rotation=0&showTitle=false&size=174815&status=done&style=none&taskId=ud0e64830-9f92-432a-ae19-27af2edb6b6&title=&width=1060)<br />![image.png](https://cdn.nlark.com/yuque/0/2022/png/957395/1647850769870-8dcf225c-a5fd-4cc9-ad07-4dcdcdea2f24.png#clientId=u54e1043e-0ed3-4&crop=0&crop=0&crop=1&crop=1&from=paste&height=251&id=u1dfc3c29&margin=%5Bobject%20Object%5D&name=image.png&originHeight=502&originWidth=692&originalType=binary&ratio=1&rotation=0&showTitle=false&size=46775&status=done&style=none&taskId=u9ba17501-815c-408a-b037-b68c92fffe0&title=&width=346)<br />创建完成后,默认引用了这四个Packages,其中coverlet.collector是用于收集代码测试覆盖率的。(dotnet test --collect:"Code Coverage" 这个需要安装Coverlet并进行一定的配置,默认配置会生成名字为Code Coverage的.coverage文件,用于收集测试代码覆盖率。)
  6. - 引用待测项目,从Test Explorer可以看到待测项目
  7. ![image.png](https://cdn.nlark.com/yuque/0/2022/png/957395/1647851141104-c4f43ba2-ebcc-42a3-8b89-ab3beb08022f.png#clientId=u54e1043e-0ed3-4&crop=0&crop=0&crop=1&crop=1&from=paste&height=356&id=ub980fab3&margin=%5Bobject%20Object%5D&name=image.png&originHeight=712&originWidth=2108&originalType=binary&ratio=1&rotation=0&showTitle=false&size=120643&status=done&style=none&taskId=u6050e80f-a6cc-4524-9329-80cf4f2113e&title=&width=1054)
  8. <a name="y6tS2"></a>
  9. ## Assert
  10. Assert(断言)是基于代码的返回值、对象的最终状态、事件是否发生等情况来评估测试的结果。<br />xUnit提供了以下类型的Assert:
  11. - boolen:True/False
  12. - String:相等/不等, 是否为空,以..开始/结束,是否包含子字符串,匹配正则表达式
  13. - 数值型:相等/不等,是否在某个范围内,浮点的精度
  14. - Collection:内容是否相等,是否包含某个元素,是否包含满足某种条件(predicate)的元素,是否所有的元素都满足某个assert
  15. - Raised events:Custom events,Framework events(例如:PropertyChanged)
  16. - Object Type:是否是某种类型,是否某种类型或继承与某种类型
  17. Tips一般每个Test方法中只有一个assert, 如果一个test里面有多个asserts,只要这些assert都是针对同一个行为的即可。
  18. <a name="k5995"></a>
  19. ### 测试案列1-boolen
  20. ```csharp
  21. public class Patient
  22. {
  23. public Patient()
  24. {
  25. IsNew = true;
  26. }
  27. public string FirstName { get; set; }
  28. public string LastName { get; set; }
  29. public string FullName => $"{FirstName} {LastName}";
  30. public int HeartBeatRate { get; set; }
  31. public bool IsNew { get; set; }
  32. public void IncreaseHeartBeatRate()
  33. {
  34. HeartBeatRate = CalculateHeartBeatRate() + 2;
  35. }
  36. private int CalculateHeartBeatRate()
  37. {
  38. var random = new Random();
  39. return random.Next(1, 100);
  40. }
  41. }
public class PatientShould
    {
        [Fact]
        public void HaveHeartBeatWhenNew()
        {
            var patient = new Patient();

            Assert.True(patient.IsNew);
        }
    }

运行测试,进入到项目目录下,dotnet test运行整个测试
image.png
只运行单个方法 dotnet test —filter TestIsNew
image.png
生成测试日志 dotnet test —logger trx
image.png
使用命令行将详细日志信息打印出来 dotnet test —logger “console;verbosity=detailed” detailed/minimal
image.png
生成测试覆盖率文件 dotnet test —collect “Code Coverage”
image.png

测试案例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,测试结果
image.png

测试案例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());
        }