第1部分: http://www.cnblogs.com/cgzl/p/8283610.html
第2部分: http://www.cnblogs.com/cgzl/p/8287588.html
請使用這個項目作為練習的開始: https://pan.baidu.com/s/1ggcGkGb
測試的分組
打開Game.Tests里面的BossEnemyShould.cs, 為HaveCorrectPower方法添加一個Trait屬性標簽:
[Fact] [Trait("Category", "Enemy")] public void HaveCorrectPower() { BossEnemy sut = new BossEnemy(); Assert.Equal(166.667, sut.SpecialAttackPower, 3); }
Trait接受兩個參數, 作為測試分類的Name和Value對.
Build項目, Run All Tests, 然后選擇選擇一下按Traits分組:

這時, Test Explorer里面的tests將會這樣顯示:

再打開EnemyFactoryShould.cs, 為CreateNormalEnemyByDefault方法添加Trait屬性標簽:
[Fact] [Trait("Category", "Enemy")] public void CreateNormalEnemyByDefault() { EnemyFactory sut = new EnemyFactory(); Enemy enemy = sut.Create("Zombie"); Assert.IsType<NormalEnemy>(enemy); }
Build, 然后查看Test Explorer:

不同的Category:
修改一下BossEnemyShould.cs里面的HaveCorrectPower方法的Trait屬性:
[Fact] [Trait("Category", "Boss")] public void HaveCorrectPower() { BossEnemy sut = new BossEnemy(); Assert.Equal(166.667, sut.SpecialAttackPower, 3); }
Build之后, 將會看見兩個分類:

