【5min+】为你的.NET应用进行一次全方位体检

231 阅读14分钟

系列介绍

【五分钟的dotnet】是一个利用您的碎片化时间来学习和丰富.net知识的博文系列。它所包含了.net体系中可能会涉及到的方方面面,比如C#的小细节,AspnetCore,微服务中的.net知识等等。

通过本篇文章您将Get:

  • .NET Core应用添加单元测试
  • .NET Core应用进行代码覆盖率度量
  • 使用Azure Devops进行自动化构建
  • 收获类似于下面的这些徽章:
    x

时长为大约有十分钟,内容丰富,建议先投币再上车观看😜

正文

发现网上很少有讲解关于.NET Core单元测试文章,代码覆盖率的文章就更少了。所以就抽时间梳理了一篇😁。

单元测试

先来说一下单元测试,对于已经开始使用单元测试的小伙伴可以直接跳过这个小节。

那么咱们为什么需要进行单元测试呢?肯定是为了减少错误和bug的发生呀,这个不用说大家都知道。网上也有很多介绍单元测试的文章,但是大多都是从一个很简单的方法入手,比如下面这个方法:

public int SimpleMehtod(int a1, int a2)
{
    return a1 + a2;
}

然后告诉大家我们需要对这个方法进行验证。 其实这种教程由潜入深,好是好,但是很多没有涉及过单元测试的小伙伴就会感到很懵逼:“这个代码这么简单,我为啥要单元测试?一眼就看出返回两个值的和”,这样反而不能更好的体现单元测试所带来的直观好处。

所以,咱们反过来,思考有下面的一个方法:

/// <summary>
/// 获取某个类型的泛型参数
/// </summary>
/// <param name="type">需要检测的类型</param>
/// <param name="genericType">检测类型所继承的泛型接口</param>
/// <returns>泛型接口的所有参数信息</returns>
public Type[] MyDemoMethod(Type type, Type genericType)
{
    return type.GetInterfaces()
                .Where(i => IsGenericType(i))
                .SelectMany(i => i.GetGenericArguments())
                .ToArray();

    bool IsGenericType(Type type1)
        => type1.IsGenericType && type1.GetGenericTypeDefinition() == genericType;
}

相对而言该方法就显得复杂一些,它的功能是返回一个类型所继承的泛型接口的所有参数。

假设我们在一次功能迭代中,编写了这样一个MyDemoMethod的方法,该方法很明显是作为一个工具方法来被其它调用者使用。那么,当我们刚刚编写完这个方法的时候,我们就很想知道这个方法是不是能够正确的执行怎么办呢?“编写一个控制台程序来测试?”、“等最后功能全部写完了再来看”、“不管了”。

在咱们没有使用单元测试的时候,上面的几个操作是常见的情况,可能很多小伙伴会基于控制台来测试;还有一些小伙伴直接F5运行应用来进行测试,这样直接运行程序会花费我们大量的琐碎时间(比如登录,操作功能,进入模块,测试该功能…………);最后是那些“等最后功能全部写完了再来看”的朋友,如果记性还不错的话,最后还可以记得来要对这个功能测试,要是后期编写了很多其它业务功能的话,可能早都忘记有这个方法了,所以最后就是完全“裸奔”。

所以,此时就需要咱们引入“单元测试”了。当一个方法被多个地方使用,过早的对该方法进行单元测试,将会大幅度的减少bug的产生。

.NET Core中使用单元测试也很简单,直接新建一个测试项目就可以了。本次文章选择的是基于Xunit所建立的测试项目,然后在测试项目中引用需要测试的项目:

x

编写测试用例

接下来您需要对您需要测试的类编写对应的测试用例。假如我们编写了如下的方法(别问我为什么不是上面的那个泛型基础方法,因为待会要测代码覆盖率,为了简单):

public int CalDemo(int s, bool checkSign = true)
{
    if (s > 10 && checkSign)
    {
        return s << 2;
    }
    else
    {
        return s;
    }
}

在实际项目中,我们可能有许许多多像这样的方法。具有几个分支,每个分支执行不同的代码。针对该CalDemo方法,很明显当传入参数s大于10和小于10的时候有着不同的执行逻辑(先忽略checkSign参数),所以我们可以分别测试当s大于10或者s小于等于10的情况:

xunit测试项目中编写以下用例:

[Fact]
public void CalDemo_ArguementMoreThan10()
{
    var testValue = 11;

    DemoClass demoClass = new DemoClass();
    var result =  demoClass.CalDemo(testValue);

    //判断结果是否等于 44。 如果是则测试通过
    Assert.Equal(44, result);
}

[Fact]
public void CalDemo_ArguementLessThan10()
{
    var testValue = 9;

    DemoClass demoClass = new DemoClass();
    var result = demoClass.CalDemo(testValue);

    //判断结果是否等于 9。 如果是则测试通过
    Assert.Equal(9, result);
}

