使用 xunit 编写测试代码

使用 xunit 编写测试代码

Intro

xunit 是 .NET 里使用非常广泛的一个测试框架,有很多测试项目都是在使用 xunit 作为测试框架,不仅仅有很多开源项目在使用,很多微软的项目也在使用 xunit 来作为测试框架。

Get Started

在 xunit 中不需要标记测试类,所有 public 的类似都可以作为测试类,测试方法需要使用 Fact 或者 Theory 注解来标注方法,来看一个基本的使用示例:

首先准备了几个要测试的方法:

internal class Helper
{public static int Add(int x, int y){return x + y;}public static void ArgumentExceptionTest() => throw new ArgumentException();public static void ArgumentNullExceptionTest() => throw new ArgumentNullException();
}

测试代码:

public class BasicTest
{[Fact]public void AddTest(){Assert.Equal(4, Helper.Add(2, 2));Assert.NotEqual(3, Helper.Add(2, 2));}[Theory][InlineData(1, 2)][InlineData(2, 2)]public void AddTestWithTestData(int num1, int num2){Assert.Equal(num1 + num2, Helper.Add(num1, num2));}
}

使用 Fact 标记的测试方法不能有方法参数,只有标记 Theory 的方法可以有方法参数

使用 Assert 来断言结果是否符合预期,xunit 提供了很丰富的 Assert 方法,可以使得我们的测试代码更加简洁。

Exception Assert

除了一般的结果断言,xunit 也支持 exception 断言,主要支持两大类,Assert.Throw/Assert.Throw<TExceptionType>/Assert.ThrowAny<TExceptionType>,对应的也有 Async 版本

[Fact]
public void ExceptionTest()
{var exceptionType = typeof(ArgumentException);Assert.Throws(exceptionType, Helper.ArgumentExceptionTest);Assert.Throws<ArgumentException>(testCode: Helper.ArgumentExceptionTest);
}[Fact]
public void ExceptionAnyTest()
{Assert.Throws<ArgumentNullException>(Helper.ArgumentNullExceptionTest);Assert.ThrowsAny<ArgumentNullException>(Helper.ArgumentNullExceptionTest);Assert.ThrowsAny<ArgumentException>(Helper.ArgumentNullExceptionTest);
}

Assert.Throw(exceptionType, action)Assert.Throw<TExceptionType>(action) 这样的 exception 类型只能是这个类型,继承于这个类型的不算,会 fail,而 Assert.ThrowAny<TExceptionType>(action) 则更包容一点,是这个类型或者是继承于这个类型的都可以。

Comparisons

很多人已经在使用其他的测试框架,如何迁移呢,xunit 也给出了与 nunit 和 mstest 的对比,详细可以参考下面的对比,具体可以参考 https://xunit.net/docs/comparisons:

NUnit 3.xMSTest 15.xxUnit.net 2.xComments
[Test][TestMethod][Fact]Marks a test method.
[TestFixture][TestClass]n/axUnit.net does not require an attribute for a test class; it looks for all test methods in all public (exported) classes in the assembly.
Assert.That Record.Exception[ExpectedException]Assert.Throws Record.ExceptionxUnit.net has done away with the ExpectedException attribute in favor of Assert.Throws. See Note 1
[SetUp][TestInitialize]ConstructorWe believe that use of [SetUp] is generally bad. However, you can implement a parameterless constructor as a direct replacement. See Note 2
[TearDown][TestCleanup]IDisposable.DisposeWe believe that use of [TearDown] is generally bad. However, you can implement IDisposable.Dispose as a direct replacement. See Note 2
[OneTimeSetUp][ClassInitialize]IClassFixture<T>To get per-class fixture setup, implement IClassFixture<T> on your test class. See Note 3
[OneTimeTearDown][ClassCleanup]IClassFixture<T>To get per-class fixture teardown, implement IClassFixture<T> on your test class. See Note 3
n/an/aICollectionFixture<T>To get per-collection fixture setup and teardown, implement ICollectionFixture<T> on your test collection. See Note 3
[Ignore("reason")][Ignore][Fact(Skip="reason")]Set the Skip parameter on the [Fact] attribute to temporarily skip a test.
[Property][TestProperty][Trait]Set arbitrary metadata on a test
[Theory][DataSource][Theory] [XxxData]Theory (data-driven test). See Note 4

Data Driven Test

