单元测试 - Moq

测试常用方法

// 准备 Mock IFoo 接口
var mock = new Mock<IFoo>();
// 配置准备模拟的方法,当调用接口中的 DoSomething 方法,并传递参数 "bing" 的时候,返回 true
mock.Setup(foo => foo.DoSomething("ping")).Returns(true);
// 方法的参数中使用了 out 参数
// out arguments
var outString = "ack";
// 当调用 TryParse 方法的时候,out 参数返回 "ack", 方法返回 true, lazy evaluated
mock.Setup(foo => foo.TryParse("ping", out outString)).Returns(true);
// ref 参数
var instance = new Bar();
// 仅仅在使用 ref 调用的时候,才会匹配下面的测试
mock.Setup(foo => foo.Submit(ref instance)).Returns(true);
// 当方法返回值得时候,还可以访问返回的值
// 这里可以使用多个参数
mock.Setup(x => x.DoSomething(It.IsAny<string>()))
.Returns((string s) => s.ToLower());
// 在被调用的时候抛出异常
mock.Setup(foo => foo.DoSomething("reset")).Throws<InvalidOperationException>();
mock.Setup(foo => foo.DoSomething("")).Throws(new ArgumentException("command");
// 延迟计算返回的结果
mock.Setup(foo => foo.GetCount()).Returns(() => count);
// 在每一次调用的时候,返回不同的值
var mock = new Mock<IFoo>();
var calls = 0;
mock.Setup(foo => foo.GetCountThing())
.Returns(() => calls)
.Callback(() => calls++);
// 第一次调用返回 0, 下一次是 1, 依次类推
Console.WriteLine(mock.Object.GetCountThing());

1)Mock 方法

预备数据

/// <summary>
    /// 球员转会申请类
    /// </summary>
    public class TransferApplication
    {
        public int Id { get; set; }
        /// <summary>
        /// 球员名字
        /// </summary>
        public string PlayrName { get; set; }
        /// <summary>
        /// 年龄
        /// </summary>
        public int PlayrAge { get; set; }
        /// <summary>
        /// 转会费(百万)
        /// </summary>
        public decimal TransferFee { get; set; }
        /// <summary>
        /// 年薪(百万)
        /// </summary>
        public decimal AnnualSalary { get; set; }
        /// <summary>
        /// 合同年限
        /// </summary>
        public int ContractYears { get; set; }
        /// <summary>
        /// 是否超级巨星
        /// </summary>
        public bool IsSuperStar { get; set; }

        /// <summary>
        /// 球员的力量
        /// </summary>
        public int PlayerStrength { get; set; }
        /// <summary>
        /// 球员的速度
        /// </summary>
        public int PlayerSpeed { get; set; }
    }

    /// <summary>
    ///  转会审批结果
    /// </summary>
    public enum TransferResult
    {
        Approved,
        Rejected,
        ReferredToBoss
    }

TransferApproval

    public class TransferApproval
    {
        /// <summary>
        /// 剩余预算(百万)
        /// </summary>
        private const int RemainingTotalBudget = 300;
        private readonly IPhysicalExamination _physicalExamination;


        public TransferApproval(IPhysicalExamination physicalExamination)
        {
            _physicalExamination = physicalExamination ?? throw new ArgumentNullException(nameof(physicalExamination));
        }

        /// <summary>
        /// 评估
        /// </summary>
        /// <param name="transfer"></param>
        /// <returns></returns>
        public TransferResult Evaluate(TransferApplication transfer)
        {
            //var isHealthy = _physicalExamination.IsHealthy(transfer.PlayrAge, transfer.PlayerStrength, transfer.PlayerSpeed);
             _physicalExamination.IsHealthy(transfer.PlayrAge, transfer.PlayerStrength, transfer.PlayerSpeed,out var isHealthy);

            if (!isHealthy)
            {
                return TransferResult.Rejected;
            }

            var totalTransferFee = transfer.TransferFee + transfer.ContractYears * transfer.AnnualSalary;
            if (RemainingTotalBudget < totalTransferFee)
            {
                return TransferResult.Rejected;
            }
            if (transfer.PlayrAge < 30)
            {
                return TransferResult.Approved;
            }
            if (transfer.IsSuperStar)
            {
                return TransferResult.ReferredToBoss;

            }
            return TransferResult.Rejected;
        }
    }

