首页 > 其他分享 >单元测试转载

单元测试转载

时间:2023-03-18 09:12:09浏览次数:49  
标签:NUnit 单元测试 Assert command DigitDataProvider new 转载 public

NUnit使用

NUnit是.net平台上使用得最为广泛的测试框架之一,本文将通过示例来描述NUnit的使用方法,并提供若干编写单元测试的建议和技巧,供单元测试的初学者参考。

继续下文之前,先来看看一个非常简单的测试用例(TestCase):

  1. [Test]  
  2. public void AdditionTest()  
  3. {  
  4.     int expectedResult = 2;  
  5.     Assert.AreEqual(exptectedResult, 1 + 1);  
  6. }  
 

你肯定会说这个TestCase也太白痴了吧!这也是许多NUnit文档被人诟病的一点,但是我的理解并不是这样,xUnit本来就是编写UT的简易框架,keep it simple and stupid,任何通过复杂的TestCase来介绍NUnit的用法都是一种误导,UT复杂之处在于如何在实际项目中应用和实施,而不是徘徊于该如何使用NUnit。
主要内容:
1、NUnit的基本用法
2、测试用例的组织
3、NUnit的断言(Assert)
4、常用单元测试工具介绍
一、NUnit的基本用法
和其他xNUnit框架不同的是,NUnit框架使用Attribute(如前面代码中的[Test])来描述测试用例的,也就是说我们只要掌握了 Attribute的用法,也就基本学会如何使用NUnit了。VSTS所集成的单元测试也支持类似NUnit的Attributes,下表对比了 NUnit和VSTS的标记:

usage

NUnit attributes

VSTS attributes

标识测试类

TestFixture

TestClass

标识测试用例(TestCase)

Test

TestMethod

标识测试类初始化函数

TestFixtureSetup

ClassInitialize

标识测试类资源释放函数

TestFixtureTearDown

ClassCleanup

标识测试用例初始化函数

Setup

TestInitialize

标识测试用例资源释放函数

TearDown

TestCleanUp

标识测试用例说明

N/A

Description

标识忽略该测试用例

Ignore

Ignore

标识该用例所期望抛出的异常

ExpectedException

ExpectedException

标识测试用例是否需要显式执行

Explicit

?

标识测试用例的分类

Category

?

现在,让我们找一个场景,通过示例来了解上述NUnit标记的用法。来看看一个存储在数据库中的数字类:
这是我们常见的DAL+Entity的设计,DigitDataProvider和Digit类的实现代码如下:
1)Digit.cs类:

 
  1. using System;  
  2. using System.Data;  
  3.   
  4. namespace Product  
  5. {  
  6.     /// <summary>  
  7.     /// Digit 的摘要说明  
  8.     /// </summary>  
  9.     /// 创 建 人: Aero  
  10.     /// 创建日期: 2006-3-22  
  11.     /// 修 改 人:   
  12.     /// 修改日期:  
  13.     /// 修改内容:  
  14.     /// 版    本:  
  15.     public class Digit  
  16.     {  
  17.         private Guid _digitId;  
  18.         public Guid DigitID  
  19.         {  
  20.             get { return this._digitId; }  
  21.             set { this._digitId = value; }  
  22.         }  
  23.   
  24.         private int _value = 0;  
  25.         public int Value  
  26.         {  
  27.             get { return this._value; }  
  28.             set { this._value = value; }  
  29.         }  
  30.  
  31.         #region 构造函数  
  32.         /// <summary>  
  33.         /// 默认无参构造函数  
  34.         /// </summary>  
  35.         /// 创 建 人: Aero  
  36.         /// 创建日期: 2006-3-22  
  37.         /// 修 改 人:   
  38.         /// 修改日期:  
  39.         /// 修改内容:  
  40.         public Digit()  
  41.         {  
  42.             //  
  43.             // TODO: 在此处添加构造函数逻辑  
  44.             //  
  45.         }  
  46.   
  47.         /// <summary>  
  48.         /// construct the digit object from a datarow  
  49.         /// </summary>  
  50.         /// <param name="row"></param>  
  51.         public Digit(DataRow row)  
  52.         {  
  53.             if (row == null)  
  54.             {  
  55.                 throw new ArgumentNullException();  
  56.             }  
  57.   
  58.             if (row["DigitID"] != DBNull.Value)  
  59.             {  
  60.                 this._digitId = new Guid(row["DigitID"].ToString());  
  61.             }  
  62.   
  63.             if (row["Value"] != DBNull.Value)  
  64.             {  
  65.                 this._value = Convert.ToInt32(row["Value"]);  
  66.             }  
  67.         }  
  68.          
  69.         #endregion  
  70.     }  
  71. }  
 