测试框架大多提供数据驱动测试的支持,简单的就如开篇中的 Theory 示例,我们再来看一些稍微复杂一些的示例,一起来看下:

要使用数据驱动的方式写测试方法,测试方法应该标记为 Theory,并且将测试数据作为测试方法的方法参数

InlineData

最基本数据驱动的方式当属 InlineData,添加多个 InlineData 即可使用不同的测试数据进行测试

[Theory]
[InlineData(1)]
[InlineData(2)]
[InlineData(3)]
public void InlineDataTest(int num)
{Assert.True(num > 0);
}

InlineData 有其限制,只能使用一些常量,想要更灵活的方式需要使用别的方式,测试结果:

MemberData

MemberData 可以一定程度上解决 InlineData 存在的问题,MemberData 支持字段、属性或方法,且需要满足下面两个条件:

  • 需要是 public

  • 需要是 static

  • 可以隐式转换为 IEnumerable<object[]> 或者方法返回值可以隐式转换为 IEnumerable<object[]>

来看下面的示例:


[Theory]
[MemberData(nameof(TestMemberData))]
public void MemberDataPropertyTest(int num)
{Assert.True(num > 0);
}public static IEnumerable<object[]> TestMemberData =>Enumerable.Range(1, 10).Select(x => new object[] { x }).ToArray();[Theory]
[MemberData(nameof(TestMemberDataField))]
public void MemberDataFieldTest(int num)
{Assert.True(num > 0);
}public static readonly IList<object[]> TestMemberDataField = Enumerable.Range(1, 10).Select(x => new object[] { x }).ToArray();[Theory]
[MemberData(nameof(TestMemberDataMethod), 10)]
public void MemberDataMethodTest(int num)
{Assert.True(num > 0);
}public static IEnumerable<object[]> TestMemberDataMethod(int count)
{return Enumerable.Range(1, count).Select(i => new object[] { i });
}

测试结果:

Custom Data Source

MemberData 相比之下提供了更大的便利和可自定义程度,只能在当前测试类中使用,想要跨测试类还是不行,xunit 还提供了 DataAttribute ,使得我们可以通过自定义方式实现测试方法数据源,甚至也可以从数据库里动态查询出数据,写了一个简单的示例,可以参考下面的示例:

自定义数据源:

public class NullOrEmptyStringDataAttribute : DataAttribute
{public override IEnumerable<object[]> GetData(MethodInfo testMethod){yield return new object[] { null };yield return new object[] { string.Empty };}
}

测试方法:

[Theory]
[NullOrEmptyStringData]
public void CustomDataAttributeTest(string value)
{Assert.True(string.IsNullOrEmpty(value));
}

测试结果:

Output

在测试方法中如果想要输出一些测试信息,直接是用 Console.Write/Console.WriteLine 是没有效果的,在测试方法中输出需要使用 ITestoutputHelper 来输出,来看下面的示例:

public class OutputTest
{private readonly ITestOutputHelper _outputHelper;public OutputTest(ITestOutputHelper outputHelper){_outputHelper = outputHelper;}[Fact]public void ConsoleWriteTest(){Console.WriteLine("Console");}[Fact]public void OutputHelperTest(){_outputHelper.WriteLine("Output");}
}

测试方法中使用 Console.Write/Console.WriteLine 的时候会有一个提示:

测试输出结果:

Console.WriteLine
TestOutputHelper.WriteLine

Test Filter

xunit 提供了 BeforeAfterTestAttribute 来让我们实现一些自定义的逻辑来在测试运行前和运行后执行,和 mvc 里的 action filter 很像,所以这里我把他称为 test filter,来看下面的一个示例,改编自 xunit 的示例:

