zoukankan      html  css  js  c++  java
  • 使用xUnit为.net core程序进行单元测试(3)

    第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-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.testsindebug
    etcoreapp2.0game.tests.dll" id="9e476ed4-3cd9-4f51-aa39-b3d411369979">
          <Execution id="4c6ec739-ccd3-4233-b2bd-8bbde4dfa67f" />
          <TestMethod codeBase="C:UserssolenprojectsGameGame.TestsinDebug
    etcoreapp2.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>
    View Code

    在里面某个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);
            }
        }
    }
    View Code

    在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);            
            }
        }
    }
    View Code

    看一下上面的代码, 里面有一个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实例:

    这一部分先到这, 还剩下最后一部分了.

    出处:http://www.cnblogs.com/cgzl/p/8438019.html

  • 相关阅读:
    从电视剧《清平乐》聊聊宋仁宗和宋词
    也读《白鹿原》:望关中平原,窥民族秘史
    听说你在做数字化转型,了解中台一下不?
    刘润《商业洞察力30讲》学习总结
    《容器化.NET应用架构指南》脑图学习笔记(一)
    也聊春节:漫天红色与春晚变迁
    我的2019年终回顾:行道迟迟,载饥载渴,而立之年,持续刷新
    ASP.NET Core on K8S深入学习(11)K8S网络知多少
    ASP.NET Core on K8S深入学习(10)K8S包管理器Helm
    【译】gRPC vs HTTP APIs
  • 原文地址:https://www.cnblogs.com/mq0036/p/8512598.html
Copyright © 2011-2022 走看看