2)DigitDataProvider类:

  
  1. using System;  
  2. using System.Data;  
  3. using System.Data.SqlClient;  
  4. using System.Collections;  
  5.   
  6. namespace Product  
  7. {  
  8.     /// <summary>  
  9.     /// DigitDataProvider 的摘要说明  
  10.     /// </summary>  
  11.     /// 创 建 人: Aero  
  12.     /// 创建日期: 2006-3-22  
  13.     /// 修 改 人:   
  14.     /// 修改日期:  
  15.     /// 修改内容:  
  16.     /// 版    本:  
  17.     public class DigitDataProvider  
  18.     {  
  19.         /// <summary>  
  20.         /// 定义数据库连接  
  21.         /// </summary>  
  22.         private SqlConnection _dbConn;  
  23.         public SqlConnection Connection  
  24.         {  
  25.             get { return this._dbConn; }  
  26.             set { this._dbConn = value; }  
  27.         }  
  28.          
  29.         #region 构造函数  
  30.         /// <summary>  
  31.         /// 默认无参构造函数  
  32.         /// </summary>  
  33.         /// 创 建 人: Aero  
  34.         /// 创建日期: 2006-3-22  
  35.         /// 修 改 人:   
  36.         /// 修改日期:  
  37.         /// 修改内容:  
  38.         public DigitDataProvider()  
  39.         {  
  40.             //  
  41.             // TODO: 在此处添加构造函数逻辑  
  42.             //  
  43.         }  
  44.   
  45.         public DigitDataProvider(SqlConnection conn)  
  46.        {  
  47.             this._dbConn = conn;  
  48.         }  
  49.          
  50.         #endregion  
  51.          
  52.         #region 成员函数定义  
  53.   
  54.         /// <summary>  
  55.         /// retrieve all Digits in the database  
  56.         /// </summary>  
  57.         /// <returns></returns>  
  58.         public ArrayList GetAllDigits()  
  59.         {  
  60.             // retrieve all digit record in database  
  61.             SqlCommand command = this._dbConn.CreateCommand();  
  62.             command.CommandText = "SELECT * FROM digits";  
  63.             SqlDataAdapter adapter = new SqlDataAdapter(command);  
  64.             DataSet results = new DataSet();  
  65.             adapter.Fill(results);  
  66.   
  67.             // convert rows to digits collection  
  68.             ArrayList digits = null;  
  69.   
  70.             if (results != null && results.Tables.Count > 0)  
  71.             {  
  72.                 DataTable table = results.Tables[0];  
  73.                 digits = new ArrayList(table.Rows.Count);  
  74.   
  75.                 foreach (DataRow row in table.Rows)  
  76.                 {  
  77.                     digits.Add(new Digit(row));  
  78.                 }  
  79.             }  
  80.   
  81.             return digits;  
  82.         }  
  83.   
  84.         /// <summary>  
  85.         /// remove all digits from the database  
  86.         /// </summary>  
  87.         /// <returns></returns>  
  88.         public int RemoveAllDigits()  
  89.         {  
  90.             // retrieve all digit record in database  
  91.             SqlCommand command = this._dbConn.CreateCommand();  
  92.             command.CommandText = "DELETE FROM digits";  
  93.   
  94.             return command.ExecuteNonQuery();  
  95.         }  
  96.   
  97.         /// <summary>  
  98.         /// retrieve and return the entity of given value  
  99.         /// </summary>  
  100.         /// <exception cref="System.NullReferenceException">entity not exist in the database</exception>  
  101.         /// <param name="value"></param>  
  102.         /// <returns></returns>  
  103.         public Digit GetDigit(int value)  
  104.         {  
  105.             // retrieve entity of given value  
  106.             SqlCommand command = this._dbConn.CreateCommand();  
  107.             command.CommandText = "SELECT * FROM digits WHERE Value='" + value + "'";  
  108.             SqlDataAdapter adapter = new SqlDataAdapter(command);  
  109.             DataSet results = new DataSet();  
  110.             adapter.Fill(results);  
  111.   
  112.             // convert rows to digits collection  
  113.             Digit digit = null;  
  114.   
  115.             if (results != null && results.Tables.Count > 0  
  116.                 && results.Tables[0].Rows.Count > 0)  
  117.             {  
  118.                 digit = new Digit(results.Tables[0].Rows[0]);  
  119.             }  
  120.             else  
  121.             {  
  122.                 throw new NullReferenceException("not exists entity of given value");  
  123.             }  
  124.   
  125.             return digit;  
  126.         }  
  127.   
  128.         /// <summary>  
  129.         /// remove prime digits from database  
  130.         /// </summary>  
  131.         /// <returns></returns>  
  132.         public int RemovePrimeDigits()  
  133.         {  
  134.             throw new NotImplementedException();  
  135.         }  
  136.  
  137.         #endregion  
  138.     }  
  139. }  
 

