概述
单元测试是针对最小的可测试软件,测试的内容包括单元的逻辑和数据流。
由于开发方式的不同,单元测试一般划分方法如下:
1. 面向对象的软件开发:以Class(类)作为测试的最小单元。以方法的内部结构作为测试的重点。
2. 结构化的软件开发:以模块(函数、过程)作为测试的最小单元。
工具
这里介绍Nunit工具的单元测试,创建一个.net core2.0的工程,然后添加如下NuGet包
示例
using NUnit.Framework;
using System;
using System.Threading;
using System.Threading.Tasks;
namespace gameServer.Test
{
[TestFixture]
public class CancelTask
{
CancellationTokenSource cancelToken;
public CancelTask()
{
cancelToken = new CancellationTokenSource();
}
[Test]
public void TestCancelTask()
{
Task.Factory.StartNew(MyTask, cancelToken.Token);
int count = 0;
while(count < 3)
{
Assert.AreNotEqual(3, count, string.Format("sleeping count=%d", count));
Thread.Sleep(1000);
count++;
}
cancelToken.Cancel();
Thread.Sleep(3000);
}
private void MyTask()
{
int count = 0;
while (cancelToken.IsCancellationRequested == false)
{
Assert.AreNotEqual(5, count, string.Format("MyTask sleeping count=%d", count));
count++;
Thread.Sleep(1000);
}
Assert.AreNotEqual(5, count, string.Format("MyTask Over, count=%d", count));
}
}
}
这里顺便介绍一哈CancellationTokenSource 的使用,这里为什么用这个类来做例子,可能很多童鞋没接触过,因为我最近在做c#版本gRPC(Google)服务端的时候发现官方的例子不好用,主线程等待子线程直接用的是控制台的等待,代码如下:
static void Main(string[] args)
{
const int Port = 50052;
var features = RouteGuideUtil.ParseFeatures(RouteGuideUtil.DefaultFeaturesFile);
Server server = new Server
{
Services = { RouteGuide.BindService(new RouteGuideImpl(features)) },
Ports = { new ServerPort("localhost", Port, ServerCredentials.Insecure) }
};
server.Start();
Console.WriteLine("RouteGuide server listening on port " + Port);
Console.WriteLine("Press any key to stop the server...");
Console.ReadKey();
server.ShutdownAsync().Wait();
}
而实际开发过程中肯定是不能用Console来阻塞主线程的,需要用到更加优雅的方式,所以就找到了CancellationTokenSource 实现此功能,此类需要和Task对象进行配合使用,Task会对当前运行状态进行控制,怎么控制我们就不关心,而CancellationTokenSource可以是外部对Task的控制,如取消、定时取消。
实现方式就是,主线程实现一个Task任务,任务就是阻塞等待CancellationTokenSource对象的cancel信号,具体实现如下:
class Program
{
const string Host = "0.0.0.0";
const int Port = 50051;
public static void Main(string[] args)
{
// Build a server
var server = new Server
{
Services = { GreetingService.BindService(new GreeterServiceImpl()) },
Ports = { new ServerPort(Host, Port, ServerCredentials.Insecure) }
};
CancellationTokenSource tokenSource = new CancellationTokenSource();
var serverTask = RunServiceAsync(server, tokenSource.Token);
Console.WriteLine("GreeterServer listening on port " + Port);
Console.WriteLine("Press any key to stop the server...");
Console.ReadKey();
tokenSource.Cancel();
Console.WriteLine("Shutting down...");
serverTask.Wait();
}
private static Task AwaitCancellation(CancellationToken token)
{
var taskSource = new TaskCompletionSource<bool>();
token.Register(() => taskSource.SetResult(true));
return taskSource.Task;
}
private static async Task RunServiceAsync(Server server, CancellationToken cancellationToken = default(CancellationToken))
{
// Start server
server.Start();
await AwaitCancellation(cancellationToken);
await server.ShutdownAsync();
}
}
Nunit断言类介绍
Nunit一共有四个断言类,分别是Assert、StringAssert、FileAssert和DirectoryAssert,它们都在NUnit.Framework命名空间,其中Assert是常用,也是我们最熟悉的,而另外三个断言类,顾名思义,分别对应于字符串的断言、文件的断言和目录的断言,理论上,仅Assert类就可以完成所有条件的判断,然而,如果合理的运用后面的三个断言,将使代码更加简洁、美观,也更加便于理解和维护。
一:Assert类
对于我们通常的单元测试代码,Assert类中的静态方法就可以完成,其中大家最熟悉的应该是Assert. AreEqual()方法,其实,除了AreEqual,NUnit还提供了众多的比较方式,满足不同的代码要求。
1:相等/不相等判断
AreEqual:判断两个值相等
AreNotEqual:判断两个值不相等
示例:
Int a=1
Assert.AreEqual(a,1);//通过判断
Assert.AreEqual(a,2);//不能通过判断
注意,除了AreEqual/ AreNotEqual,下面几乎所有的方法都支持多态(多种数据类型),以及多参数,除了经常使用的Assert .AreEqual(int a,int b),还有一种典型的参数形式: Assert.AreEqual(int a,int b,string message);
或者 Assert.XXX(int a,int b,string message);
其中第三个参数是条件不通过时输出的信息,以便于错误的定位
对于double和decimal类型,这两个方法还支持比较时的浮点误差
例如以下代码:
double a = 1.1;
double b = 1.11;
double c = 0.1;
Assert.AreEqual(a, b, c);//通过判断
c=0.01;
Assert.AreEqual(a, b, c);//不能通过判断
2:类的判断
AreSame:判断两个对象相等
AreNotSame:判断两个对象不相等
Contains:判断是否是否某对象
示例:
List list = new List();
list.Add(“a”);
list.Add(“b”);
Assert.Contains(“a”, list); //通过判断
Assert.Contains(“aa”, list); //不能通过判断
3:条件判断
IsTrue/True:判断条件为真
IsFalse/False:判断条件为假
IsNull/Null:判断是否为空
IsNotNull/NotNull:判断不为空
IsNaN:判断数值是Nan
IsEmpty/IsNotEmpty:判断字符串是否为空/不为空
IsEmpty/IsNotEmpty:判断集合是否为空/不为空
其中True,IsTrue类似的都有两个版本,其中IsTrue是用来兼容以前的版本的。
示例:
int a=100;
Assert.IsTrue(a==100);//通过判断
Assert.True(a==100);//通过判断
4:比较判断
Greater:大于
GreaterOrEqual:大于等于
Less:小于
LessOrEqual:小于大于
示例:
int a = 100;
Assert.Greater(a, 99);
Assert.GreaterOrEqual(a, 100);
5:类型判断
IsInstanceOfType/IsNotInstanceOfType:是/不是某个类的实例(从2.5之后支持泛型)
public class Person
{
public string name {set;get;}
}
Person p = new Person();
p.name = “a”;
Assert.IsInstanceOf(typeof(Person), p);//通过判断
Assert.IsInstanceOf(typeof(System.String), p);//不能通过判断
6:异常判断
Throws:应该抛出某类型的异常
DoesNotThrow:不应该抛出某类型的异常
7:其它常用的方法
Pass:强行让测试通过
Fail:强行让测试失败
Ignore:忽略该测试方法
Inconclusive:未验证该测试
Assert.Pass();
Assert.Pass(string message );
Assert.Pass(string message, object[] parms );
Assert.Fail();
Assert.Fail(string message );
Assert.Fail(string message, object[] parms );
Assert.Ignore();
Assert.Ignore(string message );
Assert.Ignore(string message, object[] parms );
Assert.Inconclusive();
Assert.Inconclusive(string message );
Assert.Inconclusive(string message, object[] parms );
这些方法让我们能直接控制测试的进程,其中Assert.Pass 让你直接通过测试来结束这次的测试:这会导致一个异常产生,是高效的让测试返回的方法。除此之外,你也可以提供一个消息,让我们可以直观看出原因。
Assert.Fail 可以让测试失败为结果来结束本次测试。
Assert.Ignore 让我们有能力动态的忽略某个测试或条件。它可以在test,setup,fixture setup方法中调用。建议只在单独的条件内调用,因为Nunit的分类机制提供更广泛的如包含、排除等,你可以更简单的选择让测试运行在不同的场合或者程序集。
Assert.Inconclusive 的作用是指出现有条件下,测试不能完成:因为它不能证明断言是成功或失败。
这几个方法一样,即使把它放到一个独立的方法中,然后再调用这个方法,它仍然会被激发来结束测试。
二:StringAssert类
StringAssert用于String类型的断言判断:
StringAssert.Contains:是否包含子串
StringAssert.StartsWith:是否以某子串开头
StringAssert.EndsWith:是否以某子串结尾
StringAssert.AreEqualIgnoringCase:两个字符串是否在不区分大小写时相等
StringAssert.IsMatch:是否匹配,(使用正则表达式进行字符串比较)
示例:
string s1 = “abc”;
StringAssert.Contains(“b”, s1);
StringAssert.StartsWith(“a”, s1);
StringAssert.EndsWith(“c”, s1);
string s2 = “aBc”;
StringAssert.AreEqualIgnoringCase(s1, s2);
StringAssert.IsMatch(“[a|book]”, “123”);
三:CollectionAssert类
AllItemsAreInstancesOfType:集合中的各项是否是某某类型的实例
AllItemsAreNotNull:集合中的各项均不为空
AllItemsAreUnique:集合中的各项唯一
AreEqual:两个集合相等
AreEquivalent:两个集合相当
AreNotEqual:两个集合不相等
AreNotEquivalent:两个集合不相当
DoesNotContain:集合中不包含某对象
IsSubsetOf:一个集合是另外一个集合的子集
IsNotSubsetOf:一个集合不是另外一个集合的子集
IsEmpty:集合为空
IsNotEmpty:集合不为空
IsOrdered:集合的各项已经排序
示例:
List a = new List();
List b = new List();
CollectionAssert.IsEmpty(a);
for (int i = 1; i <= 10; i++)
{
a.Add(i);
b.Add(i);
}
CollectionAssert.AreEqual(a, b);
CollectionAssert.IsOrdered(a);
b.Remove(1);
CollectionAssert.IsSubsetOf(b, a);
CollectionAssert.AreEqual(a, b);
CollectionAssert类提供的这些方法主要用于检查集合或者集合内容或者来比较两个集合。
AreEqual 的重载方法要求两个集合的对应元素要相等,而 AreEquivalent 要求两个集合的内容相等,而不要求顺序。两者元素的比较都应用默认的相等比较。
四:FileAssert类
FileAssert用于文件的比较判断:
AreEqual:判断两个值相等
AreNotEqual:判断两个值不相等
文件的比较,用来比较两个文件,可以Streams,FileInfos 或者路径的形式。
FileAssert.AreEqual( Stream expected, Stream actual );
FileAssert.AreNotEqual( Stream expected, Stream actual );
五:DirectoryAssert类
这个类提供的方法让我们可以对系统的文件目录进行断言,可以以DirectoryInfos 或者路径 的形式。
AreEqual:判断两个值相等
AreNotEqual:判断两个值不相等
用来比较两个目录是否相等:相同的全名,属性Attributes, 创建时间CreationTime 和最后访问时间LastAccessTime. 两个包含相等文件的不同目录是不会被认为相同的。
DirectoryAssert.AreEqual( DirectoryInfo expected, DirectoryInfo actual );
DirectoryAssert.AreNotEqual( DirectoryInfo expected, DirectoryInfo actual );
IsEmpty:集合为空
IsNotEmpty:集合不为空
这些被用来测试目录是否为空。
DirectoryAssert.IsEmpty(DirectoryInfo directory);
DirectoryAssert.IsNotEmpty(DirectoryInfo directory);
IsWithin:是子目录
IsNotWithin:不是子目录
DirectoryAssert.IsWithin(DirectoryInfo expected,DirectoryInfo actual);
DirectoryAssert.IsNotWithin(DirectoryInfo expected,DirectoryInfo actual);
这些方法用来测试第二个目录是不是第一个目录的子(孙子等)目录。