在Class級別進行分類:
只需要把Trait屬性標簽移到Class上面即可:
[Trait("Category", "Enemy")] public class EnemyFactoryShould {
Build, 查看Test Explorer可以發現EnemyFactoryShould下面所有的Test方法都分類到了Enemy下:

按分類運行測試:
鼠標右鍵點擊分類, Run Selected Tests就會運行該分類下所有的測試:

按Trait搜索:
在Test Explorer中把分類選擇到Class:

然后在旁邊的Search輸入框中輸入關鍵字, 這時下方會有提示菜單:

點擊Trait, 然后如下圖輸入, 就會把Enemy分類的測試過濾顯示出來:

這種方式同樣也可以進行Trait過濾.
使用命令行進行分類測試
使用命令行進入的Game.Tests, 首先執行命令dotnet test, 這里顯示一共有27個tests:

然后, 可以使用命令:
dotnet test --filter Category=Enemy
運行分類為Enemy的tests, 結果如圖, 有8個tests:

運行多個分類的tests:
dotnet test --filter "Category=Boss|Category=Enemy"
這句命令會運行分類為Boss或者Enemy的tests, 結果如圖:

共有9個tests.
忽略Test
為Fact屬性標簽設置其Skip屬性, 即可忽略該測試, Skip的值為忽略的原因:
[Fact(Skip = "不需要跑這個測試")] public void CreateNormalEnemyByDefault_NotTypeExample() { EnemyFactory sut = new EnemyFactory(); Enemy enemy = sut.Create("Zombie"); Assert.IsNotType<DateTime>(enemy); }
Build, 查看Test Explorer, 選擇按Trait分類顯示, 然后選中Category[Enemy]運行選中的tests:

從這里可以看到, 上面Skip的test被忽略了.
回到命令行, 執行dotnet test:

也可以看到該測試被忽略了, 並且標明了忽略的原因.
打印自定義測試輸出信息:
在test中打印信息需要用到ITestOutputHelper的實現類(注意: 這里使用Console.Writeline是無效的), 在BossEnemyShould.cs里面注入這個helper:
using Xunit; using Xunit.Abstractions; namespace Game.Tests { public class BossEnemyShould { private readonly ITestOutputHelper _output; public BossEnemyShould(ITestOutputHelper output) { _output = output; }
......
然后在test方法里面這樣寫即可:
[Fact] [Trait("Category", "Boss")] public void HaveCorrectPower() { _output.WriteLine("正在創建 Boss Enemy"); BossEnemy sut = new BossEnemy(); Assert.Equal(166.667, sut.SpecialAttackPower, 3); }
Build, Run Tests, 這時查看測試結果會發現一個output鏈接:

點擊這個鏈接, 就會顯示測試的輸出信息:

使用命令行:
dotnet test --filter Category=Boss --logger:trx
執行命令后:

可以看到生成了一個TestResults文件夾, 里面是測試的輸出文件, 使用編輯器打開, 它是一個xml文件, 內容如下:
<?xml version="1.0" encoding="UTF-8"?> <TestRun id="9e552b73-0636-46a2-83d9-c19a5892b3ab" name="solen@DELL-RED 2018-02-10 10:27:19" runUser="DELL-RED\solen" xmlns="http://microsoft.com/schemas/VisualStudio/TeamTest/2010"> <Times creation="2018-02-10T10:27:19.5005784+08:00" queuing="2018-02-10T10:27:19.5005896+08:00" start="2018-02-10T10:27:17.4990291+08:00" finish="2018-02-10T10:27:19.5176327+08:00" /> <TestSettings name="default" id="610cad4c-1066-417b-a8e6-d30dce78ef4d"> <Deployment runDeploymentRoot="solen_DELL-RED_2018-02-10_10_27_19" /> </TestSettings> <Results> <UnitTestResult executionId="4c6ec739-ccd3-4233-b2bd-8bbde4dfa67f" testId="9e476ed4-3cd9-4f51-aa39-b3d411369979" testName="Game.Tests.BossEnemyShould.HaveCorrectPower" computerName="DELL-RED" duration="00:00:00.0160000" startTime="2018-02-10T10:27:19.2099922+08:00" endTime="2018-02-10T10:27:19.2113656+08:00" testType="13cdc9d9-ddb5-4fa4-a97d-d965ccfc6d4b" outcome="Passed" testListId="8c84fa94-04c1-424b-9868-57a2d4851a1d" relativeResultsDirectory="4c6ec739-ccd3-4233-b2bd-8bbde4dfa67f"> <Output> <StdOut>正在創建 Boss Enemy</StdOut> </Output> </UnitTestResult> </Results> <TestDefinitions> <UnitTest name="Game.Tests.BossEnemyShould.HaveCorrectPower" storage="c:\users\solen\projects\game\game.tests\bin\debug\netcoreapp2.0\game.tests.dll" id="9e476ed4-3cd9-4f51-aa39-b3d411369979"> <Execution id="4c6ec739-ccd3-4233-b2bd-8bbde4dfa67f" /> <TestMethod codeBase="C:\Users\solen\projects\Game\Game.Tests\bin\Debug\netcoreapp2.0\Game.Tests.dll" executorUriOfAdapter="executor://xunit/VsTestRunner2/netcoreapp" className="Game.Tests.BossEnemyShould" name="Game.Tests.BossEnemyShould.HaveCorrectPower" /> </UnitTest> </TestDefinitions> <TestEntries> <TestEntry testId="9e476ed4-3cd9-4f51-aa39-b3d411369979" executionId="4c6ec739-ccd3-4233-b2bd-8bbde4dfa67f" testListId="8c84fa94-04c1-424b-9868-57a2d4851a1d" /> </TestEntries> <TestLists> <TestList name="Results Not in a List" id="8c84fa94-04c1-424b-9868-57a2d4851a1d" /> <TestList name="All Loaded Results" id="19431567-8539-422a-85d7-44ee4e166bda" /> </TestLists> <ResultSummary outcome="Completed"> <Counters total="1" executed="1" passed="1" failed="0" error="0" timeout="0" aborted="0" inconclusive="0" passedButRunAborted="0" notRunnable="0" notExecuted="0" disconnected="0" warning="0" completed="0" inProgress="0" pending="0" /> <Output> <StdOut>[xUnit.net 00:00:00.5525795] Discovering: Game.Tests[xUnit.net 00:00:00.6567207] Discovered: Game.Tests[xUnit.net 00:00:00.6755272] Starting: Game.Tests[xUnit.net 00:00:00.8743059] Finished: Game.Tests</StdOut> </Output> </ResultSummary> </TestRun>
在里面某個Output標簽內可以看到上面寫的測試輸出信息.
減少重復的代碼
xUnit在執行某個測試類的Fact或Theory方法的時候, 都會創建這個類新的實例, 所以有一些公用初始化的代碼可以移動到constructor里面.
打開PlayerCharacterShould.cs, 可以看到每個test方法都執行了new PlayerCharacter()這個動作. 我們應該把這段代碼移動到constructor里面:
namespace Game.Tests { public class PlayerCharacterShould { private readonly PlayerCharacter _playerCharacter; private readonly ITestOutputHelper _output; public PlayerCharacterShould(ITestOutputHelper output) {
_output = output;
_output.WriteLine("正在創建新的玩家角色"); _playerCharacter = new PlayerCharacter(); } [Fact] public void BeInexperiencedWhenNew() { Assert.True(_playerCharacter.IsNoob); } [Fact] public void CalculateFullName() { _playerCharacter.FirstName = "Sarah"; _playerCharacter.LastName = "Smith"; Assert.Equal("Sarah Smith", _playerCharacter.FullName);
......
Build, Run Tests, 都OK, 並且都有output輸出信息.
除了集中編寫初始化代碼, 也可以集中編寫清理代碼:
這需要該測試類實現IDisposable接口:
public class PlayerCharacterShould: IDisposable { ...... public void Dispose() { _output.WriteLine($"正在清理玩家{_playerCharacter.FullName}"); }
}
Build, Run Tests, 然后隨便查看一個該類的test的output:

可以看到Dispose()被調用了.
共享上下文
在執行測試的時候共享上下文
上面降到了每個測試方法運行的時候都會創建該測試類新的實例, 可以在constructor里面進行公共的初始化動作.
但是如果初始化的動作消耗資源比較大, 並且時間較長, 那么這種方法就不太好了, 所以下面介紹另外一種方法.
首先在Game項目里面添加類:GameState.cs:
using System; using System.Collections.Generic; namespace Game { public class GameState { public static readonly int EarthquakeDamage = 25; public List<PlayerCharacter> Players { get; set; } = new List<PlayerCharacter>(); public Guid Id { get; } = Guid.NewGuid(); public GameState() { CreateGameWorld(); } public void Earthquake() { foreach (var player in Players) { player.TakeDamage(EarthquakeDamage); } } public void Reset() { Players.Clear(); } private void CreateGameWorld() { // Simulate expensive creation System.Threading.Thread.Sleep(2000); } } }
在Game.Tests里面添加類: GameStateShould.cs:
using Xunit; namespace Game.Tests { public class GameStateShould { [Fact] public void DamageAllPlayersWhenEarthquake() { var sut = new GameState(); var player1 = new PlayerCharacter(); var player2 = new PlayerCharacter(); sut.Players.Add(player1); sut.Players.Add(player2); var expectedHealthAfterEarthquake = player1.Health - GameState.EarthquakeDamage; sut.Earthquake(); Assert.Equal(expectedHealthAfterEarthquake, player1.Health); Assert.Equal(expectedHealthAfterEarthquake, player2.Health); } [Fact] public void Reset() { var sut = new GameState(); var player1 = new PlayerCharacter(); var player2 = new PlayerCharacter(); sut.Players.Add(player1); sut.Players.Add(player2); sut.Reset(); Assert.Empty(sut.Players); } } }
看一下上面的代碼, 里面有一個Sleep 2秒的動作, 所以執行兩個測試方法的話每個方法都會執行這個動作, 一共用了這些時間:

為了解決這個問題, 我們首先建立一個類 GameStateFixture.cs, 它需要實現IDisposable接口:
using System; namespace Game.Tests { public class GameStateFixture : IDisposable { public GameState State { get; private set; } public GameStateFixture() { State = new GameState(); } public void Dispose() { // Cleanup } } }
然后在GameStateShould類實現IClassFixture接口並帶有泛型的類型:
using Xunit; using Xunit.Abstractions; namespace Game.Tests { public class GameStateShould : IClassFixture<GameStateFixture> { private readonly GameStateFixture _gameStateFixture; private readonly ITestOutputHelper _output; public GameStateShould(GameStateFixture gameStateFixture, ITestOutputHelper output) { _gameStateFixture = gameStateFixture; _output = output; } [Fact] public void DamageAllPlayersWhenEarthquake() { _output.WriteLine($"GameState Id={_gameStateFixture.State.Id}"); var player1 = new PlayerCharacter(); var player2 = new PlayerCharacter(); _gameStateFixture.State.Players.Add(player1); _gameStateFixture.State.Players.Add(player2); var expectedHealthAfterEarthquake = player1.Health - GameState.EarthquakeDamage; _gameStateFixture.State.Earthquake(); Assert.Equal(expectedHealthAfterEarthquake, player1.Health); Assert.Equal(expectedHealthAfterEarthquake, player2.Health); } [Fact] public void Reset() { _output.WriteLine($"GameState Id={_gameStateFixture.State.Id}"); var player1 = new PlayerCharacter(); var player2 = new PlayerCharacter(); _gameStateFixture.State.Players.Add(player1); _gameStateFixture.State.Players.Add(player2); _gameStateFixture.State.Reset(); Assert.Empty(_gameStateFixture.State.Players); } } }
這個注入的_gameStateFixture在運行多個tests的時候只有一個實例. 所以把消耗資源嚴重的動作放在GameStateFixture里面就可以保證該段代碼只運行一次, 並且被所有的test所共享調用. 要注意的是, 因為上述原因, GameStateFixture里面的代碼不可以有任何副作用, 也就是說可以影響其他的測試結果.
Build, Run Tests:

可以看到運行時間少了很多, 因為那段Sleep代碼只需要運行一次.
再查看一下這個兩個tests的output是一樣的, 也就是說明確實是只生成了一個GameState實例:

在不同的測試類中共享上下文
上面講述了如何在一個測試類中不同的測試里共享代碼的方法, 而xUnit也可以讓我們在不同的測試類中共享上下文.
在Tests項目里建立 GameStateCollection.cs:
using Xunit; namespace Game.Tests { [CollectionDefinition("GameState collection")] public class GameStateCollection : ICollectionFixture<GameStateFixture> {} }
這個類GameStateCollection需要實現ICollectionFixture<T>接口, 但是它沒有具體的實現.
它上面的CollectionDefinition屬性標簽作用是定義了一個Collection名字叫做GameStateCollection.
再建立TestClass1.cs:
using Xunit; using Xunit.Abstractions; namespace Game.Tests { [Collection("GameState collection")] public class TestClass1 { private readonly GameStateFixture _gameStateFixture; private readonly ITestOutputHelper _output; public TestClass1(GameStateFixture gameStateFixture, ITestOutputHelper output) { _gameStateFixture = gameStateFixture; _output = output; } [Fact] public void Test1() { _output.WriteLine($"GameState ID={_gameStateFixture.State.Id}"); } [Fact] public void Test2() { _output.WriteLine($"GameState ID={_gameStateFixture.State.Id}"); } } }
和TestClass2.cs:
using Xunit; using Xunit.Abstractions; namespace Game.Tests { [Collection("GameState collection")] public class TestClass2 { private readonly GameStateFixture _gameStateFixture; private readonly ITestOutputHelper _output; public TestClass2(GameStateFixture gameStateFixture, ITestOutputHelper output) { _gameStateFixture = gameStateFixture; _output = output; } [Fact] public void Test3() { _output.WriteLine($"GameState ID={_gameStateFixture.State.Id}"); } [Fact] public void Test4() { _output.WriteLine($"GameState ID={_gameStateFixture.State.Id}"); } } }
TestClass1和TestClass2在類的上面使用Collection屬性標簽來調用名為GameState collection的Collection. 而不需要實現任何接口.
這樣, xUnit在運行測試之前會建立一個GameState實例共享與TestClass1和TestClass2.
Build, 同時運行TestClass1和TestClass2的Tests:

運行的時間為3秒多:

查看這4個test的output, 可以看到它們使用的是同一個GameState實例:

這一部分先到這, 還剩下最后一部分了.