IPhysicalExamination 接口

    public interface IPhysicalExamination
    {
        bool IsHealthy(int age, int strength, int speed);
        void IsHealthy(int age, int strength, int speed, out bool isHealthy);
    }

PhysicalExamination 类

    public class PhysicalExamination:IPhysicalExamination
    {
         public bool IsHealthy(int age, int strength, int speed)
        {
            throw new NotImplementedException();
        }

        public void IsHealthy(int age, int strength, int speed, out bool isHealthy)
        {
            throw new NotImplementedException();
        }
    }   

而由于Moq对依赖项进行了包装, 所以要获得实际的mock依赖项, 我们需要使用mockExamination.Object属性. 而这个属性的类型就是IPhysicalExamination.

Mock<IPhysicalExamination> mockExamination = new Mock<IPhysicalExamination>();

var approval = new TransferApproval(mockExamination.Object);

1.1)It类

这里用到了It这个类, 在Moq里, It这个类是用来做参数匹配的, it 就是"它"的意思, 它就代表需要被匹配的参数.

It.IsAny<T>(), 它表示传递给方法的参数的类型只要是T就可以, 值是任意的. 只要满足了这个条件, 那么方法的返回值就是后边Returns()方法里设定的值.

Mock<IPhysicalExamination> mockExamination = new Mock<IPhysicalExamination>();

mockExamination.Setup(x => x.IsHealthy(It.IsAny<int>(), It.IsAny<int>(), It.IsAny<int>())).Returns(true);

var approval = new TransferApproval(mockExamination.Object);

It 有下面几种用法:

  • Is<TValue>(Expression<Func<TValue, Boolean>>)

可以接受一个表达式作为参数,并指定参数应该满足的条件。在Moq中,参数匹配器用于定义模拟对象应该如何响应特定输入的情况。

mock.Setup(x => x.MyMethod(It.Is(value => value > 10))).Returns(true);

并指定一个条件表达式 value => value > 10,我们告诉模拟对象只有当传入的参数大于10时才返回 true。

  • IsAny<TValue>()
  • IsIn<TValue>(IEnumerable<TValue>)
var validValues = new List<int> { 1, 2, 3 };

// 设置模拟对象的行为
mock.Setup(x => x.MyMethod(It.IsIn(validValues))).Returns(true);

// 在测试中使用模拟对象
bool result1 = mock.Object.MyMethod(2);  // 参数匹配成功,返回 true
bool result2 = mock.Object.MyMethod(5);  // 参数匹配失败,返回默认值 (false)
  • IsInRange<TValue>(TValue, TValue, Range)
  • IsNotIn<TValue>(IEnumerable<TValue>)
  • IsNotNull<TValue>()
  • IsRegex(string)

2)Mock 属性

添加属性 IsMedicalRoomAvaiable

    public interface IPhysicalExamination
    {
        bool IsHealthy(int age, int strength, int speed);
        void IsHealthy(int age, int strength, int speed, out bool isHealthy);
        /// <summary>
        /// 体检室是否可以使用
        /// </summary>
        bool IsMedicalRoomAvailable { get; set; }
    }

PhysicalExamination 实现

    public class PhysicalExamination:IPhysicalExamination
    {
         public bool IsHealthy(int age, int strength, int speed)
        {
            throw new NotImplementedException();
        }

        public void IsHealthy(int age, int strength, int speed, out bool isHealthy)
        {
            throw new NotImplementedException();
        }

        public bool IsMedicalRoomAvailable
        {
            get => throw new NotImplementedException();
            set => throw new NotImplementedException();
        }
    }

添加一个推迟的结果

