单元测试(C#版)

发表于:2018-5-02 13:10

字体: | 上一篇 | 下一篇 | 我要投稿

 作者:丑小丫大笨蛋    来源:简书

  所谓单元测试(unit testing),就是对软件中的最小单元进行检查和验证,其一般验证对象是一个函数或者一个类。值得一提的是,虽然单元测试是开发者为了验证一段代码功能正确性而写的一段代码,但是我们写一个单元测试的出发点并不是针对一段代码或者一个方法,而是针对一个应用场景(scenario),即在某些条件下某个特定的函数的行为。
  1.单元测试的必要性
  单元测试不但会使你的工作完成得更轻松,而且会令你的设计变得更好,甚至大大减少你花在调试上面的时间。
  (1)单元测试能让你确定自己的代码功能和逻辑的正确性,还可以让你增加对程序的信心,并且能够及早发现程序中的不足。
  (2)在写好功能模块之前、之中和之后考虑好单元测试怎么写,不仅可以让你更加清楚你写的功能模块的逻辑,还能及早地改进一些不当的设计。
  (3)每完成一块功能模块就用单元测试进行验证修改bug,比整个软件写完再验证调试要容易得多。而且有了单元测试,在整体软件出问题的时候,我们可以直接对怀疑的某模块在单元测试中进行debug,这往往比调试整个系统要容易得多。
  (4)帮助我们及早地发现问题。有的时候对A的修改可能会影响看起来毫不相关的B,如果没有单元测试,A的修改checkin之后可能就会引发比较严重的问题。而如果在checkin之前能够运行所有的单元测试的话,B的单元测试可能就会发现引入的问题,从而阻止此次不当修改的checkin。
  我想,其实很多程序员都应该知道单元测试重要性的那些大道理,只是要改变它就像要戒掉拖延症一样。明明知道那样不好并发誓下一次改进,却一直没有摆脱掉那些恶习。拜托,不要从明天或者从下一次开始了,就从现在开始吧!当你真正开始去写单元测试并坚持写,你会从中得到好处的,那时候你才会真正领悟到它的必要性。
  2.开始写你的第一个单元测试吧
  我们先来用VS2012中自带的测试模块来写一个简单的单元测试吧。
  新建一个solution,并添加工程MyMathLib,在该工程中添加MyMathLib类,并书写一个静态的Largest()函数来找出一个整型列表中的最大值。然后添加一个TestLargest工程,如图1所示,Add -> New Project 之后选择Test -> Unit Test Project。新建好test工程之后,你会得到一个test模板,即一个带有[TestClass] attribute标记的类和一个带有[TestMethod] attribute标记的空方法public void TestMethod1()。
  Figure 1. Add unit test project
  现在我们的solution就具有了图2中所示的目录结构,打开刚添加的TestLargest工程下的references,我们可以看到它自动引用了Microsoft.VisualStudio.QuanlityTools.UnitTestFramework。
  Figure 2. Projects in the solution
  分别在MyMathLib和TestLargest添加代码如下:
  // MyMathLib.cs
  namespace FirstUnitTest.MyMathLib
  {
      public static class MyMathLib
      {
          public static int Largest(List<int> list)
          {
              int maxNum = Int32.MaxValue;
              foreach (var num in list)
              {
                  if (num > maxNum) maxNum = num;
              }
              return maxNum;
          }
          static void Main(string[] args)
          {
          }
      }
  }

  // UnitTest1.cs
  using FirstUnitTest.MyMathLib;
  namespace FirstUnitTest.Test
  {
      [TestClass]
      public class UnitTest1
      {
          [TestMethod]
          public void TestMethod1()
          {
              var list = new List<int>() { 9, 8, 7 };
              Assert.AreEqual(9, MyMathLib.MyMathLib.Largest(list));
          }
      }
  }```
  写好之后你会发现有编译错误,`cannot resolve MyMathLib.MyMathLib.Largest`,所以我们在`TestLargest`工程里光添加`using FirstUnitTest.MyMathLib;`是不够的,还需要在`references`中增加对`MyMathLib`工程的引用。这样在`TestMethod1()`上单击右键选择`Run Tests`就可以在`Test Explorer`里看到单元测试的运行结果(如`图3`所示)。
  ![Figure 3. Unit test failed](http://upload-images.jianshu.io/upload_images/1764093-1aaee9257486b3a0.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)
  可以看到,我们在单元测试中提供的例子的期望最大值是`9`,运行结果却是`2147483647`。再看一看我们得`Largest`方法,原来是在对`maxNum`进行第一次赋值的时候不小心把`Int32.MinValue`写成了`Int32.MaxValue`。你看,单元测试就是能够发现一些意向不到的错误。不要以为这里的bug很低级,类似的情况确实会在现实中发生。
  我们把上面的错误更正后,再次运行`TestMethod1()`就会得到`test passed`的结果(如`图4`所示)。
  ![Figure 4. Unit test passed](http://upload-images.jianshu.io/upload_images/1764093-dc01274645da4520.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)
  ####2. 一个例子告诉你该如何写单元测试
  我们现在要利用`List`来写一个模拟栈操作的类,该类提供`Push`、`Pop`、`Top`和`Empty`方法,现在要对这个类进行单元测试。
  首先我们要明确这个类的主要功能:这里的栈用来存储数据,这些数据按照存入时间有序(为方便描述,不妨将最早进入的数据位置称为栈底,最后进来的位置称为栈顶);当需要存入数据时,采用`Push`操作将该数据放在栈顶;当需要从栈中取出数据时,采用`Pop`操作将栈顶的数据取出;当需要查看栈顶元素时,采用`Top`操作即可得到栈顶元素值;当需要知道栈中是否有数据时,采用`Empty`查看它是否为空。
  那我们就针对这些功能来想想我们应用这个栈的场景吧,然后就可以把这些场景写成单元测试。
  (1)往一个空栈中`Push`数据,该操作成功的话栈应该不空,并且栈顶元素就是刚`Push`进去的那个数据。
  (2)连续地往栈中`Push`数据,每次操作后查看栈顶元素都是刚刚放进去的那个数据。
  (3)往栈中`Push`特殊的数据,我们这里存放的是`string`,所以添加`string.Empty`和`null`也应该是成功的。
  (4)连续地`Pop`操作,确认每次取出的都是栈顶元素。
  (5)对空栈进行`Pop`或`Top`操作,会抛出异常。

  // StackExercise Class
  namespace FirstUnitTest
  {
  public class StackExercise
  {
  private List<string> _stack;
      public StackExercise()
      {
          _stack = new List<string>();
      }
      public void Push(string str)
      {
          _stack.Add(str);
      }
      public void Pop()
      {
          if (Empty())
          {
              throw new InvalidOperationException("Empty stack cannot pop");
          }
          _stack.Remove(_stack.Last());
      }
      public string Top()
      {
          if (Empty())
          {
              throw new InvalidOperationException("Empty stack cannot get top");
          }
          return _stack.Last();
      }
      public bool Empty()
      {
          return (!_stack.Any());
      }
  }
  }
  // Unit Tests
  namespace FirstUnitTest
  {
  [TestClass]
  public class TestStackExercise
  {
  [TestMethod]
  public void Test_SuccessAndNotEmpty_AfterPush()
  {
  // Arrange
  var stack = new StackExercise();
  var testElement = "testElement";
          // Action
          stack.Push(testElement);
          // Assert
          Assert.IsFalse(stack.Empty());
          Assert.AreEqual(testElement, stack.Top());
      }
      [TestMethod]
      public void Test_Success_PushMoreThanOnce()
      {
          // Arrange
          var stack = new StackExercise();
          var testElement = "testElement_{0}";
          // Action & Assert
          for (int i = 0; i < 10; ++i)
          {
              stack.Push(string.Format(testElement, i));
              Assert.AreEqual(string.Format(testElement, i), stack.Top());
          }
      }
      [TestMethod]
      public void Test_Success_PushEmptyString()
      {
          // Arrange
          var stack = new StackExercise();
          string emptyString = string.Empty;
          string nullString = null;
          // Action & Assert
          stack.Push(emptyString);
          Assert.AreEqual(emptyString, stack.Top());
          stack.Push(nullString);
          Assert.AreEqual(nullString, stack.Top());
      }
      [TestMethod]
      public void Test_Success_PopLastTwoElements()
      {
          // Arrange
          var stack = new StackExercise();
          var testElement1 = "test1";
          var testElement2 = "test2";
          // Action & Assert
          stack.Push(testElement1);
          stack.Push(testElement2);
          Assert.AreEqual(testElement2, stack.Top());
          stack.Pop();
          Assert.IsFalse(stack.Empty());
          Assert.AreEqual(testElement1, stack.Top());
          stack.Pop();
          Assert.IsTrue(stack.Empty());            
      }
      [TestMethod]
      [ExpectedException(typeof(InvalidOperationException))]
      public void Test_ThrowException_PopFromEmptyStack()
      {
          var stack = new StackExercise();
          stack.Pop();
      }
      [TestMethod]
      [ExpectedException(typeof(InvalidOperationException))]
      public void Test_ThrowException_TopFromEmptyStack()
      {
          var stack = new StackExercise();
          stack.Top();
      }
  }
  }```
  所以,究竟该如何写单元测试呢?《单元测试之道C#版》里总结得很好:Right-BICEP。
  Right,验证结果(主要功能和逻辑)是否正确;
  B,边界条件是否正确;
  I,是否可以检查反向关联;这里所谓反向关联,是指用反向逻辑来验证我们的结果,比如说要验证平方根是否正确时,可以求这个平方根的平方跟我们的输入是否一致。
  C,是否可以采用其他方法来cross-check结果;cross-check是在单元测试中采用与实际模块中不同的方法来实现同样的功能作为期望结果,去与实际模块中得到的结果做对比。
  E,错误条件是否可以重现;
  P,性能方面是否满足条件。



上文内容不用于商业目的,如涉及知识产权问题,请权利人联系博为峰小编(021-64471599-8017),我们将立即处理。
21/212>
《2023软件测试行业现状调查报告》独家发布~

关注51Testing

联系我们

快捷面板 站点地图 联系我们 广告服务 关于我们 站长统计 发展历程

法律顾问:上海兰迪律师事务所 项棋律师
版权所有 上海博为峰软件技术股份有限公司 Copyright©51testing.com 2003-2024
投诉及意见反馈:webmaster@51testing.com; 业务联系:service@51testing.com 021-64471599-8017

沪ICP备05003035号

沪公网安备 31010102002173号