3)新建测试数据库:

  1. CREATE TABLE [dbo].[digits] (  
  2.     [DigitID] [uniqueidentifier] NOT NULL ,  
  3.     [Value] [int] NOT NULL   
  4. ) ON [PRIMARY]  
  5. GO  

下面,我们开始尝试为DigitDataProvider类编写UT,新建DigitDataProviderTest.cs类。
1、添加nunit.framework引用:

并在DigitDataProviderTest.cs中添加:

using NUnit.Framework;

2、编写测试用例
1)标识测试类:NUnit要求每个测试类都必须添加TestFixture的Attribute,并且携带一个public无参构造函数。

  1. [TestFixture]  
  2. public class DigitProviderTest  
  3. {  
  4.     public DigitProviderTest()  
  5.     {  
  6.     }  
  7. }  
 

2)编写DigitDataProvider.GetAllDigits()的测试函数

 
  1. /// <summary>  
  2. /// regular test of DigitDataProvider.GetAllDigits()  
  3. /// </summary>  
  4. [Test]  
  5. public void TestGetAllDigits()  
  6. {  
  7.     // initialize connection to the database  
  8.     // note: change connection string to ur env  
  9.     IDbConnection conn = new SqlConnection(  
  10.         "Data source=localhost;user id=sa;password=sa;database=utdemo");  
  11.     conn.Open();  
  12.   
  13.     // preparing test data  
  14.     IDbCommand command = conn.CreateCommand();  
  15.     string commadTextFormat = "INSERT INTO digits(DigitID, Value) VALUES('{0}', '{1}')";  
  16.   
  17.     for (int i = 1; i <= 100; i++)  
  18.     {  
  19.         command.CommandText = string.Format(commadTextFormat, Guid.NewGuid().ToString(), i.ToString());  
  20.         command.ExecuteNonQuery();  
  21.     }  
  22.     // test DigitDataProvider.GetAllDigits()  
  23.     int expectedCount = 100;  
  24.     DigitDataProvider provider = new DigitDataProvider(conn as SqlConnection);  
  25.     IList results = provider.GetAllDigits();  
  26.   
  27.     // that works?  
  28.     Assert.IsNotNull(results);  
  29.     Assert.AreEqual(expectedCount, results.Count);  
  30.   
  31.    // delete test data  
  32.     command = conn.CreateCommand();  
  33.     command.CommandText = "DELETE FROM digits";  
  34.     command.ExecuteNonQuery();  
  35.   
  36.     // close connection to the database  
  37.     conn.Close();  
  38. }  
 

什么?很丑?很麻烦?这个问题稍后再讨论,先来看看一个完整的测试用例该如何定义:

  1. [Test]  
  2. public void TestCase()  
  3. {  
  4.     // 1) initialize test environement, like database connection  
  5.       
  6.   
  7.     // 2) prepare test data, if neccessary  
  8.       
  9.   
  10.     // 3) test the production code by using assertion or Mocks.  
  11.       
  12.   
  13.     // 4) clear test data  
  14.       
  15.   
  16.     // 5) reset the environment  
  17.       
  18. }  
 

