使用xUnit為.net core程式進行單元測試(3)
請使用這個專案作為練習的開始: 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-REDsolen" 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:userssolenprojectsgamegame.testsbindebugnetcoreapp2.0game.tests.dll" id="9e476ed4-3cd9-4f51-aa39-b3d411369979">
<Execution id="4c6ec739-ccd3-4233-b2bd-8bbde4dfa67f" />
<TestMethod codeBase="C:UserssolenprojectsGameGame.TestsbinDebugnetcoreapp2.0Game.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例項:
這一部分先到這, 還剩下最後一部分了.