添加一个判断

    public TransferResult Evaluate(TransferApplication transfer)
        {
            if (!_physicalExamination.IsMedicalRoomAvailable)
            {
                return TransferResult.PostPoned;
            }

//....

下面是设置属性

 public void AprroveYoungCheapPlayerTransfer()
        {
            Mock<IPhysicalExamination> mockExamination = new Mock<IPhysicalExamination>();

            mockExamination.Setup(x => x.IsMedicalRoomAvailable).Returns(true); //设置属性
            //mockExamination.Setup(x => x.IsHealthy(It.IsAny<int>(), It.IsAny<int>(), It.IsAny<int>())).Returns(true);
            bool isHealthy = true;
            mockExamination.Setup(x => x.IsHealthy(It.IsAny<int>(), It.IsAny<int>(), It.IsAny<int>(), out isHealthy));

2.1)递归

var mock = new Mock<IFoo> { DefaultValue = DefaultValue.Mock }; 

// 默认是 DefaultValue.Empty  
// 现在这个属性将会返回一个新的 Mock 对象 
IBar value = mock.Object.Bar;  

// 可以使用返回的 Mock 对象, 后即对属性的访问返回相同的对象实例
// 这就允许我们可以进行后继的设置  
// set further expectations on it if we want 
var barMock = Mock.Get(value); 
barMock.Setup(b => b.Submit()).Returns(true);

2.2)属性值变化跟踪

上面的代码也就是说, 我的mock对象的某个属性在测试的时候它的值会发生变化. 而Moq可以记住这些mock属性的变化的值.....

新写一个测试:

这里使用mockObj.SetupProperty()方法来开始追踪属性. 这个测试会通过。

2.2.1)属性值设置默认值 mock.SetupProperty

该方法也可以通过下面的写法来为被追踪的属性设置默认值:

mockExamination.SetupProperty(x => x.PhysicalGrade, PhysicalGrade.Failed);

2.2.2)对所有属性值跟踪 mock.SetupAllProperties

mock.SetupAllProperties();

当你调用 mock.SetupAllProperties() 方法时,Moq 将为模拟对象的所有公共属性自动生成 Getter 和 Setter,并将它们的行为设置为默认行为,以便你可以对这些属性进行读取和设置操作。

注意, 这个方法应该最先调用, 否则的话其它的设置可能会被覆盖.

3)Mock 行为

介绍的是行为测试, 也就是说我们要确认某些方法会被执行或者某些属性被访问了.

3.1) 确认方法是否被调用 mock.Verify

与状态测试不同, 这里我不使用Assert, 我是用的是mock.Verify()来判定其参数里的方法会被执行. 在这里也可以使用It类进行参数匹配.

        [Fact()]
        public void SholdPhysicalExamineWhenTransferringSuperStar()
        {
            Mock<IPhysicalExamination> mockExamination = new Mock<IPhysicalExamination>();

            mockExamination.Setup(x => x.MedicalRoom.Status.IsAvailable).Returns("可用");
            mockExamination.SetupProperty(x => x.PhysicalGrade, PhysicalGrade.Passed);
            bool isHealthy = true;
            mockExamination.Setup(x => x.IsHealthy(It.IsAny<int>(), It.IsAny<int>(), It.IsAny<int>(), out isHealthy));

            var approval = new TransferApproval(mockExamination.Object);

            var cr7Transfer = new TransferApplication
            {
                PlayrName = "Ronalod",
                PlayrAge = 33,
                TransferFee = 112m,
                AnnualSalary = 30m,
                ContractYears = 4,
                IsSuperStar = true,
                PlayerStrength = 90,
                PlayerSpeed = 90
            };

            var result = approval.Evaluate(cr7Transfer);

            //(1)Unit Test: Failed
            //Moq.MockException : 
            //    Expected invocation on the mock at least once, but was never performed: x => x.IsHealthy(33, 95, 88, True)
            mockExamination.Verify(x => x.IsHealthy(33, 95, 88, out isHealthy));

            //(2)Unit Test: Pass
            //mockExamination.Verify(x => x.IsHealthy(It.IsAny<int>(), It.IsAny<int>(), It.IsAny<int>(), out isHealthy));
        }

第1种失败,第2种成功。这是因为IsHealthy()方法被调用时的参数与我所期待的参数不一致.

正确的参数:

第1种应该在调用的时候使用同样的参数。应该是 mockExamination.Verify(x => x.IsHealthy(33, 90, 90, out isHealthy));

3.1.1) mock.Verify 自定义错误信息

3.1.2)mock.Verify 方法被调用次数 Times

