20.1、单元测试

基本的单元测试,可以在系统测试之前,把大部分比较低级的错误都消灭掉。

什么是单元测试

【引用百度百科】

单元测试(unit testing),是指对软件中的最小可测试单元进行检查和验证。对于单元测试中单元的含义,一般来说,要根据实际情况去判定其具体含义,如C语言中单元指一个函数,Java里单元指一个类,图形化的软件中可以指一个窗口或一个菜单等。总的来说,单元就是人为规定的最小的被测功能模块。单元测试是在软件开发过程中要进行的最低级别的测试活动,软件的独立单元将在与程序的其他部分相隔离的情况下进行测试。

单元测试好处

消灭低级错误

基本的单元测试,可以在系统测试之前,把大部分比较低级的错误都消灭掉,减少系统测试过程中的问题,这样也就减少了系统测试中定位和解决问题的时间成本了。

找出潜在的bug

某些类型的bug,靠系统测试是很难找到的。例如一些代码分支,平时99%的场景基本上都走不到,但一旦走到了,如果没有提前测试好,那么可能就是一个灾难。

上库前的保证

加了新代码,上库前跑一把单元测试,都通过,说明代码可能没有影响到之前的逻辑,这样上库也比较放心。如果之前的单元测试跑不过,那么很有可能新的代码有潜在的问题,赶紧修复去吧。

重构代码的机会

写单元测试的过程中,你可能会顺手把一些code重构了,为什么?举例,一些长得非常像的代码,如果每次都要写一堆测试代码去测同样的code,你会不会抓狂?不测吧,覆盖率又上不去,于是我就会想方设法把待测试的code改得尽量的精简,重复代码减少,这样覆盖率上去了,测试也好测了,代码也简洁了。如果没有单元测试和覆盖率的要求的话,坦白说可能一来自己不会发现这些重复的code,另一方面即使发现了,可能也没有太大的动力去改进。

另外,由于单元测试中,你需要尝试去覆盖一些异常分支,这是系统测试常常走不到的地方,于是就会引起你的一些思考,例如这个异常分支是否真的需要?是否真的会发生?对于一些实际上绝对不会出错的函数,那么我觉得可能异常分支是没必要存在的。

重新review代码的机会

写UT的过程中,我总是会好好看哪些代码执行到了,哪些代码没有执行到,这其实也是一个review自己代码的机会,有些时候,并不是UT本身帮我找到bug,而是回头review自己代码的时候发现的。

单元测试几种类型

  • 基于API接口测试(白盒+浅度黑盒测试)

  • 基于项目代码测试(深度黑盒测试)

主流的单元测试包

  • MSTest(推荐,和VS 2019 深度集成)

  • NUnit

  • xUnit(最流行的库)

SpecFlow 和 Gherkin

什么是 SpecFlow

SpecFlow 是.Net平台的BDD工具,可以用自然语言编写测试用例,根据这些测试案例,能够根据测试案例生成测试类、测试方法及到处测试汇总报表。

行为驱动开发(Behavior-Driven Development)(简写BDD),在软件工程中,BDD是一种敏捷软件开发的技术。

什么是Gherkin-BDD语言?

在了解Gherkin之前,有必要了解跨项目不同领域的公共语言的重要性和需求。我所说的不同领域是指客户、开发人员、测试人员、业务分析师和管理团队。让我们先讨论开发项目中的常见问题,然后再讨论解决方案,在此过程中,我们将遇到对公共语言的需求。

所以为了能够让不同领域的任意都能够了解业务功能,就有了 Gherkin。

Gherkin使用一组特殊的关键字来为可执行规范赋予结构和意义,而且文件后缀名为:*.feature,如 test_hoa_author.feature

Feature: [Test Url]: /api/Hoa/Author

	[Function description]:
		Get frame author information.

	[Test Cases]:
		1) Can be accessed normally.
		2) Interface return value contains "Powered by Monk".
		3) Interface exception while entering true value

@case1
Scenario: 1) Can be accessed normally.
	When I submit
	Then The result should not be empty and error free

@case2
Scenario: 2) Interface return value contains "Powered by Monk".
	When I submit
	Then The result should return """Powered by Monk""" string.

@case3
Scenario: 3) Interface exception while entering true value
	Given I input """true""" value
	When I submit
	Then The result should throw exception.

Gherkin 常用的关键字

  • Feature:目的是提供软件特性的高级描述,并对相关场景进行分组。

  • Scenario/Scenario Outline:我们具体的测试案例场景,建议统一用 Scenario Outline

  • Given:用于描述系统的初始上下文—场景,也就是我们常说的 假设

  • And:是对 Given的一个补充

  • When:描述动作或行为

  • Then:用于描述预期的结果

  • But:对 Then 的一个补充

  • Examples:指定多个假设,也就是同一个场景不同的假设条件。

  • @mytag:用来指定场景唯一标识,没实际作用,只做标识。

  • #:编写注释

