端午节有空本来是为整理一下C#调用C/C++库的方法,为了测试用例顺便实现了一直想实现的一个小的测试框架跟同事们分享一下,NUnit搞得已经比较复杂了,这个非常简单理解起来容易,用起来比较方便,谁再想用其它功能再自己加吧,如果功能要求比较多就直接用NUnit好了,不要再自己造轮子了。
此篇献给伟大的屈原,是他给我们创造了这样一个假期!
代码比较简单,注释也写了不少,就不再多说了,直接上代码,时间仓促,不保证没有BUG:
/** * A mini test framework: TTest * [email protected] * version: 0.1 * updated: 2012-06-24 * * Usage: * 1) Write a static test case method to do test, which must need no arguments; * 2) Tag the test method with TestCaseAttribute attribute; * 3) Call Test.Run( ) functions at anytime; * * If a log file is needed, we have at least two methods to get that: * 1) Run the test in a console and redirect the output to a file, like this: * .\myapp.exe > test.log * 2) Set the TTest.Out, like this * using (FileStream fs = new FileStream(string.Format("Test_{0}.log", DateTime.Now.ToString("yyyy-MM-dd_HH-mm-ss")),FileMode.CreateNew)) { using (StreamWriter sw = new StreamWriter(fs)) { Test.Out = sw; Test.Run(); Test.Out = Console.Out; } } * 3) You must know, create it! */ using System; using System.Diagnostics; using System.IO; using System.Reflection; namespace Noock.TTest { /// <summary> /// An attribute which indicate that the method is a test case, it will be run by the test engine. /// </summary> /// <remarks> /// A test case method should: /// 1) requires no arguments; /// 2) static; /// 3) private (recommend) /// 4) returns void (recommend); /// </remarks> public class TestCaseAttribute : System.Attribute { public string Target { get; set; } } /// <summary> /// Indicates a failed test case /// </summary> public class TestFailedException : Exception { public TestFailedException(string msg) : base(msg) { } } /// <summary> /// Test engine /// </summary> /// <remarks> /// <para>Call Equals functions for condition check for every test case method</para> /// <para>call a Run( ) function to start test</para> /// </remarks> public static class TTest { #region Condition checkers /// <summary> /// Compare two int values /// </summary> /// <param name="expect">expected value</param> /// <param name="actual">actual value</param> /// <param name="msg">a message to display if failed</param> public static void AreEqual(int expect, int actual, string msg = null) { if (expect != actual) throw new TestFailedException(msg ?? string.Format("Values not equals: expect {0}, got {1}", expect, actual)); } /// <summary> /// Compare two double values /// </summary> /// <param name="expect">expected value</param> /// <param name="actual">actual value</param> /// <param name="msg">a message to display if failed</param> public static void AreEqual(double expect, double actual, string msg = null) { if (Math.Abs(expect - actual) > double.Epsilon) throw new TestFailedException(msg ?? string.Format("Values not equals: expect {0}, got {1}", expect, actual)); } /// <summary> /// Compare two strings /// </summary> /// <param name="expect">expected value</param> /// <param name="actual">actual value</param> /// <param name="msg">a message to display if failed</param> /// <param name="ignoreCase">true: case insensitive, false: case sensitive</param> public static void AreEqual(string expect, string actual, bool ignoreCase = false, string msg = null) { if( !expect.Equals(actual, ignoreCase ? StringComparison.InvariantCultureIgnoreCase : StringComparison.InvariantCulture) ) throw new TestFailedException(msg ?? string.Format("Values not equals: expect '{0}', got '{1}'", expect, actual)); } /// <summary> /// Compare two bool values /// </summary> /// <param name="expect">expected value</param> /// <param name="actual">actual value</param> /// <param name="msg">a message to display if failed</param> public static void AreEqual(bool expect, bool actual, string msg = null) { if (expect != actual) throw new TestFailedException(msg ?? string.Format("Values not equals: expect {0}, got {1}", expect, actual)); } /// <summary> /// Check if two arrays equal, which meet one of: /// 1) the same object /// 2) every inner one dimension array equals and values equal /// </summary> /// <param name="expect"></param> /// <param name="actual"></param> /// <param name="msg">if null an auto-generated message is used</param> public static void AreEqual(byte[][] expect, byte[][] actual, string msg = null) { if( object.ReferenceEquals(expect, actual) ) return; int mExpect = expect.Length; int mActual = actual.Length; if (mExpect != mActual) throw new TestFailedException(msg ?? string.Format("First dimension no equals: expect {0}, got {1})" , mExpect,mActual)); for (int i = 0; i < mExpect; ++i) { if(expect[i] == null && actual[i] == null) continue; else if(object.ReferenceEquals(expect[i],actual[i])) continue; int nExpect = expect[i].Length; int nActual = actual[i].Length; if(nExpect != nActual) throw new TestFailedException(msg ?? string.Format("Array dimension no equals: expect[{0}] length = {1}, actual[{0}] length = {2}" , i, nExpect, nActual)); for (int j = 0; j < nExpect; ++j) { if( expect[i][j] != actual[i][j]) throw new TestFailedException(msg ?? string.Format("Array values no equals: expect ({0},{1}) == {2}, got {3}" , i, j, expect[i][j], actual[i][j])); } } } /// <summary> /// Compare two 2-dimension arraies /// </summary> /// <param name="expect"></param> /// <param name="actual"></param> /// <param name="msg"></param> public static void AreEqual(byte[,] expect, byte[,] actual, string msg = null) { if (object.ReferenceEquals(expect, actual)) return; int mExpect = expect.GetUpperBound(0)+1; int mActual = actual.GetUpperBound(0)+1; int nExpect = expect.GetUpperBound(1)+1; int nActual = actual.GetUpperBound(1)+1; if (mExpect != mActual || nExpect != nActual) throw new TestFailedException(msg ?? string.Format("Array dimensions no equals: expect ({0},{1}), got ({2},{3})" , mExpect, nExpect, mActual, nActual)); for (int i = 0; i < mExpect; ++i) { for (int j = 0; j < nExpect; ++j) { if (expect[i,j] != actual[i,j]) throw new TestFailedException(msg ?? string.Format("Array values no equals: expect ({0},{1}) == {2}, got {3}" , i, j, expect[i,j], actual[i,j])); } } } #endregion #region Test engine control /// <summary> /// Run tests in an assembly /// </summary> /// <param name="assembly">assemble to be test</param> public static void Run(Assembly assembly) { lock (_settingLock) { // set test options if (Out == Console.Out) { Console.ForegroundColor = ConsoleColor.White; } Type[] types = assembly.GetTypes(); int nTotalOK = 0, nTotalFailed = 0; int nType = 0; foreach (Type t in types) { MethodInfo[] methods = t.GetMethods(BindingFlags.Static | BindingFlags.NonPublic | BindingFlags.Public); bool needTest = false; foreach (var m in methods) { TestCaseAttribute att = m.GetCustomAttribute(typeof(TestCaseAttribute)) as TestCaseAttribute; if (att != null) { needTest = true; break; } } if (!needTest) continue; Out.WriteLine("\r\n\r\nTesting {0} ...", t.FullName); int nOk = 0, nFailed = 0; foreach (var m in methods) { TestCaseAttribute att = m.GetCustomAttribute(typeof(TestCaseAttribute)) as TestCaseAttribute; if (att == null) continue; Out.Write("-- [{0}] : {1}\r\n", m.Name, att.Target); try { m.Invoke(null, null); if (Out == Console.Out) { Console.ForegroundColor = ConsoleColor.Green; } Out.WriteLine("[OK]"); if (Out == Console.Out) { Console.ForegroundColor = ConsoleColor.White; } ++nOk; } catch (TestFailedException ex) { if (Out == Console.Out) { Console.ForegroundColor = ConsoleColor.Red; } Out.Write("[FAILED]"); if (Out == Console.Out) { Console.ForegroundColor = ConsoleColor.White; } Out.WriteLine(ex.Message); ++nFailed; } catch (Exception ex) { if (Out == Console.Out) { Console.ForegroundColor = ConsoleColor.Red; } Exception baseExp = ex.GetBaseException(); if (baseExp is TestFailedException) Out.Write("[FAILED]"); else Out.Write("[ERROR]"); if (Out == Console.Out) { Console.ForegroundColor = ConsoleColor.White; } if (baseExp is TestFailedException) Out.WriteLine(baseExp.Message); else Out.WriteLine(ex.Message); ++nFailed; } } Out.WriteLine("------------------------------------------------"); Out.WriteLine("Test finished: {0} OK, {1} Failed", nOk, nFailed); nTotalOK += nOk; nTotalFailed += nFailed; ++nType; } Out.WriteLine("================================================"); Out.WriteLine("Test finished: {0} OK, {1} Failed in {2} types", nTotalOK, nTotalFailed, nType); } } /// <summary> /// Run all test in current AppDomain /// </summary> public static void Run() { Assembly[] assemblies = AppDomain.CurrentDomain.GetAssemblies(); foreach (var a in assemblies) { Run(a); } } #endregion #region Test settings static TTest() { Out = Console.Out; } private static TextWriter _out = null; private static object _settingLock = new object(); /// <summary> /// Out stream writer, default to <see cref="Console.Out"/>/> /// </summary> /// <remarks> /// Current thread will be blocked if test has been started. /// </remarks> public static TextWriter Out { get { return _out; } set { lock (_settingLock) { if (value == null) throw new ArgumentNullException(); _out = value; } } } #endregion } }