3.2)确认属性是否被访问 mock.VerifyGet

        [Fact()]
        public void SholdCheckMedicalRoomIsAvaliableWhenTransferringSuperStar()
        {
            Mock<IPhysicalExamination> mockExamination = new Mock<IPhysicalExamination>();

            mockExamination.Setup(x => x.MedicalRoom.Status.IsAvailable).Returns("可用");
            mockExamination.SetupProperty(x => x.PhysicalGrade, PhysicalGrade.Passed);
            bool isHealthy = true;
            mockExamination.Setup(x => x.IsHealthy(It.IsAny<int>(), It.IsAny<int>(), It.IsAny<int>(), out isHealthy));

            var approval = new TransferApproval(mockExamination.Object);

            var cr7Transfer = new TransferApplication
            {
                PlayrName = "Ronalod",
                PlayrAge = 33,
                TransferFee = 112m,
                AnnualSalary = 30m,
                ContractYears = 4,
                IsSuperStar = true,
                PlayerStrength = 90,
                PlayerSpeed = 90
            };

            var result = approval.Evaluate(cr7Transfer);


            mockExamination.VerifyGet(x => x.MedicalRoom.Status.IsAvailable); //成功
            //Moq.MockException : 
            //    Expected invocation on the mock at least once, but was never performed: x => x.MedicalRoom.Name
            mockExamination.VerifyGet(x => x.MedicalRoom.Name); //没被访问
        }

下面是失败的, 确实没调用 Name

mockExamination.VerifyGet(x => x.MedicalRoom.Name); //没被访问

下面是成功的

mockExamination.VerifyGet(x => x.MedicalRoom.Status.IsAvailable); //成功

3.3)确认属性是否被赋值 mock.VerifySet

在VerifySet方法里需要设定被Set的属性以及被Set的值.

mockExamination.VerifySet(x => x.PhysicalGrade = PhysicalGrade.Passed); 
// 普通属性
mock.Setup(foo => foo.Name).Returns("bar");
// 多层的属性
mock.Setup(foo => foo.Bar.Baz.Name).Returns("baz");
// 期望设置属性的值为 "foo"
mock.SetupSet(foo => foo.Name = "foo");
// 或者直接验证赋值
mock.VerifySet(foo => foo.Name = "foo");

3.4)抛出异常

不使用泛型, 直接抛出异常:

mockExamination.Setup(x => x.IsHealthy(It.Is<int>(age => age < 16), It.IsAny<int>(), It.IsAny<int>(), out isHealthy))
                .Throws(new Exception("the player is still a child.!"));

使用泛型

mockExamination.Setup(x => x.IsHealthy(It.Is<int>(age => age < 16), It.IsAny<int>(), It.IsAny<int>(), out isHealthy))
                .Throws<Exception>();

3.5)Event 事件 mock.Raise

因为该event并没有被触发(PhysicalExamination里并没做什么动作).

这时, 我们可以使用mock对象来触发该事件, 在测试方法里, 手动调用mock对象的Raise()方法:

第一个参数是lambda表达式, 该事件绑定到null, 第二个参数针对本例是EventArgs.Empty即可.

第二种方法是在设置IsHealthy()方法的时候对事件进行触发设定,这样的话只要IsHealthy()方法被调用, 那么HealthChecked这个事件也会被触发.

        /// <summary>
        /// 测试 event
        /// </summary>
        [Fact]
        public void SholdPlayerHealthCheckedWhenTransferingSuperStar()
        {
            Mock<IPhysicalExamination> mockExamination = new Mock<IPhysicalExamination>()
            {
                DefaultValue = DefaultValue.Mock
            };

            mockExamination.SetupProperty(x => x.PhysicalGrade, PhysicalGrade.Failed);
            mockExamination.Setup(x => x.MedicalRoom.Status.IsAvailable).Returns("可用");
            bool isHealthy = true;
            mockExamination.Setup(x => x.IsHealthy(It.IsAny<int>(), It.IsAny<int>(), It.IsAny<int>(), out isHealthy))
                .Raises(x => x.HealthChecked += null, EventArgs.Empty); //方法二:在设置IsHealthy()方法的时候对事件进行触发设定.

            var approval = new TransferApproval(mockExamination.Object);

            var cr7Transfer = new TransferApplication
            {
                PlayrName = "Ronalod",
                PlayrAge = 33,
                TransferFee = 112m,
                AnnualSalary = 30m,
                ContractYears = 4,
                IsSuperStar = true,
                PlayerStrength = 90,
                PlayerSpeed = 90
            };

            var result = approval.Evaluate(cr7Transfer);

            //mockExamination.Raise(x => x.HealthChecked += null, EventArgs.Empty); //方法一:我们可以使用mock对象来触发该事件, 在测试方法里, 手动调用mock对象的Raise()方法.

            Assert.True(approval.PlayerHealthChecked);
        }

