前言
今天與同事在討論.Net下測試框架的時候,說到NUnit等大多數測試框架的SetUp以及TearDown方法並非顯得那麼完美,因此在公司內部的項目中採用了Xunit框架。那麼到底是什麼樣的緣由,讓咱們放棄了大多數框架都在用的Nunit或MSTest框架呢?git
1. Xunit簡介
首先奉上馬丁大叔2006年對XUnit介紹的文章,http://www.martinfowler.com/bliki/Xunit.html。github
Xunit實際上是JUnit的衍生版,最開始是應用在Smalltalk中,其目的是支持持續集成,關於單元測試等相關內容能夠參考我以前TDD系列文章,這裏不作過多的介紹,只是介紹Why we choose Xunit。sql
GitHub地址:https://github.com/xunit/xunit編程
官方文檔:http://xunit.github.io/緩存
2. Xunit簡單Demo
如此簡單:框架
提示:須要經過NuGet下載xunit.net和xunit.visualstudio這兩個安裝包,而後啓動「Test Explorer」運行測試,詳情請參考這裏。less
3. Xunit對比Nunit的優勢
這部份內容參考了官方文章以及一些本身對測試框架的場景的理解,若有錯誤之處,還請指出。post
3.1 每一個測試單一實例的討論,SetUp以及TestFixtureSetUp
請參考馬丁大師對單一實例的論述:http://martinfowler.com/bliki/JunitNewInstance.html,文章指出:對於測試緩存或每次測試以前從新實例化對象,這種作法是值得商榷的。雖然其有利於對象的調用,並且基本不用考慮對象回收的問題(僅當在TearDown中回收了資源),但這樣仍然不符合絕對意義上的「對象隔離」原則。並且有些變量是隻需全局實例化一次(在Nunit框架中要使用TestFeature建立),雖然這樣也能知足需求,可是程序中仍是有不少這種框架的特性須要熟悉,相比沒有這些框架(指沒有SetUp和TestFixtureSetUp)的語法來說跟不方便一些,固然這些僅僅是一些思考。單元測試
同時,James Newkrik也在文章中提到,與其在SetUp中初始化更多的參數,破壞單一職責的原則,另外加上每回測試都要回顧SetUp和TearDown方法所執行的內容,倒不如將其放在Test內部,去掉SetUp和TearDown來加強測試的的表達性以及隔離性。
3.2 Xunit沒有ExpectException
不採用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 (); } }
3.3 Xunit更像面向切面的語言
Xunit中使用Fact、Theory、XxxData、Fact(Timeout=n)等標籤來組織測試,從功能上講更像切面編程。 請參考下一節。
3.4 Xunit去除了更多的Attribute
保留不多一部分標籤有利於簡化測試框架,加快熟悉測試框架的時間,使框架更爲簡潔、實用。
NUnit 2.2 | MSTest | xUnit.net | Comments |
---|---|---|---|
[Test] | [TestMethod] | [Fact] | Marks a test method. |
[TestFixture] | [TestClass] | n/a | xUnit.net does not require an attribute for a test class; it looks for all test methods in all public (exported) classes in the assembly. |
[ExpectedException] | [ExpectedException] | Assert.Throws orRecord.Exception | xUnit.net has done away with the ExpectedException attribute in favor of Assert.Throws. SeeNote 1. |
[SetUp] | [TestInitialize] | Constructor | We believe that use of [SetUp]is generally bad. However, you can implement a parameterless constructor as a direct replacement. See Note 2. |
[TearDown] | [TestCleanup] | IDisposable.Dispose | We believe that use of[TearDown] is generally bad. However, you can implementIDisposable.Dispose as a direct replacement. See Note 2. |
[TestFixtureSetUp] | [ClassInitialize] | IUseFixture<T> | To get per-fixture setup, implement IUseFixture<T> on your test class. See Note 3 |
[TestFixtureTearDown] | [ClassCleanup] | IUseFixture<T> | To get per-fixture teardown, implement IUseFixture<T> on your test class. See Note 3 |
[Ignore] | [Ignore] | [Fact(Skip="reason")] | Set the Skip parameter on the[Fact] attribute to temporarily skip a test. |
n/a | [Timeout] | [Fact(Timeout=n)] | Set the Timeout parameter on the [Fact] attribute to cause a test to fail if it takes too long to run. Note that the timeout value for xUnit.net is in milliseconds. |
[Property] | [TestProperty] | [Trait] | Set arbitrary metadata on a test |
n/a | [DataSource] | [Theory], [XxxData] | Theory (data-driven test). SeeNote 4 |
3.4 Xunit使用IDisposable和IUseFixture<T>接口來代替顯示聲明SetUp和TestFixtureSetUp
首先,建立一個支持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#自己處理,減小工做量。
4. 文章引用
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/