这样我们就完成了对该方法的测试,当然您还可以编写:“传入参数等于10”,“传入参数为空”,“传入参数为负数”等等用例。

VS中打开"测试资源管理器"来运行测试看看吧:

x

有关xunit的使用,您可以参考:Getting Started with xUnit.net

代码覆盖率

通过“测试资源管理器”,我们可以看到单元测试的正确与否。但是,我如何知道该单元的代码是否都测试完成了呢?如果没有完成我还需要编写哪些测试用例呢?

这个时候,我们就需要对测试进行度量,度量哪些代码已经被我们测试过,哪些代码没有被测试到。针对没有测试到的部分,我们再编写一些Case进行测试。

所以我们可以引入代码覆盖率的概念来进行评估。关于该概念的内容我这里就不在过多阐述了,大家有兴趣可以“百度谷歌必应”三条龙服务。

VS中,为我们提供了代码覆盖率的菜单项:在“测试” 菜单中,选择“分析所有测试的代码覆盖率” 。

x

通过该功能我们就可以对已有的单元测试进行代码覆盖率度量。

x

是不是很简单? 但是你会发现:“你根本找不到这个按钮!!!!!!”。

别找了,您的Visual Studio 2019没得这个菜单? 为什么呢? 因为您没有充钱啊!!!,该功能只针对Visual Studio Enterprise(企业版)提供。使用社区版的我,眼泪流下来。

你以为这样就能难倒我了吗? 直接上开源的度量工具:coverlet。来看看关于Coverlet的介绍:“Coverlet是一个跨平台的.NET代码覆盖框架,支持行、分支和方法覆盖。它与Windows上的.NET Framework和所有受支持的平台上的.NET Core一起工作。”

这里我强烈推荐大家使用Coverlet来进行代码覆盖率测试,为什么呢?因为它跨平台呀。后面我们会使用Linux环境来进行自动化构建,所以Coverlet具有明显的优势,在Azure的官方文档中也推荐大家使用Coverlet:

x

使用Coverlet

使用Coverlet也很简单,直接在您的测试项目安装对应的Nuget包依赖就可以了:

dotnet add package coverlet.collector

因为跨平台的特性,所以您可能已经想到了,咱们接下来就没有像“测试资源管理器”那样的界面可以一键点击了,所以我们得使用命令行的方式来进行操作,对于一些小伙伴可能需要习惯习惯。

xunit项目中执行以下命令:

dotnet test --collect:"XPlat Code Coverage"

我个人比较喜欢用powershell来执行,当然您可以在vs中用程序包管理控制台来选中项目执行:

x

执行后您会发现在项目中多了一个叫做TestResults的文件夹,该文件夹就是本次代码度量的结果:

x

度量报告

但是您马上又会发现一个问题,这个报告它喵的是xml格式,看起来十分费解。

所以,我们又引入了另外一个神器:ReportGenerator。关于该工具的描述可以参考:ReportGenerator。 它的作用就像它的名字一样,就是为了生成代码覆盖的报告。

ReportGenerator提供了几种使用方式,一种是您通过Nuget包来使用它,还有就是把他作为一个全局的命令行指令工具来安装它。 这里我们选择了第二种,为了以后使用,所以选全局的来的爽。

powershell中执行下面命令:

dotnet tool install --global dotnet-reportgenerator-globaltool

然后就可以使用它来生成报告了,还是用powershellxunit测试项目中执行下面的代码:

reportgenerator "-reports:**\coverage.cobertura.xml" "-targetdir:coveragereport"

这句话的意思是:根据将xunit项目下的coverage.cobertura.xml文件来生成报告,输出目录为coveragereport

x

执行之后,在目录中就会出现一个名为coveragereport的文件夹,打开以后点击里面的Index文件打开网页。

哇,效果舒服多了:

x

这里您会看到有两个度量指标:一个叫做Line coverage(语句覆盖),另一个叫做Branch coverage(分支覆盖率)。然后您可以点击咱们的源代码文件进入,看看为什么会有这样的结果:

x

红色的部分就是咱们已经覆盖的语句,直观的就能看到我们测试了哪些代码。而左侧箭头所标记的地方就是具有分支的地方,这个s > 10 && checkSing就是一个明显的分支。

通过这种方式我们就能够清楚并且直观的知道我们的代码哪些完成了测试,哪些地方有遗漏等。

单元测试 + 代码覆盖率 的方式能够大幅度的减少我们开发中隐藏的bug,特别是作为个人开发者来说,因为没有专门的测试人员,所以需要自己检测自己的代码,纯靠肉眼来观察的话是很粗糙的,毕竟自己写的代码自己最难发现bug。因此假如时间允许,我们应该尽可能的引入单元测试代码覆盖率

