今天與同事在討論.Net下測試框架的時候,說到NUnit等大多數測試框架的SetUp以及TearDown方法並不是顯得那麼完美,所以在公司內部的項目中采用了Xunit框架。那麼究竟是什麼樣的原因,讓我們放棄了大多數框架都在用的Nunit或MSTest框架呢?
首先奉上馬丁大叔2006年對XUnit介紹的文章,http://www.martinfowler.com/bliki/Xunit.html。
Xunit其實是JUnit的衍生版,最開始是應用在Smalltalk中,其目的是支持持續集成,關於單元測試等相關內容可以參考我之前TDD系列文章,這裡不做過多的介紹,只是介紹Why we choose Xunit。
GitHub地址:https://github.com/xunit/xunit
官方文檔:http://xunit.github.io/
如此簡單:
提示:需要通過NuGet下載xunit.net和xunit.visualstudio這兩個安裝包,然後啟動“Test Explorer”運行測試,詳情請參考這裡。
這部分內容參考了官方文章以及一些自己對測試框架的場景的理解,如有錯誤之處,還請指出。
請參考馬丁大師對單一實例的論述:http://martinfowler.com/bliki/JunitNewInstance.html,文章指出:對於測試緩存或每次測試之前重新實例化對象,這種做法是值得商榷的。雖然其有利於對象的調用,而且基本不用考慮對象回收的問題(僅當在TearDown中回收了資源),但這樣仍然不符合絕對意義上的“對象隔離”原則。而且有些變量是只需全局實例化一次(在Nunit框架中要使用TestFeature創建),雖然這樣也能滿足需求,但是程序中還是有很多這種框架的特性需要熟悉,相比沒有這些框架(指沒有SetUp和TestFixtureSetUp)的語法來講跟不方便一些,當然這些僅僅是一些思考。
同時,James Newkrik也在文章中提到,與其在SetUp中初始化更多的參數,破壞單一職責的原則,另外加上每回測試都要回顧SetUp和TearDown方法所執行的內容,倒不如將其放在Test內部,去掉SetUp和TearDown來增強測試的的表達性以及隔離性。
不采用Attribute的方式來捕捉異常有兩方面的好處:
1. 在代碼中直接斷言(Assert)能捕捉到更多種類的異常。
2. 遵守Arrange-Act-Assert (or "3A") 模式:即測試命名上“范圍-作用-斷言”規范。
public class TestClass1 { [ Fact ] public void testException() { Assert .Throws< InvalidOperationException >(() => operation()); } void operation() { throw new InvalidOperationException (); } }
Xunit中使用Fact、Theory、XxxData、Fact(Timeout=n)等標簽來組織測試,從功能上講更像切面編程。 請參考下一節。
保留很少一部分標簽有利於簡化測試框架,加快熟悉測試框架的時間,使框架更為簡潔、實用。
首先,創建一個支持IDisposable對象:
using System; using System.Configuration; using System.Data.SqlClient; public class DatabaseFixture : IDisposable { SqlConnection connection; int fooUserID; public DatabaseFixture() { string connectionString = ConfigurationManager.ConnectionStrings["DatabaseFixture"].ConnectionString; connection = new SqlConnection(connectionString); connection.Open(); string sql = @"INSERT INTO Users VALUES ('foo', 'bar'); SELECT SCOPE_IDENTITY();"; using (SqlCommand cmd = new SqlCommand(sql, connection)) fooUserID = Convert.ToInt32(cmd.ExecuteScalar()); } public SqlConnection Connection { get { return connection; } } public int FooUserID { get { return fooUserID; } } public void Dispose() { string sql = @"DELETE FROM Users WHERE ID = @id;"; using (SqlCommand cmd = new SqlCommand(sql, connection)) { cmd.Parameters.AddWithValue("@id", fooUserID); cmd.ExecuteNonQuery(); } connection.Close(); } }
最後增加測試,並實現IClassFixture<DatabaseFixture>接口:
using System; using System.Configuration; using System.Data.SqlClient; using Xunit; public class ClassFixtureTests : IClassFixture<DatabaseFixture> { DatabaseFixture database; public ClassFixtureTests(DatabaseFixture data) { database = data; } [Fact] public void ConnectionIsEstablished() { Assert.NotNull(database.Connection); } [Fact] public void FooUserWasInserted() { string sql = "SELECT COUNT(*) FROM Users WHERE ID = @id;"; using (SqlCommand cmd = new SqlCommand(sql, database.Connection)) { cmd.Parameters.AddWithValue("@id", database.FooUserID); int rowCount = Convert.ToInt32(cmd.ExecuteScalar()); Assert.Equal(1, rowCount); } } }
從這裡讀者可能體會到,Xunit更多的利用了C#本身的一些特性,而非使用一些特殊的Attribute或者方法(例如SetUp),在設計哲學上更多的考慮了對象自動實現自我管理的機制,而非人為去管理,從某種意義上來講,解除了部分依賴性,將部分功能交給程序C#本身處理,減少工作量。
Martin Flower介紹Xunit: http://www.martinfowler.com/bliki/Xunit.html
Xunit Github地址:https://github.com/xunit/xunit
Nunit 官方地址:http://www.nunit.org/
周公介紹Xunit:http://zhoufoxcn.blog.51cto.com/792419/1172320/