/// <summary>
/// Apply this attribute to your test method to replace the
/// <see cref="Thread.CurrentThread" /> <see cref="CultureInfo.CurrentCulture" /> and
/// <see cref="CultureInfo.CurrentUICulture" /> with another culture.
/// </summary>
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method)]
public class UseCultureAttribute : BeforeAfterTestAttribute
{private readonly Lazy<CultureInfo> _culture;private readonly Lazy<CultureInfo> _uiCulture;private CultureInfo _originalCulture;private CultureInfo _originalUiCulture;/// <summary>/// Replaces the culture and UI culture of the current thread with/// <paramref name="culture" />/// </summary>/// <param name="culture">The name of the culture.</param>/// <remarks>/// <para>/// This constructor overload uses <paramref name="culture" /> for both/// <see cref="Culture" /> and <see cref="UICulture" />./// </para>/// </remarks>public UseCultureAttribute(string culture): this(culture, culture) { }/// <summary>/// Replaces the culture and UI culture of the current thread with/// <paramref name="culture" /> and <paramref name="uiCulture" />/// </summary>/// <param name="culture">The name of the culture.</param>/// <param name="uiCulture">The name of the UI culture.</param>public UseCultureAttribute(string culture, string uiCulture){_culture = new Lazy<CultureInfo>(() => new CultureInfo(culture, false));_uiCulture = new Lazy<CultureInfo>(() => new CultureInfo(uiCulture, false));}/// <summary>/// Gets the culture./// </summary>public CultureInfo Culture { get { return _culture.Value; } }/// <summary>/// Gets the UI culture./// </summary>public CultureInfo UICulture { get { return _uiCulture.Value; } }/// <summary>/// Stores the current <see cref="Thread.CurrentPrincipal" />/// <see cref="CultureInfo.CurrentCulture" /> and <see cref="CultureInfo.CurrentUICulture" />/// and replaces them with the new cultures defined in the constructor./// </summary>/// <param name="methodUnderTest">The method under test</param>public override void Before(MethodInfo methodUnderTest){_originalCulture = Thread.CurrentThread.CurrentCulture;_originalUiCulture = Thread.CurrentThread.CurrentUICulture;Thread.CurrentThread.CurrentCulture = Culture;Thread.CurrentThread.CurrentUICulture = UICulture;CultureInfo.CurrentCulture.ClearCachedData();CultureInfo.CurrentUICulture.ClearCachedData();}/// <summary>/// Restores the original <see cref="CultureInfo.CurrentCulture" /> and/// <see cref="CultureInfo.CurrentUICulture" /> to <see cref="Thread.CurrentPrincipal" />/// </summary>/// <param name="methodUnderTest">The method under test</param>public override void After(MethodInfo methodUnderTest){Thread.CurrentThread.CurrentCulture = _originalCulture;Thread.CurrentThread.CurrentUICulture = _originalUiCulture;CultureInfo.CurrentCulture.ClearCachedData();CultureInfo.CurrentUICulture.ClearCachedData();}
}

这里实现了一个设置测试用例运行过程中 Thread.CurrentThread.Culture 的属性,测试结束后恢复原始的属性值,可以用作于 Class 也可以用在测试方法中,使用示例如下:

[UseCulture("en-US", "zh-CN")]
public class FilterTest
{[Fact][UseCulture("en-US")]public void CultureTest(){Assert.Equal("en-US", Thread.CurrentThread.CurrentCulture.Name);}[Fact][UseCulture("zh-CN")]public void CultureTest2(){Assert.Equal("zh-CN", Thread.CurrentThread.CurrentCulture.Name);}[Fact]public void CultureTest3(){Assert.Equal("en-US", Thread.CurrentThread.CurrentCulture.Name);Assert.Equal("zh-CN", Thread.CurrentThread.CurrentUICulture.Name);}
}

测试结果如下:

Shared Context

单元测试类通常共享初始化和清理代码(通常称为“测试上下文”)。xunit 提供了几种共享初始化和清理代码的方法,具体取决于要共享的对象的范围。

  • 构造器和 Dispose 方法 (共享初始化和 Dispose,不需要共享对象)

  • Class Fixtures (同一个测试类中共享对象)

  • Collection Fixtures (同一个 Collection 中(可以是多个测试类)中共享对象实例)

通常我们可以使用 Fixture 来实现依赖注入,但是我更推荐使用 Xunit.DependencyInjection 这个项目来实现依赖注入,具体使用可以参考之前的文章 在 xunit 测试项目中使用依赖注入 中的介绍

More

希望对你使用 xunit 有所帮助

文章中的示例代码可以从 https://github.com/WeihanLi/SamplesInPractice/tree/master/XunitSample 获取

xunit 还有很多可以扩展的地方,更多可以参考 xunit 的示例 https://github.com/xunit/samples.xunit

