-----------------------
绝对原创! 版权所有, 转发需经过作者同意.
-----------------------
在谈到特性的使用场景时, 还有一个绝对离不开的就是
单元测试
按飞哥的定义, 单元测试是开发人员自己用代码实现的测试. 注意这个定义, 其核心在于:
主体是 "开发人员", 不是测试人员.
途径是 "通过代码实现", 不是通过手工测试.
实质是一种 "测试", 不是代码调试.
暂时还有点抽象, 同学们记着这个概念, 我们先用一个
NUnit 项目
来看一看单元测试长个什么样.
在 solution 上右键添加项目, 选择 Test 中的 NUnit Test Project, 输入项目名称, 点击 OK:
Visual Studio 直接集成了 NUnit 说明微软在开源和社区支持的路上确实是一路狂奔, 因为 NUnit 是一个由社区支持的, 完全开源的, 和微软自己的 MSTest Test 和 Unit Test 直接竞争的单元测试框架. 微软确实已经从 "什么都要自己有" 向 "借用 (不仅是借鉴) 乃至大力支持一切优质开源项目" 华丽转身.
新建的单元测试项目包含一个默认的类文件: UnitTest1.cs, 其中首先使用了 using:
using NUnit.Framework;
因为 NUnit 的所有成员 (类和方法等) 都在 NUnit.Framework 命名空间之下.
然后有一个类:
- public class Tests
- {
- [SetUp]
- public void Setup()
- {
- }
- [Test]
- public void Test1()
- {
- Assert.Pass();
- }
- }
你发现这个项目和 Console Project 不同, 它没有没有 Main()函数作为入口, 怎么运行呢? 就算我知道它可以由 NUnit 调用, 但 NUnit 怎么调用呢? 这就需要用到反射了: NUnit 会在整个程序集 (项目) 中遍历, 找到带有特定标签 (特性) 的类和方法, 予以相应的处理.
注意这个类里面的两个方法都被贴上了特性:
SetUp: 被标记的方法将会在每一个测试方法被调用前调用
Test: 被标记的方法会被依次调用
NUnit 是依据特性而不是方法名来确定如何调用这些方法的, 所以 Tests 的类名和其中的方法名都可以修改.
那么如何启动测试呢? 快捷键 Ctrl+E+T, 或者在 VS 的菜单栏上, 依次: Test-Windows-Test Explore 打开测试窗口即可:
然后在 Test1 上点击右键, 就可以 Run(运行)或者 Debug(调试)这个测试方法了.
演示:
测试方法中现在可以使用
Assert(断言)
调用各种方法, 最常用的是 Assert.AreEqual(), 比较传入的两个参数:
- [Test]
- public void Test1()
- {
- Assert.AreEqual(5, 3 + 2);
- }
- [Test]
- public void Test2()
- {
- Assert.AreEqual(8, 3 + 2);
- }
前面一个参数代表你期望获得的值, 后面一个参数代表实际获得的值. 如果两个值相等, 测试通过; 否则会抛出 AssertException 异常.
一个方法里可以有多条 Assert 语句, 只有方法里所有 Assert 语句全部通过, 方法才算通过测试. 方法通过, 用绿色√表示; 否则, 用红色 * 标识.
点击未通过的方法, 可以看到其详细信息:
尤其是 StackTrace, 是我们定位未通过 Assert 的有力工具.
当然上面的演示是没有实际作用的, 3+2=5 这是在测试 C# 的运算能力呢,^_^. 我们要测试的, 是我们自己写的代码 (通常是方法). 比如, Student 类(学生) 有一个实例方法 Grow(), 每调用一次该方法, 这个学生的年龄就增长一岁.
所以我们应该怎么做? 先实现这个方法吧...... 注意, 注意, 注意! 标准 (推荐) 的做法不是这样的, 而应该是: 先测试, 再开发.
啥? 一脸懵逼,(黑人问号. jpg
这就不得不提到大名鼎鼎的:
TDD
其全称是 Test-DrivenDevelopment(测试驱动开发), 其核心是: 在开发功能代码之前, 先编写单元测试用例代码. 具体来说, 它要求的开发流程是这样的:
写一个未实现的开发代码. 比如定义一个方法, 但没有方法实现
为其编写单元测试. 确定方法应该实现的功能
测试, 无法通过.^_^, 因为没有方法实现嘛. 但这一步必不可少, 以免单元测试的代码有误, 无论是否正确实现方法功能测试都可以通过
实现开发代码. 比如在方法中完成方法体.
再次测试. 如果通过, Over; 否则, 查找原因, 修复, 直到通过.
以上述 Student.Grow()的需求为例:
首先, 在 Student 中定义该方法但不要有真正的实现, 所以可以是这样的:
- public class Student
- {
- public int Age { get; set; }
- public void Grow()
- {
- // 没有方法实现
- }
- }
然后, 为该方法编写一个单元测试:
- [Test]
- public void Grow()
- {
- // 测试准备: 得到一个学生对象, 其年龄为 18 岁
- Student student = new Student();
- student.Age = 18;
- // 调用 Grow()方法
- student.Grow();
- // 检查是否实现了预期的结果
- // 该学生的年龄变成了 19(=18+1)
- Assert.AreEqual(19, student.Age);
- }
注意我们是在一个新项目中测试另外一个项目, 一个项目使用另外一个项目的代码, 必须要添加引用.
演示: 接下来, 不要忘了要跑一遍这个测试, 当然这个测试是无法通过的.
再然后, 才去完成方法 Grow():
- public void Grow()
- {
- Age++;
- }
再跑一遍测试, 通过! 收工,^_^
为什么要这么做呢? 为了避免你的开发代码影响了你的测试思路.
同学们注意调试和测试的区别: 调试是为了实现功能修复 bug, 而测试是为了找到 bug! 换言之, 测试就是要 get 到你开发没有 get 到的点上去. 如果你先写了开发代码, 脑子里已经有了实现的细节, 那就很容易出现: 写的测试代码, 无非就是把开发代码再 "翻译" 一遍, 这样的测试几乎没有意义.
你说, 我其实也没看出来你上面这个单元测试有啥意义,^_^
Wonderful! 这说明你是带着脑子在听课的.
为了表现出单元测试的意义, 我们来完成这样一个功能:
双向链表
- public class DoubleLinked
- {
- public DoubleLinked Previous { get; set; }
- public DoubleLinked Next { get; set; }
- public int Value { get; set; }
- }
- public bool IsHead
- {
- get
- {
- return Previous == null;
- }
- }
- public bool IsTail
- {
- get
- {
- return Next == null;
- }
- }
- /// <summary>
- /// 在 node 之后插入当前节点
- /// </summary>
- /// <param name="node">在哪一个节点之后插入</param>
- public void InsertAfter(DoubleLinked node)
- {
- }
- [Test] // 不要忘记 [Test] 特性
- public void InsertAfterTest() // 测试方法也不需要任何返回值
- {
- }
- // 在单元测试中, 命名可以带 123 等后缀区分
- DoubleLinked node1 = new DoubleLinked();
- DoubleLinked node2 = new DoubleLinked();
- DoubleLinked node3 = new DoubleLinked();
- DoubleLinked node4 = new DoubleLinked();
- node1.Next = node2;
- node2.Next = node3;
- node3.Next = node4;
- node4.Previous = node3;
- node3.Previous = node2;
- node2.Previous = node1;
- DoubleLinked inserted = new DoubleLinked();
- inserted.InsertAfter(node2);
- Assert.AreEqual(inserted, node2.Next);
- Assert.AreEqual(inserted, node3.Previous);
- Assert.AreEqual(node2, inserted.Previous);
- Assert.AreEqual(node3, inserted.Next);
- // 在单元测试中, 命名可以带 123 等后缀区分
- DoubleLinked node1, node2, node3, node4;
- [SetUp]
- public void Setup()
- {
- node1 = new DoubleLinked();
- node2 = new DoubleLinked();
- node3 = new DoubleLinked();
- node4 = new DoubleLinked();
- node1.Next = node2;
- node2.Next = node3;
- node3.Next = node4;
- node4.Previous = node3;
- node3.Previous = node2;
- node2.Previous = node1;
- }
- [Test]
- public void InsertAfterTailTest()
- {
- DoubleLinked inserted = new DoubleLinked();
- inserted.InsertAfter(node4);
- Assert.AreEqual(inserted, node4.Next);
- Assert.AreEqual(node4, inserted.Previous);
- Assert.AreEqual(null, inserted.Next);
- }
- public void InsertAfter(DoubleLinked node)
- {
- if (node.Next == null)
- {
- node.Next = this;
- this.Previous = node;
- }
- else
- {
- this.Next = node.Next;
- this.Previous = node;
- node.Next = this;
- this.Next.Previous = this;
- }
- }
- node.Next = this;
- this.Previous = node;
- public void InsertAfter(DoubleLinked node)
- {
- node.Next = this;
- this.Previous = node;
- if (node.Next != null)
- {
- this.Next = node.Next;
- this.Next.Previous = this;
- }
- }
来源: https://www.cnblogs.com/freeflying/p/11983193.html