第 1 部分: https://cloud.tencent.com/developer/article/1019835
第 2 部分: https://cloud.tencent.com/developer/article/1020850
请使用这个项目作为练习的开始: https://pan.baidu.com/s/1ggcGkGb
测试的分组
- [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 将会这样显示:
- [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 实例:
来源: https://cloud.tencent.com/developer/article/1041722