3.6)设定连续调用的不同返回值 mock.SetupSequence

3.7)没有接口实现,实现方法

3.8)Protected Virtual 方法

4)回调 mock.Callback

var mock = new Mock<IFoo>();
mock.Setup(foo => foo.Execute("ping"))
.Returns(true)
.Callback(() => calls++);
// 使用调用的参数
mock.Setup(foo => foo.Execute(It.IsAny<string>()))
.Returns(true)
.Callback((string s) => calls.Add(s));
// 使用泛型语法
mock.Setup(foo => foo.Execute(It.IsAny<string>()))
.Returns(true)
.Callback<string>(s => calls.Add(s));
// 使用多个参数
mock.Setup(foo => foo.Execute(It.IsAny<int>(), It.IsAny<string>()))
.Returns(true)
.Callback<int, string>((i, s) => calls.Add(s));
// 调用之前和之后的回调
mock.Setup(foo => foo.Execute("ping"))
.Callback(() => Console.WriteLine("Before returns"))
.Returns(true)
.Callback(() => Console.WriteLine("After returns"));

当使用 Moq 进行模拟对象设置时,可以使用 Callback 方法来定义在调用模拟对象的方法时执行的自定义行为。Callback 方法接受一个或多个参数,这些参数代表模拟对象方法的输入参数,并且可以在方法调用时执行指定的操作。

Callback 方法可以用于执行诸如记录日志、修改参数值、触发事件等自定义操作。下面是 Callback 方法的语法:

mockObject.Setup(x => x.MethodName(It.IsAny<ArgType1>(), It.IsAny<ArgType2>(), ...))
    .Callback<ArgType1, ArgType2, ...>((arg1, arg2, ...) =>
    {
        // 自定义操作,可以根据需要执行任何代码
    });

在上述代码中,mockObject 是模拟对象,MethodName 是要设置行为的模拟对象的方法名。

.Callback 方法接受一个或多个参数,这些参数类型应与模拟对象方法的参数类型匹配。然后可以在回调函数中进行自定义操作。

例如,我们可以在回调函数中修改参数的值:

mockObject.Setup(x => x.MethodName(It.IsAny<int>()))
    .Callback<int>((number) =>
    {
        number = number + 1;
        Console.WriteLine($"Modified number: {number}");
    });

在上述示例中,我们将参数 number 的值加1,并在回调函数中打印出修改后的值。

下面来个具体的示例

 public interface IFoo
    {
        bool Execute(string input);
        bool Execute(int number, string input);
    }

    public class Foo : IFoo
    {
        public bool Execute(string input)
        {
            // 在实际情况下,这里可能会有更复杂的逻辑
            return input == "ping";
        }

        public bool Execute(int number, string input)
        {
            // 在实际情况下,这里可能会有更复杂的逻辑
            return input == "ping" && number > 0;
        }
    }

    public class TestCallbackMethod
    {
        private readonly IFoo foo;

        public TestCallbackMethod(IFoo foo)
        {
            this.foo = foo;
        }

        public static void TestCallbackOut(TestCallbackMethod test)
        {
            test.foo.Execute("ping");
        }
    }


    public class CallbackTest 
    {

        [Fact()]
        public void CallbackTest1()
        {
            var calls = 0;
            var mock = new Mock<IFoo>();
            mock.Setup(foo => foo.Execute("ping"))
            .Returns(true)
            .Callback(() => {
                calls++;
                Console.WriteLine($"Modified calls: {calls}");
            });

            var foo = new TestCallbackMethod(mock.Object);

            TestCallbackMethod.TestCallbackOut(foo);
            Assert.Equal(1, calls); //这里是call 是 1
        }

    }

5)Linq to Mocks - mock.Of

算了,这个有些不好没有返回值的写起来不方便,可以看看就行了

参考

使用 Moq 测试.NET Core 应用 - Why Moq? - yangxu-pro - 博客园

moq 的常用使用方法(推荐)_ASP.NET_猪先飞

posted @ 2023-07-08 19:02  【唐】三三  阅读(59)  评论(0编辑  收藏  举报