NUnit要求每一个测试函数都可以独立运行(往往有人会误解NUnit并按照Consoler中的排序来执行),这就要求我们在调用目标函数之前先要初始化目标函数执行所需要的环境,如打开数据库连接、添加测试数据等。为了不影响其他的测试函数,在调用完目标函数后,该测试函数还要负责还原初始环境,如删除测试数据和关闭数据库连接等。对于同一测试类里的测试函数来说,这些操作往往是相同的,让我们对上面的代码进行一次Refactoring, Extract Method:

 
  1. /// <summary>  
  2. /// connection to database  
  3. /// </summary>  
  4. private static IDbConnection _conn;  
  5.   
  6. /// <summary>  
  7. /// 初始化测试类所需资源  
  8. /// </summary>  
  9. [TestFixtureSetUp]  
  10. public void ClassInitialize()  
  11. {  
  12.     // note: change connection string to ur env  
  13.     DigitProviderTest._conn = new SqlConnection(  
  14.         "Data source=localhost;user id=sa;password=sa;database=utdemo");  
  15.     DigitProviderTest._conn.Open();  
  16. }  
  17.   
  18. /// <summary>  
  19. /// 释放测试类所占用资源  
  20. /// </summary>  
  21. [TestFixtureTearDown]  
  22. public void ClassCleanUp()  
  23. {  
  24.     DigitProviderTest._conn.Close();  
  25. }  
  26.   
  27. /// <summary>  
  28. /// 初始化测试函数所需资源  
  29. /// </summary>  
  30. [SetUp]  
  31. public void TestInitialize()  
  32. {  
  33.     // add some test data  
  34.     IDbCommand command = DigitProviderTest._conn.CreateCommand();  
  35.     string commadTextFormat = "INSERT INTO digits(DigitID, Value) VALUES('{0}', '{1}')";  
  36.   
  37.     for (int i = 1; i <= 100; i++)  
  38.     {  
  39.         command.CommandText = string.Format(  
  40.             commadTextFormat, Guid.NewGuid().ToString(), i.ToString());  
  41.         command.ExecuteNonQuery();  
  42.     }  
  43. }  
  44.   
  45. /// <summary>  
  46. /// 释放测试函数所需资源  
  47. /// </summary>  
  48. [TearDown]  
  49. public void TestCleanUp()  
  50. {  
  51.     // delete all test data  
  52.     IDbCommand command = DigitProviderTest._conn.CreateCommand();  
  53.     command.CommandText = "DELETE FROM digits";  
  54.   
  55.     command.ExecuteNonQuery();  
  56. }  
  57.   
  58. /// <summary>  
  59. /// regular test of DigitDataProvider.GetAllDigits()  
  60. /// </summary>  
  61. [Test]  
  62. public void TestGetAllDigits()  
  63. {  
  64.     int expectedCount = 100;  
  65.     DigitDataProvider provider =   
  66.         new DigitDataProvider(DigitProviderTest._conn as SqlConnection);  
  67.   
  68.     IList results = provider.GetAllDigits();  
  69.     // that works?  
  70.     Assert.IsNotNull(results);  
  71.     Assert.AreEqual(expectedCount, results.Count);  
  72. }  
 

NUnit提供了以下Attribute来支持测试函数的初始化:
TestFixtureSetup:在当前测试类中的所有测试函数运行前调用;
TestFixtureTearDown:在当前测试类的所有测试函数运行完毕后调用;
Setup:在当前测试类的每一个测试函数运行前调用;
TearDown:在当前测试类的每一个测试函数运行后调用。

3)编写DigitDataProvider.RemovePrimeDigits()的测试函数
唉,又忘了质数判断的算法,这个函数先不实现(throw new NotImplementedException()),对应的测试函数先忽略。

 
  1. /// <summary>  
  2. /// regular test of DigitDataProvider.RemovePrimeDigits  
  3. /// </summary>  
  4. [Test, Ignore("Not Implemented")]  
  5. public void TestRemovePrimeDigits()  
  6. {  
  7.     DigitDataProvider provider =   
  8.         new DigitDataProvider(DigitProviderTest._conn as SqlConnection);  
  9.   
  10.     provider.RemovePrimeDigits();  
  11. }  
 

Ignore的用法:

Ignore(string reason)

