单元测试 - 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
算了,这个有些不好没有返回值的写起来不方便,可以看看就行了