单元测试的两种方式
在单元测试中,可通过两种方式来验证代码是否正确地工作。一种是基于结果状态的测试,一种是基于交互行为的测试。
测试结果与测试行为之间有什么区别呢?
基于结果状态的测试,也就意味着我们需要验证被测试代码需要返回正确的结果。
1 [TestMethod] 2 public void TestSortNumberResult() 3 { 4 IShellSorter<int> shellSorter = new ShellSorter<int>(); 5 IBubbleSorter<int> bubbleSorter = new BubbleSorter<int>(); 6 7 NumberSorter numberSorter = new NumberSorter(shellSorter, bubbleSorter); 8 int[] numbers = new int[] { 3, 1, 2 }; 9 numberSorter.Sort(numbers); 10 11 // 验证返回值是否已经被正确排序。 12 // 只要返回值正确即可,并不关心使用了哪个算法。 13 CollectionAssert.AreEqual(new int[] { 1, 2, 3 }, numbers); 14 }
基于交互行为的测试,也就意味着我们需要验证被测试代码是否正确合理地调用了某些方法。
1 [TestMethod] 2 public void TestUseCorrectSortingAlgorithm() 3 { 4 IShellSorter<int> mockShellSorter = Substitute.For<IShellSorter<int>>(); 5 IBubbleSorter<int> mockBubbleSorter = Substitute.For<IBubbleSorter<int>>(); 6 7 NumberSorter numberSorter = new NumberSorter(mockShellSorter, mockBubbleSorter); 8 int[] numbers = new int[] { 3, 1, 2 }; 9 numberSorter.Sort(numbers); 10 11 // 验证排序器是否使用冒泡排序算法。 12 // 如果排序器未使用冒泡排序算法,或者使用了该算法但传递了错误的参数,则验证失败。 13 mockBubbleSorter.Received().Sort(Arg.Is<int[]>(numbers)); 14 }
第二种测试方法可能会得出较好的代码覆盖率,但它却没有告诉我们排序结果是否正确,而只是确认调用了 bubbleSorter.Sort() 方法。所以交互行为测试并不能证明代码可以正确工作。这也就是在大多数情况下,我们需要测试结果和状态,而不是测试交互和行为。
通常来说,如果程序的正确性不能仅仅靠程序的输出结果来决定,而还需要判断结果是怎么产生的,在这种条件下,我们就需要对交互和行为进行测试。在上面的示例中,你可能想在得到正确测试结果的前提下,额外的再测试下交互行为,因为可能确认正确地使用了某种算法非常重要,例如某些算法在给定条件下运行速度更快,否则的话测试交互行为的意义并不大。
通常在什么条件下需要对交互行为进行测试呢?
这里给出两种较适合的场景:
- 假设被测试代码需要调用了一个方法,但可能由于其被调用的次数不同,或者被调用的顺序不同,而导致产生了不同的结果,或者出现了其他类似时间延迟、多线程死锁等副作用。例如该方法负责发送邮件,我们需要确认只调用了一次邮件发送函数。或者例如该方法的不同调用顺序会产生不同的线程锁控制,导致死锁。在类似这些情况下,测试交互行为可以有效地帮助你确认方法调用是否正确。
- 假设我们在测试一个UI程序,其中已经通过抽象将UI渲染部分与UI逻辑部分隔离,可以考虑是某种MVC或MVVM模式。那么在我们测试 Controller 或 ViewModel 层时,如果有的话,可能只关心 View 上的哪些方法被调用了,而并不关系具体该方法内部是如何渲染的,所以此处测试与 View 的交互就比较合适。类似的,对于 Model 层也一样。
完整代码
1 [TestClass] 2 public class UnitTestTwoWays 3 { 4 public interface IShellSorter<T> 5 where T : IComparable 6 { 7 void Sort(T[] list); 8 } 9 10 public interface IBubbleSorter<T> 11 where T : IComparable 12 { 13 void Sort(T[] list); 14 } 15 16 public class ShellSorter<T> : IShellSorter<T> 17 where T : IComparable 18 { 19 public void Sort(T[] list) 20 { 21 int inc; 22 23 for (inc = 1; inc <= list.Length / 9; inc = 3 * inc + 1) ; 24 25 for (; inc > 0; inc /= 3) 26 { 27 for (int i = inc + 1; i <= list.Length; i += inc) 28 { 29 T t = list[i - 1]; 30 int j = i; 31 32 while ((j > inc) && (list[j - inc - 1].CompareTo(t) > 0)) 33 { 34 list[j - 1] = list[j - inc - 1]; 35 j -= inc; 36 } 37 38 list[j - 1] = t; 39 } 40 } 41 } 42 } 43 44 public class BubbleSorter<T> : IBubbleSorter<T> 45 where T : IComparable 46 { 47 public void Sort(T[] list) 48 { 49 int i, j; 50 bool done = false; 51 52 j = 1; 53 while ((j < list.Length) && (!done)) 54 { 55 done = true; 56 57 for (i = 0; i < list.Length - j; i++) 58 { 59 if (list[i].CompareTo(list[i + 1]) > 0) 60 { 61 done = false; 62 T t = list[i]; 63 list[i] = list[i + 1]; 64 list[i + 1] = t; 65 } 66 } 67 68 j++; 69 } 70 } 71 } 72 73 public interface INumberSorter 74 { 75 void Sort(int[] numbers); 76 } 77 78 public class NumberSorter : INumberSorter 79 { 80 private IShellSorter<int> _shellSorter; 81 private IBubbleSorter<int> _bubbleSorter; 82 83 public NumberSorter( 84 IShellSorter<int> shellSorter, 85 IBubbleSorter<int> bubbleSorter) 86 { 87 _shellSorter = shellSorter; 88 _bubbleSorter = bubbleSorter; 89 } 90 91 public void Sort(int[] numbers) 92 { 93 _bubbleSorter.Sort(numbers); 94 } 95 } 96 97 [TestMethod] 98 public void TestSortNumberResult() 99 { 100 IShellSorter<int> shellSorter = new ShellSorter<int>(); 101 IBubbleSorter<int> bubbleSorter = new BubbleSorter<int>(); 102 103 NumberSorter numberSorter = new NumberSorter(shellSorter, bubbleSorter); 104 int[] numbers = new int[] { 3, 1, 2 }; 105 numberSorter.Sort(numbers); 106 107 // 验证返回值是否已经被正确排序。 108 // 只要返回值正确即可,并不关心使用了哪个算法。 109 CollectionAssert.AreEqual(new int[] { 1, 2, 3 }, numbers); 110 } 111 112 [TestMethod] 113 public void TestUseCorrectSortingAlgorithm() 114 { 115 IShellSorter<int> mockShellSorter = Substitute.For<IShellSorter<int>>(); 116 IBubbleSorter<int> mockBubbleSorter = Substitute.For<IBubbleSorter<int>>(); 117 118 NumberSorter numberSorter = new NumberSorter(mockShellSorter, mockBubbleSorter); 119 int[] numbers = new int[] { 3, 1, 2 }; 120 numberSorter.Sort(numbers); 121 122 // 验证排序器是否使用冒泡排序算法。 123 // 如果排序器未使用冒泡排序算法,或者使用了该算法但传递了错误的参数,则验证失败。 124 mockBubbleSorter.Received().Sort(Arg.Is<int[]>(numbers)); 125 } 126 }
关于单元测试 mocking 技术,请参考《NSubstitute完全手册》。