4)编写DigitDataProvider.GetDigit()的测试函数
当查找一个不存在的Digit实体时,GetDigit()会不会像我们预期一样抛出NullReferenceExceptioin呢?

 
  1. /// <summary>  
  2. /// Exception test of DigitDataProvider.GetDigit()  
  3. /// </summary>  
  4. [Test, ExpectedException(typeof(NullReferenceException))]  
  5. public void TestGetDigit()  
  6. {  
  7.     int expectedValue = 999;  
  8.     DigitDataProvider provider = new DigitDataProvider(DigitProviderTest._conn as SqlConnection);  
  9.     Digit digit = provider.GetDigit(expectedValue);  
  10. }  
 

ExpectedException的用法

ExpectedException(Type t)
ExpectedException(Type t, string expectedMessage)

在NUnitConsoler里执行一把,欣赏一下黄绿灯吧。本文相关代码可从UTDemo_Product.rar下载。
二、测试函数的组织
现在有一个性能测试的Testcase,执行一次要花上一个小时,我们并不需要(也无法忍受)每次自动化测试时都去执行这样的Testcase,使用NUnit的Explicit标记可以让这个TestCase只有在显示调用下才会执行:

  1. [Test, Explicit]  
  2. public void OneHourTest()  
  3. {  
  4.     //  
  5. }  
 


不幸的是,这样耗时的TestCase在整个测试工程中可能有数十个,或许更多,我们能不能把这些TestCase都组织起来,要么一起运行,要么不运行呢?NUnit提供的Category标记可实现此功能:

  1. [Test, Explicit, Category("LongTest")]  
  2. public void OneHourTest()  
  3. {  
  4.     ...  
  5. }  
  6.   
  7. [Test, Explicit, Category("LongTest")]  
  8. public void TwoHoursTest()  
  9. {  
  10.     ...  
  11. }  
 

这样,只有当显示选中LongTest分类时,这些TestCase才会执行

三、NUnit的断言
NUnit提供了一个断言类NUnit.Framework.Assert,可用来进行简单的state base test(见idior的Enterprise Test Driven Develop),可别对这个断言类期望太高,在实际使用中,我们往往需要自己编写一些高级断言。
常用的NUnit断言有:

method

usage

example

Assert.AreEqual(object expected, object actual[, string message])

验证两个对象是否相等

Assert.AreEqual(2, 1+1)

Assert.AreSame(object expected, object actual[, string message])

验证两个引用是否指向同意对象

object expected = new object();

object actual = expected;

Assert.AreSame(expected, actual)

Assert.IsFalse(bool)

验证bool值是否为false

Assert.IsFalse(false)

Assert.IsTrue(bool)

验证bool值是否为true

Assert.IsTrue(true)

Assert.IsNotNull(object)

验证对象是否不为null

Assert.IsNotNull(new object())

Assert.IsNull(object)

验证对象是否为null

Assert.IsNull(null);

这里要特殊指出的Assert.AreEqual只能处理基本数据类型和实现了Object.Equals接口的对象的比较,对于我们自定义对象的比较,通常需要自己编写高级断言,这个问题郁闷了我好一会,下面给出一个用于level=1的情况下的对象比较的高级断言的实现:

  1. public class AdvanceAssert  
  2. {  
  3.     /// <summary>  
  4.     /// 验证两个对象的属性值是否相等  
  5.     /// </summary>  
  6.     /// <remarks>  
  7.     /// 目前只支持的属性深度为1层  
  8.     /// </remarks>  
  9.     public static void AreObjectsEqual(object expected, object actual)  
  10.     {  
  11.         // 若为相同引用,则通过验证  
  12.         if (expected == actual)  
  13.         {  
  14.             return;  
  15.         }  
  16.   
  17.         // 判断类型是否相同  
  18.         Assert.AreEqual(expected.GetType(), actual.GetType());  
  19.   
  20.         // 测试属性是否相等  
  21.         Type t = expected.GetType();  
  22.         PropertyInfo[] properties = t.GetProperties(BindingFlags.Instance | BindingFlags.Public);  
  23.   
  24.         foreach (PropertyInfo property in properties)  
  25.         {  
  26.             object obj1 = t.InvokeMember(property.Name,   
  27.                 BindingFlags.Instance | BindingFlags.Public | BindingFlags.GetProperty,   
  28.                 null, expected, null);  
  29.             object obj2 = t.InvokeMember(property.Name,   
  30.                 BindingFlags.Instance | BindingFlags.Public | BindingFlags.GetProperty,   
  31.                 null, actual, null);  
  32.   
  33.             // 判断属性是否相等  
  34.             AdvanceAssert.AreEqual(obj1, obj2, "assertion failed on " + property.Name);  
  35.         }  
  36.     }  
  37.   
  38.     /// <summary>  
  39.     /// 验证对象是否相等  
  40.     /// </summary>  
  41.     private static void AreEqual(object expected, object actual, string message)  
  42.     {  
  43.         Type t = expected.GetType();  
  44.   
  45.         if (t.Equals(typeof(System.DateTime)))  
  46.         {  
  47.             Assert.AreEqual(expected.ToString(), actual.ToString(), message);  
  48.         }  
  49.         else  
  50.         {  
  51.             // 默认使用NUnit的断言  
  52.             Assert.AreEqual(expected, actual, message);  
  53.         }  
  54.     }  
  55. }  
 