Gherkin 传递参数

  1. Gherkin 默认能够对 关键字中的描述带有 数值类型的符号进行解析成参数,如:

Feature: Calculator
       In order to avoid silly mistakes
       As a math idiot
       I want to be told the sum of two numbers

@mytag
Scenario: Add two numbers
       Given I have entered 50 into the calculator
       And I have also entered 70 into the calculator
       When I press add
       Then the result should be 120 on the screen

此时,Scenario 中的 Given/And/Then 中包含的 50、70、120 会自动解析成参数。

2. 传递字符串参数,如果需要传入字符串类型,只需要采用 三引号即可:""",如:

@case2
Scenario: 2) Interface return value contains "Powered by Monk".
	When I submit
	Then The result should return """Powered by Monk""" string.

此时,Powered by Monk 就会自动生成字符串参数。

3. 传递对象、数组类型参数,采用类似 Markdown 表格的写法,如:

Given the following users exist:
  | name   | email              | twitter         |
  | Aslak  | aslak@cucumber.io  | @aslak_hellesoy |
  | Julien | julien@cucumber.io | @jbpros         |
  | Matt   | matt@cucumber.io   | @mattwynne      |

4. 相同测试场景,不同的测试参数,如:

正常写法:

Scenario: eat 5 out of 12
  Given there are 12 cucumbers
  When I eat 5 cucumbers
  Then I should have 7 cucumbers

Scenario: eat 5 out of 20
  Given there are 20 cucumbers
  When I eat 5 cucumbers
  Then I should have 15 cucumbers

推荐写法:

Scenario Outline: eating
  Given there are <start> cucumbers
  When I eat <eat> cucumbers
  Then I should have <left> cucumbers

  Examples:
    | start | eat | left |
    |    12 |   5 |    7 |
    |    20 |   5 |   15 |

特别注意👈👈👈

在 Given/And/When/Then/But 中,必须与 "." 作为语句结束符,不如会生成不正确的语句。👈👈👈

更多 Gherkin 知识可查阅官方文档

编写单元测试

前期配置

第一步

在 Visual Studio 2019 中安装 SpecFlow for Visual Studio 2019

安装后重启 Visual Studio 2019 即可。

第二步

在Visual Studio 2019 中登录你的 outlook.com 账号,然后选择 Hoa.MSTest.Remote 项目,右键选择 运行测试然后在 输出 工具栏设置 显示输出来源测试。就会看到有这样一串链接,点击注册即可,注册成功后重启 Visual Studio 即可完成单元测试基础配置操作。

编写测试案例

Hoa.MSTest.Remote 项目下的 Features 文件夹下创建 .feature 文件,如,这里我们测试 /api/Member/PersonalDetails 接口,我们可以创建命为:Test_Api_Member_PersonalDetails.feature

假设我们要测试以下案例:👈👈👈

  1. 接口能够正常访问

  2. 如果不输入 userName 参数,提示 “用户名不能为空”

  3. 如果输入错误的 `userName 参数,提示 “用户不存在”

  4. 如果输入正确的 `userName" 参数:snrcsoft,则正确返回数据

这时,我们就可以将 上述的测试案例翻译成 Gherkin 语言,如:

Feature: [Test Url]: /api/Member/PersonalDetails

	[Function description]:
		Get member contact informations.

	[Test Cases]:
		1) Can be accessed normally.
		2) If the userName parameter is empty, return "The userName field is required."
		3) If set the userName parameter incorrect value, return "The user does not exist".
		4) If set the userName parameter is "snrcsoft", return member informations.

@case1
Scenario Outline: 1) Can be accessed normally.
	When I submit.
	Then The result should be not empty and error free.

@case2
Scenario Outline: 2) If the userName parameter is empty, return "The userName field is required.".
	When I submit.
	Then The result should be """The userName field is required.""".

@case3
Scenario Outline: 3) If set the userName parameter incorrect value, return "The user does not exist".
	Given I input """testname""" value.
	When I submit.
	Then The result should be """The user does not exist""".

@case4
Scenario Outline: 4) If set the userName parameter is "snrcsoft", return member informations.
	Given I input """snrcsoft""" value.
	When I submit.
	Then The result should be
		| email             | phone          |
		| snrc@snrcsoft.com | (111) 111-1111 |

生成单元测试类

Test_Api_Member_PersonalDetails.feature 右键选择 Generate Step Definitions,并在弹出的菜单中点击 Preview 预览是否正确生成,确保没问题后点击 Generate 按钮,并生成到 Hoa.MSTest.RemoteStepDefinitions文件夹中。

编写测试断言

第一步

Hoa.MSTest.SpecFlowMaterials 文件夹下的 IApi_Hoa.cs 中添加下列代码:

using Hoa.MSTest.SpecFlow.Models;
using Refit;
using System.Threading.Tasks;

namespace Hoa.MSTest.SpecFlow.Materials
{
    public interface IApi_Hoa
    {
        [Post("/api/Member/PersonalDetails")]
        Task<object> GetMemberPersonalDetails([Query] string userName);
    }
}

第二步

在刚刚生成的 TestUrlApiMemberPersonalDetailsSteps.cs 中写入以下代码:

using FluentAssertions;
using Hoa.MSTest.SpecFlow;
using Hoa.MSTest.SpecFlow.Extensions;
using Hoa.MSTest.SpecFlow.Materials;
using Newtonsoft.Json.Linq;
using Refit;
using TechTalk.SpecFlow;
using TechTalk.SpecFlow.Assist;

namespace Hoa.MSTest.Remote.StepDefinitions
{
    [Binding]
    public class TestUrlApiMemberPersonalDetailsSteps
    {
        object result;
        string userName;

        int statusCode;

        [Given(@"I input """"""(.*)"""""" value\.")]
        public void GivenIInputValue_(string giveData)
        {
            userName = giveData;
        }

        [When(@"I submit\.")]
        public void WhenISubmit_()
        {
            var (response, status, success) = RestService.For<IApi_Hoa>(TestConst.SERVER_API_ADDRESS)
                 .GetMemberPersonalDetails(userName)
                 .ToSync();

            statusCode = status;

            // 这里判断是否返回200状态码
            if (success)
            {
                result = response.Content;
            }
            else
            {
                // 由于接口没有统一返回值,所以这里才这么复杂
                result = status == 400 ? JArray.Parse(response.Error.Content).First["Message"].Value<string>() : response.Error.Content;
            }
        }

        [Then(@"The result should be not empty and error free\.")]
        public void ThenTheResultShouldBeNotEmptyAndErrorFree_()
        {
            statusCode.Should().NotBe(500);
        }

        [Then(@"The result should be """"""(.*)""""""\.")]
        public void ThenTheResultShouldBe_(string expectedValue)
        {
            result.ToString().Should().StartWith(expectedValue);
        }

        [Then(@"The result should be")]
        public void ThenTheResultShouldBe(Table expectedValue)
        {
            var email = expectedValue.Rows[0].GetString("email");
            var phone = expectedValue.Rows[0].GetString("phone");
            result.ToString().Should().Contain(email).And.Contain(phone);
        }
    }
}

运行单元测试

点击 Visual Studio 2019 顶部 测试 菜单,点击 运行所有测试 并打开 测试资源管理器,并点击运行所有测试即可看到所有结果。

关于断言

断言是编程术语,表示为一些布尔表达式,程序员相信在程序中的某个特定点该表达式值为真,可以在任何时候启用和禁用断言验证,因此可以在测试时启用断言而在部署时禁用断言。

同样,程序投入运行后,最终用户在遇到问题时可以重新启用断言。 使用断言可以创建更稳定、品质更好且 不易于出错的代码。当需要在一个值为FALSE时中断当前操作的话,可以使用断言。单元测试必须使用断言。

断言有几种方式

在Hoa Framework 中支持两种断言方式:

  • Assert:这时是MSTest 单元测试默认自带的,在 Microsoft.VisualStudio.TestTools.UnitTesting 命名空间下

  • FluentAssertions:这时第三方提供的库,拥有非常丰富的断言方法,推荐使用

回归测试

回归测试是指修改了旧代码后,重新进行测试以确认修改没有引入新的错误或导致其他代码产生错误。自动回归测试将大幅降低系统测试、维护升级等阶段的成本。

回归测试作为软件生命周期的一个组成部分,在整个软件测试过程中占有很大的工作量比重,软件开发的各个阶段都会进行多次回归测试。在渐进和快速迭代开发中,新版本的连续发布使回归测试进行的更加频繁,而在极端编程方法中,更是要求每天都进行若干次回归测试。因此,通过选择正确的回归测试策略来改进回归测试的效率和有效性是很有意义的。

如何进行回归测试

在 Hoa Framework 中,只需要重新 运行所有测试即可

导出报表

在 Hoa Framework 框架中,只要运行过单元测试,就会自动在 项目根目录下生成 TestResults 文件夹,里面就包含了所有测试的数据报表。

常见错误

  1. All steps are bound!

这个提示的意思是此测试案例已经生成过测试类了,无需再次生成,如果需要强制生成,只需要在运行中输入:%TEMP%,然后在打开的文件夹中搜索 specflow 并删除带 specflow 的文件即可。

关于项目代码测试

在 Hoa Framework 框架中,支持两种单元测试,上面一种是基于接口测试,还有一种是基于项目代码测试,主要是在 Hoa.MSTest.Projects 项目层中,该层已经自动引用了 Hoa, Hoa.Application, Hoa.Core, Hoa.EntityFramework.Core 项目引用了,只需要手动编写测试调用代码即可。

由于我们大量用到了依赖注入方式创建对象,所以框架默认引如了 Moq 第三方组件,可以快速帮助我们创建对象的所有依赖。

最后更新于