References

  • https://github.com/WeihanLi/SamplesInPractice/tree/master/XunitSample

  • https://github.com/xunit/samples.xunit

  • https://xunit.net/#documentation

  • https://xunit.net/docs/comparisons

  • https://xunit.net/docs/shared-context

  • 在 xunit 测试项目中使用依赖注入

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/304399.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

白话AI:看懂深度学习真的那么难吗?初中数学,就用10分钟

如果在这个人工智能的时代&#xff0c;作为一个有理想抱负的程序员&#xff0c;或者学生、爱好者&#xff0c;不懂深度学习这个超热的话题&#xff0c;似乎已经跟时代脱节了。但是&#xff0c;深度学习对数学的要求&#xff0c;包括微积分、线性代数和概率论与数理统计等&#…

IdentityServer4密码模式

Oatuth2协议的密码模式介绍用户会将用户名&#xff0c;密码给予客户端&#xff0c;但是客户端不保存此信息&#xff0c;客户端带着用户的密码请求认证服务器&#xff0c;认证服务器密码验证通过后后将token返回给客户端。 这里借用下阮一峰老师画的图&#xff08;博客地址》htt…

IKVM 编程武林之.NET派的北冥神功

为什么80%的码农都做不了架构师&#xff1f;>>> 在编程武林中&#xff0c;Java派成立较久底子雄厚&#xff0c;虽然掌门人Sun已经老态龙钟&#xff0c;镇山之技的Java语言已经被后进的新秀.NET派的C#压得喘不过气来&#xff0c;甚至有时候Sun老大还得跑到.NET派潜伏…

php 自定义菜单 openid,微信公众平台开发(99) 自定义菜单获取OpenID

关键字 微信公众平台 自定义菜单 OpenID作者&#xff1a;方倍工作室原文&#xff1a;http://www.cnblogs.com/txw1958/p/weixin-menu-get-openid.html在这篇微信公众平台开发教程中&#xff0c;我们将介绍如何在自定义菜单中获得用户的OpenID。本篇开发教程的实质是微信自定义菜…

mysql优化的重要参数 key_buffer_size table_cache

MySQL服务器端的参数有很多&#xff0c;但是对于大多数初学者来说&#xff0c;众多的参数往往使得我们不知所措&#xff0c;但是哪些参数是需要我们调整的&#xff0c;哪些对服务器的性能影响最大呢&#xff1f;对于使用Myisam存储引擎来说&#xff0c;主要有key_buffer_size和…

代码传奇 | 明明可以靠颜值 却用代码把人类送上了月球的女人——Margaret Hamilton

据说「软件工程师」这个名词就是她发明的玛格丽特站在阿波罗计算机指导手册 (AGC) 的源代码程序列表旁边&#xff0c;这些材料摞起来比她的人还要高。图片来源&#xff1a;Margaret Hamilton缔造传奇的人似乎有个共性&#xff1a;本来没想干一票大的&#xff0c;甚至她的打算都…

每扇区2048字节的U盘乱码的数据恢复

每扇区2048字节的U盘乱码的数据恢复一个U盘&#xff0c;FAT32分区&#xff0c;显示的是乱码&#xff0c;远程看对方的U盘参数&#xff0c;发现一个比较怪的现象&#xff1a;每扇区字节数是2048字节&#xff08;U盘量产时可能是以光盘形式形成的&#xff09;&#xff0c;对方传的…

为什么有些大公司的效率弱爆了?

阅读本文大概需要5分钟。上周写了篇文章&#xff1a;为什么有些大公司的技术弱爆了&#xff1f;不少朋友读完后表示有同感&#xff0c;还有一些朋友在读者群探讨大公司效率问题。有几个朋友谈到自己的公司效率低下&#xff0c;做一件事需要层层审批&#xff0c;并且遇到各种阻力…

2018年最有前景的十大行业

我们想和大家分享的是围绕十个行业、数十个细分领域&#xff0c;在2018年发展趋势展望&#xff1a;01 消费新零售——平台级近半年&#xff0c;新零售已被多次提及。但在年终盘点我们再次提到这个“热词”&#xff0c;是因为该领域未来还将出现超级平台级的公司。新零售未来更多…

昨日搬至办公室的书籍

由于家里地方小&#xff0c;总有一部分书籍需要跟着我辗转至各公司。其中一些公司只能让我放抽屉&#xff08;地方小&#xff09;甚至地板上&#xff0c;而有一些公司就能让我把书都摞桌子上。昨天下午我特地来了一次办公室&#xff0c;只为搬运书籍。灭霍霍&#xff0c;可真是…

