本文共 11275 字,大约阅读时间需要 37 分钟。
第1部分:
第2部分:
请使用这个项目作为练习的开始:
打开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(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之后, 将会看见两个分类:
只需要把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.
为Fact属性标签设置其Skip属性, 即可忽略该测试, Skip的值为忽略的原因:
[Fact(Skip = "不需要跑这个测试")] public void CreateNormalEnemyByDefault_NotTypeExample() { EnemyFactory sut = new EnemyFactory(); Enemy enemy = sut.Create("Zombie"); Assert.IsNotType(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文件, 内容如下:
在里面某个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 ListPlayers { get; set; } = new List (); 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{ 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{}}
这个类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实例:
这一部分先到这, 还剩下最后一部分了.
下面是我的关于ASP.NET Core Web API相关技术的公众号--草根专栏:
转载地址:http://dgsxx.baihongyu.com/