四、常用单元测试工具介绍:
1、NUnit:目前最高版本为2.2.7(也是本文所使用的NUnit的版本)
下载地址:http://www.nunit.org
2、TestDriven.Net:一款把NUnit和VS IDE集成的插件

下载地址:http://www.testdriven.net/
3、NUnit2Report:和nant结合生成单元测试报告
下载地址:http://nunit2report.sourceforge.net
4、Rhino Mocks 2:个人认为时.net框架下最好的mocks库,而且支持.net 2.0, rocks~!
下载地址:http://www.ayende.com/projects/rhino-mocks.aspx

出处:http://blog.csdn.net/fogle/article/details/5690510

标签:NUnit,单元测试,Assert,command,DigitDataProvider,new,转载,public
From: https://www.cnblogs.com/zhangfengggg/p/17229369.html

相关文章

  • NET中使用NLog记录日志转载
    .NET中使用NLog记录日志 以前小编记录日志使用的是Log4Net,虽然好用但和NLog比起来稍显复杂。下面小编就和大伙分享一下NLog的使用方式。引用NLog.Config在使用NLog......
  • JSON详解转载
    JSON详解阅读目录JSON的两种结构认识JSON字符串在JS中如何使用JSON在.NET中如何使用JSON总结JSON的全称是”JavaScriptObjectNotation”,意思是JavaScript对象表示......
  • Socket 中运用 BufferedStream 类(转载)
    下面的代码示例演示如何使用 BufferedStream 类,而使用 NetworkStream 类来提高某些I/O操作的性能。在启动客户端之前,在远程计算机上启动服务器。启动客户端时,将远......
  • 连接系统架构目录(SLD)失败 的解决方案 - 转载
    当您从工作站打开SAPB1客户端,并收到以下错误之一:·连接系统架构目录(SLD)失败;请联系系统管理员·连接到许可证服务器时出错;请联系系统管理员此问题可能与工作站上的某些......
  • js中的promise详解【转载】
    一、概述Promise是异步编程的一种解决方案,可以替代传统的解决方案--回调函数和事件。ES6统一了用法,并原生提供了Promise对象。作为对象,Promise有以下两个特点:(1)对......
  • [转载] Python:使用in判断元素是否在列表(list)中,如何提升搜索效率?
    经常会做的一个操作是使用in来判断元素是否在列表中,这种操作非常便捷,省去了自行遍历的工作,而且因为大多数时候列表数据量比较小,搜索的速度也能满足需求。key_list=[1,......
  • Qt内存回收(转载)
    在Qt中创建对象的时候会提供一个Parent对象指针(可以查看类的构造函数),下面来解释这个parent到底是干什么的。QObject是以对象树的形式组织起来的。当你创建一个QObje......
  • [转载] 新版VSCode中Python设置自动补全函数括号
    前言在网上能找到的关于如何让VSCode中Python自动补全函数括号的方法都是同样的,但基本上都是几年前的方法了,在VSCode更新后引入了Pylance,使得之前的设置项不存在了。在自......
  • OpenSSH相关漏洞处理(转载)
    原文链接:https://www.lmlphp.com/user/57828/article/item/2348688/OpenSSH(OpenBSDSecureShell)是OpenBSD计划组所维护的一套用于安全访问远程计算机的连接工具。当前低......
  • 【转载】Socket 与 TCP 四次挥手(下)
    [转载](https://demonlee.tech/archives/2208002)Socket与TCP四次挥手(下)Demon.Lee2022年08月21日206次浏览本文实践环境:Operat......