如何在 ASP.Net Core 中对接 WCF

在 REST API 出现之前&#xff0c;SOAP (Simple Object Access Protocol) 一直都是基于 web 的标准协议&#xff0c;虽然现在 REST 大行其道&#xff0c;但在平时开发中总会遇到对接第三方服务采用的是基于SOAP协议的场景&#xff0c;在这篇文章中&#xff0c;我们将会讨论如何…

什么是区块链? 区块链的入门教程~

区块链&#xff08;blockchain&#xff09;是眼下的大热门&#xff0c;新闻媒体大量报道&#xff0c;宣称它将创造未来。可是&#xff0c;简单易懂的入门文章却很少。区块链到底是什么&#xff0c;有何特别之处&#xff0c;很少有解释。下面&#xff0c;我就来尝试&#xff0c;…

linux提升权限命令提示符,win10如何直接使用命令提示符提高管理员权限?

原标题&#xff1a;win10如何直接使用命令提示符提高管理员权限?在使用普通的命令提示符时&#xff0c;如果遇到需要管理员权限的操作&#xff0c;往往需要重新打开一个具有管理员权限的命令提示符页面进行操作。而在Linux操作系统中&#xff0c;可以通过输入su来获取系统最高…

探索 .NET Core 依赖注入的 IServiceProvider

在上一篇文章中&#xff0c;我们学习了Microsoft.Extensions.DependencyInjection中的IServiceCollection&#xff0c;包括服务注册转换为ServiceDescriptors&#xff0c;然后添加到集合中。探索 .NET Core 依赖注入的 IServiceCollection[1]在本文中&#xff0c;我们会学习 IS…

如何快速高效地学习互联网新技术

我们生活在一个振奋人心的时代&#xff0c;区块链、人工智能、大数据&#xff0c;现在各种新技术层出不穷&#xff0c;那么如何快速高效地学习互联网新技术呢&#xff1f;我认为首先思想要主动求变&#xff0c;敢于跳出自己的舒适区&#xff0c;对任何技术都抱有开放的心态。贪…

.NET Core用数据库做配置中心加载Configuration

本文介绍了一个在.NET中用数据库做配置中心服务器的方式&#xff0c;介绍了读取配置的开源自定义ConfigurationProvider&#xff0c;并且讲解了主要实现原理。1、 为什么用数据库做配置中心在开发youzack.com这个学英语网站的时候&#xff0c;需要保存第三方接口AppKey、JWT等配…

Abp Vnext Pro 的 Vue 实现版本

Abp Vnext Pro 的 Vue 实现版本开箱即用的中后台前端/设计解决方案知识点.Net Core5.0Abp Vnext 4.x ,Ant Design, Vue2.xMysql,Redis,Hangfire,ES(日志可选),Nocas(可选,未集成,计划中),RabbitMq(未集成,计划中)微服务架构设计, DDD 实践容器化 CI CD系统功能用户管理角色管理…

2018年,该转行AI工程师吗?

如此火爆的AI&#xff0c;会不会像Android和iOS一样&#xff0c;五年后归于平淡&#xff1f;转型AI真的有必要吗&#xff1f;2017年&#xff0c;AI就像一个点石成金的神器&#xff0c;所有的行业&#xff0c;任何的创业&#xff0c;抑或是职位背景&#xff0c;只要沾着这个词&a…

使用 Tye 辅助开发 dotnet 应用程序

newbe.pro 已经给我们写了系列文章介绍Tye 辅助开发k8s 应用&#xff1a;使用 Tye 辅助开发 k8s 应用竟如此简单&#xff08;一&#xff09;使用 Tye 辅助开发 k8s 应用竟如此简单&#xff08;二&#xff09;使用 Tye 辅助开发 k8s 应用竟如此简单&#xff08;三&#xff09;使…

你感兴趣的大学专业真相 | 16万人参与调查,看完80%都哭了

俗话说&#xff0c;隔行如隔山学科专业也是如此你以为我的专业十分高大上事实却是我也不知道我在学啥下面就由各个专业的同学为你揭秘他们专业的真相~人文社会类法学▼泛泛SaMa&#xff1a;在我们法学界&#xff0c;唯有秃头这件事&#xff0c;不分男女不墮紅塵&#xff1a;第一…