C#

C# Unit Test moq mock的使用

Posted by Kerwen Blog on November 23, 2023

moq提供了mock,使我们在做C# unit test时可以mock接口,从而减少外部依赖。

假设我们有个加法计算器程序,

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public class CalculateImp
{
    private const string calculateProgID = "Calculate.CalculateManager";
    public int DoCalculate()
    {
        var cal = GetCalculateInstance();
        return cal.Sum(1, 2);
    }

    public virtual CalculateManager GetCalculateInstance()
    {
        Type calculateManagerType = Type.GetTypeFromProgID(calculateProgID);
        CalculateManager calculate = (CalculateManager)Activator.CreateInstance(calculateManagerType);
        return calculate;
    }
}

它依赖于一个外部接口 CalculateManager, 这个dll以COM的形式注册,所以我们需要调用Activator.CreateInstance来初始化dll。 CalculateManager的代码如下:

1
2
3
4
5
6
7
public class CalculateManager
{
    public virtual int Sum(int a, int b)
    {
        return a + b;
    }
}

当我们测试DoCalculate时,首先要做的是隔离外部依赖,将CalculateManager的初始化放到一个单独的虚函数GetCalculateInstance中。然后使用mock做一个假的接口.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
[TestClass()]
public class CalculateImpTests:CalculateImp
{
    public override CalculateManager GetCalculateInstance()
    {
        var mock = new Mock<CalculateManager>();
        mock.Setup(r => r.Sum(1, 2)).Returns(10);
        return mock.Object;
    }

    [TestMethod()]
    public void DoCalculateTest()
    {
        int result = base.DoCalculate();
        Assert.AreEqual(result, 10);
    }
}

如果接口的参数中有引用参数,则Unit test需要加Callback,测试也就更复杂一些。

1
2
3
4
5
6
7
public class CalculateManager
{
    public virtual void Sum2(int a, int b, ref int amount)
    {
        amount = a + b;
    }
}

使用ref参数来获取最终的结果

1
2
3
4
5
6
7
8
9
10
11
12
public class CalculateImp
{
    public int DoCalculate2()
    {
        var cal = GetCalculateInstance();
        int result = 0;
        int a = 1;
        int b = 2;
        cal.Sum2(a, b, ref result);
        return result;
    }
}

如果对DoCalculate2写单元测试,则需要增加callback

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
[TestClass()]
public class CalculateImpTests:CalculateImp
{
    delegate void Sum2Callback(int a, int b, ref int amount);

    public override CalculateManager GetCalculateInstance()
    {
        var mock = new Mock<CalculateManager>();
        mock.Setup(m => m.Sum2(1, 2, ref It.Ref<int>.IsAny))  // match any value passed by-ref
            .Callback(new Sum2Callback((int a, int b, ref int amount) =>
            {
                amount = 33;
            }));

        return mock.Object;
    }

    [TestMethod()]
    public void DoCalculate2Test()
    {
        int result = base.DoCalculate2();
        Assert.AreEqual(result, 33);
    }
}

这是在声明mock的时候用到了ref It.Ref<int>.IsAny

如果同时有引用参数和返回值的接口:

1
2
3
4
5
6
7
8
public class CalculateManager
{
    public virtual bool Sum3(int a, int b, ref int amount)
    {
        amount = a + b;
        return true;
    }
}

调用sum3

1
2
3
4
5
6
7
8
9
10
11
12
public class CalculateImp
{
    public int DoCalculate3()
    {
        var cal = GetCalculateInstance();
        int result = 0;
        int a = 1;
        int b = 2;
        cal.Sum3(a, b, ref result);
        return result;
    }
}

针对DoCalculate3,我们需要在callback的基础上加Returns

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
[TestClass()]
public class CalculateImpTests:CalculateImp
{
    delegate void Sum3Callback(int a, int b, ref int amount);
    delegate bool Sum3Returns(int a, int b, ref int amount);

    public override CalculateManager GetCalculateInstance()
    {
        var mock = new Mock<CalculateManager>();
        mock.Setup(m => m.Sum3(1, 2, ref It.Ref<int>.IsAny))  
            .Callback(new Sum3Callback((int a, int b, ref int amount) =>
            {
                amount = 61;
            }))
            .Returns(new Sum3Returns((int a, int b, ref int amount) => true));

        return mock.Object;
    }

    [TestMethod()]
    public void DoCalculate3Test()
    {
        int result = base.DoCalculate3();
        Assert.AreEqual(result, 61);
    }
}

完整代码如下

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
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
public class CalculateManager
{
    public virtual int Sum(int a, int b)
    {
        return a + b;
    }

    public virtual void Sum2(int a, int b, ref int amount)
    {
        amount = a + b;
    }
    public virtual bool Sum3(int a, int b, ref int amount)
    {
        amount = a + b;
        return true;
    }
}

public class CalculateImp
{
    private const string calculateProgID = "Calculate.CalculateManager";
    public int DoCalculate()
    {
        var cal = GetCalculateInstance();
        return cal.Sum(1, 2);
    }
    public int DoCalculate2()
    {
        var cal = GetCalculateInstance();
        int result = 0;
        int a = 1;
        int b = 2;
        cal.Sum2(a, b, ref result);
        return result;
    }
    public int DoCalculate3()
    {
        var cal = GetCalculateInstance();
        int result = 0;
        int a = 1;
        int b = 2;
        cal.Sum3(a, b, ref result);
        return result;
    }

    public virtual CalculateManager GetCalculateInstance()
    {
        Type calculateManagerType = Type.GetTypeFromProgID(calculateProgID);
        CalculateManager calculate = (CalculateManager)Activator.CreateInstance(calculateManagerType);
        return calculate;
    }
}

[TestClass()]
public class CalculateImpTests:CalculateImp
{
    delegate void Sum2Callback(int a, int b, ref int amount);

    delegate void Sum3Callback(int a, int b, ref int amount);
    delegate bool Sum3Returns(int a, int b, ref int amount);

    public override CalculateManager GetCalculateInstance()
    {
        var mock = new Mock<CalculateManager>();
        mock.Setup(r => r.Sum(1, 2)).Returns(10);

        mock.Setup(m => m.Sum2(1, 2, ref It.Ref<int>.IsAny))  
            .Callback(new Sum2Callback((int a, int b, ref int amount) =>
            {
                amount = 33;
            }));

        mock.Setup(m => m.Sum3(1, 2, ref It.Ref<int>.IsAny))  
            .Callback(new Sum3Callback((int a, int b, ref int amount) =>
            {
                amount = 61;
            }))
            .Returns(new Sum3Returns((int a, int b, ref int amount) => true));

        return mock.Object;
    }

    [TestMethod()]
    public void DoCalculateTest()
    {
        int result = base.DoCalculate();
        Assert.AreEqual(result, 10);
    }

    [TestMethod()]
    public void DoCalculate2Test()
    {
        int result = base.DoCalculate2();
        Assert.AreEqual(result, 33);
    }

    [TestMethod()]
    public void DoCalculate3Test()
    {
        int result = base.DoCalculate3();
        Assert.AreEqual(result, 61);
    }
}

Reference

Method calls