一般来说,编写单元测试会扩大代码量至3倍以上,所以这也是很多公司或者开发者选择放弃使用单元测试的原因。但是“出来混迟早是要还的”,假如是一个长期运行的项目,越早发现bug是越关键的一件事,这将关系到项目后期能否稳定运行下去。

注意!!!,哪怕代码覆盖率达到了100%,也不是证明项目就不会出现bug了。单元测试的全覆盖只能证明您的单元没有问题,需求理解错误或者功能集成时所导致的bug是不会在该阶段被发现的,因此我们还是需要进行其它的测试,比如集成测试,自动化接口测试等。

Azure Devops

既然有了这么好的单元测试代码覆盖率,那我肯定希望每次提交代码的时候就能够为这次的代码进行一次测试和反馈。所以咱们可以使用微软这些年吹爆了的Azure平台,人人上云,云上两开花。

接下来,将展示如何利用Azure Devops进行自动化构建。在这之前,咱们先来看看微软为我们开发者带来的一些福利:

x
x

关于自动化构建,您也可以选用Github Action。大家都知道,自从Github被纳入到微软旗下之后,势头也是越发的猛,现在Github teams都直接免费了。再来看看Azure Devops这边,假如是开源项目,直接免费使用,就算是私有,每个都有30个小时的使用时间。这两兄弟双管齐下,尔等小菜只能说一句“微软巨硬牛B”。

每一次自动化构建的Job背后都是使用云服务器的资源来进行构建,微软直接在Github和Azure这边提供了免费的资源来供您构建,配置好像还是一台2核4G的主机。用老罗的话来说,真的是“不赚钱,就交个朋友”。

所以要使用Azure Devops的话,请先注册您的微软账号。下面的演示我将代码托管在Github上,权限为公开,然后从Azure Devops这边链接Github的库进行构建。

Pipelines中新建一个Pipelines:

Pipelines

x

我这里选择的是Github的代码库,然后下一步进行选择,选择项会有几个模板供您选择,您可以随意选择一个AspNet Core的模板,然后进行下一步进行配置。在下面的图片中,表示了一个对.NET Core程序进行“自动生成->测试->生成代码覆盖率”的job,您可以根据您的自身情况进行参考和更改:

x
x

然后提交该配置。当master分支的代码进行变动的时候,job就会自动执行,执行的结果可以在Pipelines看到:

x

再来看看咱们的代码度量结果:

x

完美。

徽章收集

不知道有没有人像一样,很喜欢点QQ图标之类的东西。(所以我在博客园添加了两个徽章😂)

当然,使用徽章的话可以让用户一下就了解到项目的情况,比如版本号,下载数量,开源协议等等。

x

Azure Piplines提供了一个徽章,您可以从job的右上角获取到:

x

该徽章是关于job的构建成功的信息。但是如果您想获取到其它的信息,可以使用shields来进行获取:

打开shields的官网:“shields.io/category/do… 选择您所需要添加的徽章类别,这里咱们选择了Azure Coverage:

进行输入对应信息后,就可以获取到刚才咱们job中所得到的代码覆盖率的结果了。

然后…………选则一些您需要展示的信息,很快就累计了一排勋章了😂。

最后

说几个大家可能在单元测试过程中可能涉及到的几个小点:

  1. 有时候您会测试一个internal级别的类,但是当测试项目引用之后是没有办法找到该类的,您可以通过将程序集标记为对测试项目可见来进行测试:
[assembly: InternalsVisibleTo("MyDemo.Tests")]
namespace DuDuDu.MyDemo.Internals
{
    internal class DefaultDemoClass 
    {
    }
}

2.如果有依赖注入怎么办? 比如咱们测试AspNetCore的应用时,会有很多类其实是被注入到了DI容器中,但是测试的类又依赖了这些类。

可以使用ServiceCollection来作为测试的容器实现,然后把涉及的服务都添加进去:

[Fact]
public void Test()
{
    IServiceCollection Services = new ServiceCollection();
    Services.AddTransient<xx,xx>();
    Services.AddTransient<xx,xx>();
    Services.AddTransient<xx,xx>();

    var provider = Services.BuildServiceProvider();
    var testClass = provider.GetService<xx>();

    testClass.TestMethod();
}

如果有依赖的类还没有实现的时候,可以通过Mock的方法来模拟一个接口完成操作。

3.如果项目多了的话,怎么执行测试和代码度量呢?

我现在选用的是使用Powershell脚本来编写脚本完成的。开发的时候利用VS的“测试资源管理器”来进行单元测试,当单元测试验证的差不多的时候,使用“Powershell”脚本来进行代码覆盖率进行测试,查看忽略的代码然后继续测试。测试通过之后再提交代码到Github,然后Azure Devops进行构建。

好啦,今天的内容有些多,但是对您开发.NET Core项目来说的话,是实实在在的有用。

预告一下,下一期会为大家带来“对AspNet Core返回结果进行自动包装”的文章😄。

最后,偷偷说一句:创作不易,点个推荐吧.....

x