Nunit工具做C#的单元测试

 

Nunit工具做C#的单元测试

学习心得

 

编写人:罗旭成

时间:2013年9月2日星期一

 

1.开发人员如何做单元测试

单元测试是针对最小的可测试软件元素(单元)的,它所测试的内容包括单元的内部结构(如逻辑和数据流)以及单元的功能和可观测的行为。使用白盒测试方法测试单元的内部结构,使用黑盒测试方法测试单元的功能和可观测的行为。

由于开发方式的不同,单元测试一般划分方法如下:

1. 面向对象的软件开发:以Class(类)作为测试的最小单元。以方法的内部结构作为测试的重点。

2. 结构化的软件开发:以模块(函数、过程)作为测试的最小单元。

a.代码的可读性

开发人员在写代码的时候都有自己的习惯,比如在写一个新的类的时候,类的名称有的用英语命名,有的用汉语拼音命名。而对于注释而言,有的存在一部分,有的从头到尾根本没有注释(这样的代码让开发人员过半年自己再看,他自己也可能不知道这段代码实现的是什么功能)那么是否有一段可用代码编写规范在执行,开发人员是否都按规范来进行开发,一段开发人员自己都看不明白的代码,该如何进行单元测试?特别是对公用的方法要进行注释,这样会减少单元测试的时间。可见规范化的代码编写是进行单元测试必不可少的一部分。

b.设计单元测试

在进行单元测试之前需要弄清楚被测试代码实现的功能和相应的逻辑关系;同时还要考虑到测试的输入内容。也就是说,测试用例的设计。用例的设计要保证面面俱到,是否覆盖了每一条路径。而如何做到面面俱到这就需要开发人员对每个函数进行详细的分析,将分析和讨论的结果归入相关的测试库中。初期工作的进度慢并不要紧,只要能做得很详细对于以后的测试还是有很大的帮助。或许以后的测试中,只要直接调用原来写过的测试类库,修改部分简单的语句就可以实现新模块的单元测试了。

2.Nunit的安装

Nunit是一款开源的C#单元测试工具,下载地址为:http://www.nunit.org/index.php?p=download

目前最高版本为:2.6.2。安装文件为:                         ,

例子文件: ,下载安装。

VS2010 NUnit 整合插件 Visual Nunit 2010下载:http://visualstudiogallery.msdn.microsoft.com/c8164c71-0836-4471-80ce-633383031099 , 下载安装完毕就能在 VS2010 的 view(视图)->Other Windows(其他窗口)中看到 Visual Nunit了(或使用快捷键Ctrl + F7),打开该视图,将之拖到合适的位置。如下图(2.1.1):

 

图(2.1.1)

3.测试样例程序

3.1、新建测试项目UnitTestApp(示例为简单的控制台应用程序),引入nunit.framework类库(默认安装后文件所在路径:C:\Program Files \NUnit 2.6\bin\framework\nunit.framewor.dll)

 

3.2、添加Calculator类,实现一个简单加法和获取单例方法

namespace UnitTestApp

{

    public class Calculator

    {

        public int Add(int a, int b)

        {

            return a + b;

        }

 

        private static readonly object objSync = new object();

        private static Calculator instance = null;

 

        public static Calculator GetInstance()

        {

            if (instance == null)

            {

                lock (objSync)

                {

                    if (instance == null)

                    {

                        instance = new Calculator();

                    }

                }

            }

            return instance;

        }

    }

}

3.3、新增NUnitTest类进行单元测试

using NUnit.Framework;

namespace UnitTestApp

{

    [TestFixture]

    public class NUnitTest

    {

        [Test]

        public void AddTest()

        {

            var calc = new Calculator();

            var result = calc.Add(1, 1);

            Assert.AreEqual(2, result);//判断是否相等

        }

 

        [Test]

        public void AddTestFailure()

        {

            var calc = new Calculator();

            var result = calc.Add(1, 1);

            Assert.AreEqual(10, result); //判断是否相等

        }

 

        [Test]

        public void SingtonTest1()

        {

            var calc = Calculator.GetInstance();

            Assert.IsNull(calc); //判断是否空值

        }

 

        [Test]

        public void SingtonTest2()

        {

            var calc1 = Calculator.GetInstance();

            var calc2 = Calculator.GetInstance();

            Assert.IsTrue(object.Equals(calc1, calc2)); //对象比较

        }

    }

}

编译控制台项目,点击Visual Nunit 插件的“Run”按钮,然后一切清静了。

注:在上面的代码中引用 NUnit.Framework,使用TestFixture标注这是用于测试的类,在其中使用Test表示具体的测试用例。可以看到单元测试中最重要的是断言,其他完全交给框架自动化。测试结果可以通过插件及时看到:

 

测试例子程序:

4.测试代码必须做以下几件事情

1)   准备测试所需的条件

2)   调用要测试的方法

3)   验证被测方法的行为和期望的行为是否一致

4)   完成后清理各种资源

5.使用NUnit框架,需要做这些工作

1)   使用using声明引用必要的NUnit类(并添加一个指向NUnitDll的引用)

2)   定义一个测试类,必须是Public的、包换一个public的没有参数的构造函数,并且在类定义上加上[TestFixture]attribute标记。(表明是测试类)

3)   在测试类中包含用[Test]attribute标记的方法。(表明是测试方法)

6.Nunit断言类介绍

Nunit一共有四个断言类,分别是Assert、StringAssert、FileAssert和DirectoryAssert,它们都在NUnit.Framework命名空间,其中Assert是常用,也是我们最熟悉的,而另外三个断言类,顾名思义,分别对应于字符串的断言、文件的断言和目录的断言,理论上,仅Assert类就可以完成所有条件的判断,然而,如果合理的运用后面的三个断言,将使代码更加简洁、美观,也更加便于理解和维护。

一:Assert

对于我们通常的单元测试代码,Assert类中的静态方法就可以完成,其中大家最熟悉的应该是Assert. AreEqual()方法,其实,除了AreEqual,NUnit还提供了众多的比较方式,满足不同的代码要求。

1:相等/不相等判断

AreEqual:判断两个值相等

AreNotEqual:判断两个值不相等

示例:

Int a=1 
Assert.AreEqual(a,1);//通过判断 
Assert.AreEqual(a,2);//不能通过判断
注意,除了AreEqual/ AreNotEqual,下面几乎所有的方法都支持多态(多种数据类型),以及多参数,除了经常使用的Assert .AreEqual(int a,int b),还有一种典型的参数形式:               Assert.AreEqual(int a,int b,string message);
或者 Assert.XXX(int a,int b,string message);

其中第三个参数是条件不通过时输出的信息,以便于错误的定位

对于double和decimal类型,这两个方法还支持比较时的浮点误差

例如以下代码:

double a = 1.1; 
double b = 1.11; 
double c = 0.1; 
Assert.AreEqual(a, b, c);//通过判断 
c=0.01; 
Assert.AreEqual(a, b, c);//不能通过判断

2:类的判断

AreSame:判断两个对象相等

AreNotSame:判断两个对象不相等

Contains:判断是否是否某对象

示例:

List<string> list = new List<string>(); 
list.Add("a"); 
list.Add("b"); 
Assert.Contains("a", list); //通过判断 
Assert.Contains("aa", list); //不能通过判断

3:条件判断

IsTrue/True:判断条件为真

IsFalse/False:判断条件为假

IsNull/Null:判断是否为空

IsNotNull/NotNull:判断不为空

IsNaN:判断数值是Nan

IsEmpty/IsNotEmpty:判断字符串是否为空/不为空

IsEmpty/IsNotEmpty:判断集合是否为空/不为空

其中True,IsTrue类似的都有两个版本,其中IsTrue是用来兼容以前的版本的。

示例:

int a=100; 
Assert.IsTrue(a==100);//通过判断 
Assert.True(a==100);//通过判断

4:比较判断

Greater:大于

GreaterOrEqual:大于等于

Less:小于

LessOrEqual:小于大于

示例:

int a = 100; 
Assert.Greater(a, 99); 
Assert.GreaterOrEqual(a, 100);

5:类型判断

IsInstanceOfType/IsNotInstanceOfType:是/不是某个类的实例(从2.5之后支持泛型)

public class Person 
{ 
    public string name {set;get;} 
} 
 
Person p = new Person(); 
p.name = "a"; 
Assert.IsInstanceOf(typeof(Person), p);//通过判断 
Assert.IsInstanceOf(typeof(System.String), p);//不能通过判断

6:异常判断

Throws:应该抛出某类型的异常

DoesNotThrow:不应该抛出某类型的异常

7:其它常用的方法

Pass:强行让测试通过

Fail:强行让测试失败

Ignore:忽略该测试方法

Inconclusive:未验证该测试

Assert.Pass();

Assert.Pass(string message );

Assert.Pass(string message, object[] parms );

 

Assert.Fail();

Assert.Fail(string message );

Assert.Fail(string message, object[] parms );

 

Assert.Ignore();

Assert.Ignore(string message );

Assert.Ignore(string message, object[] parms );

 

Assert.Inconclusive();

Assert.Inconclusive(string message );

Assert.Inconclusive(string message, object[] parms );

这些方法让我们能直接控制测试的进程,其中Assert.Pass 让你直接通过测试来结束这次的测试:这会导致一个异常产生,是高效的让测试返回的方法。除此之外,你也可以提供一个消息,让我们可以直观看出原因。

Assert.Fail 可以让测试失败为结果来结束本次测试。

Assert.Ignore 让我们有能力动态的忽略某个测试或条件。它可以在test,setup,fixture setup方法中调用。建议只在单独的条件内调用,因为Nunit的分类机制提供更广泛的如包含、排除等,你可以更简单的选择让测试运行在不同的场合或者程序集。

Assert.Inconclusive 的作用是指出现有条件下,测试不能完成:因为它不能证明断言是成功或失败。

这几个方法一样,即使把它放到一个独立的方法中,然后再调用这个方法,它仍然会被激发来结束测试。

二:StringAssert

StringAssert用于String类型的断言判断:

StringAssert.Contains:是否包含子串

StringAssert.StartsWith:是否以某子串开头

StringAssert.EndsWith:是否以某子串结尾

StringAssert.AreEqualIgnoringCase:两个字符串是否在不区分大小写时相等

StringAssert.IsMatch:是否匹配,(使用正则表达式进行字符串比较)

示例:

string s1 = "abc"; 
 
StringAssert.Contains("b", s1); 
StringAssert.StartsWith("a", s1); 
StringAssert.EndsWith("c", s1); 
 
string s2 = "aBc"; 
StringAssert.AreEqualIgnoringCase(s1, s2); 
 
StringAssert.IsMatch("[a|book]", "123");

三:CollectionAssert

AllItemsAreInstancesOfType:集合中的各项是否是某某类型的实例

AllItemsAreNotNull:集合中的各项均不为空

AllItemsAreUnique:集合中的各项唯一

AreEqual:两个集合相等

AreEquivalent:两个集合相当

AreNotEqual:两个集合不相等

AreNotEquivalent:两个集合不相当

DoesNotContain:集合中不包含某对象

IsSubsetOf:一个集合是另外一个集合的子集

IsNotSubsetOf:一个集合不是另外一个集合的子集

IsEmpty:集合为空

IsNotEmpty:集合不为空

IsOrdered:集合的各项已经排序

 示例:

List<int> a = new List<int>(); 
List<int> b = new List<int>(); 
 
CollectionAssert.IsEmpty(a); 
 
for (int i = 1; i <= 10; i++) 
{ 
    a.Add(i); 
    b.Add(i); 
} 
 
CollectionAssert.AreEqual(a, b); 
CollectionAssert.IsOrdered(a); 
 
b.Remove(1); 
CollectionAssert.IsSubsetOf(b, a); 
 
CollectionAssert.AreEqual(a, b);

CollectionAssert类提供的这些方法主要用于检查集合或者集合内容或者来比较两个集合。

AreEqual 的重载方法要求两个集合的对应元素要相等,而 AreEquivalent 要求两个集合的内容相等,而不要求顺序。两者元素的比较都应用默认的相等比较。

四:FileAssert

FileAssert用于文件的比较判断:

AreEqual:判断两个值相等

AreNotEqual:判断两个值不相等

文件的比较,用来比较两个文件,可以Streams,FileInfos 或者路径的形式。

FileAssert.AreEqual( Stream expected, Stream actual );

FileAssert.AreNotEqual( Stream expected, Stream actual );

五:DirectoryAssert

这个类提供的方法让我们可以对系统的文件目录进行断言,可以以DirectoryInfos 或者路径 的形式。

AreEqual:判断两个值相等

AreNotEqual:判断两个值不相等

用来比较两个目录是否相等:相同的全名,属性Attributes, 创建时间CreationTime 和最后访问时间LastAccessTime. 两个包含相等文件的不同目录是不会被认为相同的。

DirectoryAssert.AreEqual( DirectoryInfo expected, DirectoryInfo actual );

DirectoryAssert.AreNotEqual( DirectoryInfo expected, DirectoryInfo actual );

IsEmpty:集合为空

IsNotEmpty:集合不为空

这些被用来测试目录是否为空。

DirectoryAssert.IsEmpty(DirectoryInfo directory);

DirectoryAssert.IsNotEmpty(DirectoryInfo directory);

IsWithin:是子目录

IsNotWithin:不是子目录

DirectoryAssert.IsWithin(DirectoryInfo expected,DirectoryInfo actual);

DirectoryAssert.IsNotWithin(DirectoryInfo expected,DirectoryInfo actual);

这些方法用来测试第二个目录是不是第一个目录的子(孙子等)目录。

7.Nunit属性介绍

Nunit属性可分为四大类,分别是控制类、数值类、平台类和其他类。

一:控制类

这些Attribute控制测试类或者测试方法,而且成对出现,功能基本相比,了解一个就自然了解另外一个。

[TestFixture]/[Explicit]:声明一个类是测试类/声明一个类只有在UI界面被选中时才执行

[TestFixtureSetup]/[TestFixtureTearDown]:在测试开始前/结束后要执行的操作,一个TestFixture只能有一个TestFixtureSetup或TestFixtureTearDown。

[Test]/[Ignore]:是测试方法/忽略掉,测试时不执行

[Setup]/[TearDown]:每个测试方法在执行前/结束后要执行的操作

二:数值类

数值类Attribue是NUnit的一大亮点,善于使用这些Attribue,将使测试方法变得清晰、简洁、可读性强、可扩展性强,有利于进行数据驱动下的测试。

为了说明下面的例子,使用下面的方法,也就是打印输出输入值及其数据类型,这样便于把执行的结果通过打印的方式在NUnit中输出

Console.WriteLine(t + ", its type:" + typeof(T));

[Values]以参数的形式传入一组值,NUnit会把这组值分解成相应数量的子测试。

例如下面的代码,分别在[Values]中传入三组共8个值,在执行时便一共会分解成8个子测试方法,例如

TestPrintInt([Values(1, 2, 3)] int x)会被分解为 
TestPrintInt(1) 
TestPrintInt(2) 
TestPrintInt(3)三组

如下图,当选中ValueDemo这个节点,然后点击Run按钮后,会看到一共测试了8个测试用例。

 

[ValueSource]以数据源(Object类型)作为参数,如下图,左边是源程序,右边是编译后生成的子测试

 

[Combinatorial]两组测试数据进行组合,下面的代码会生成12个子测试,如图

 

[Sequential]:和上面[Combinatorial]的类似,不同之处见下图

 

[Range]:生成某个数据范围内的子测试,如

TestPrintColsole([Range(0.2, 0.6, 0.2)] double d):生成0.2,0.4,0.6三个子测试用例 
TestPrintColsole([Values(1, 2, 3, 4, 5, 6)] int x, [Range(0.2, 0.6, 0.2)] double d):两个Attribuee结合,共生成18个子测试用例

 [Random]:在某个范围内随机生成测试用例,

TestPrintColsole([Random(-1.0, 1.0, 5)] double d):-1到1之间生成5个测试用例 
TestPrintColsole([Values(1, 2, 3, 4, 5, 6)] int x, [Random(-1.0, 1.0, 5)] double d) :共生成30个子测试用例

三:平台类

[Culture]:当Culture满足某条件下,才执行测试,如:

[Culture("fr-FR")]:仅当在法文Culture时,执行测试 
[Culture(Exclude = "en,de")]:非英文和德文Culture时,才执行测试

[Platform]:当Platform满足某条件下,才执行测试,如:

[Platform("NET-4.0")]:仅当framework版本是4.0时,才执行测试

下面是NUnit官网声称的,其支持的平台类型:

Win

Win32

Win32S

Windows

Win32NT

WinCE

Win95

Win98

WinMe

NT3

NT4

NT5

NT6

Win2K

WinXP

Win2003Server

Vista

Win2008Server

Win2008ServerR2

Windows7

Unix

Linux

Net

Net-1.0

Net-1.1

Net-2.0

Net-3.0

Net-3.5

Net-4.0

NetCF

SSCLI

Rotor

Mono

Mono-1.0

Mono-2.0

Mono-3.0

Mono-3.5

 

 

 

四:其它类

[Category]:将测试方法分组,NUnit会取出所有的组名,列在Categories中,点击它可以看到。这提供了另外一种测试用例的运行方法,用户可以在些选择某一类的用例进行测试,而不是只能以点击分单位

[ExpectedException]:运行时抛出的异常才能预期的行为。

如果没有这个Attribute,要测试某些异常时,只能使用Try Catch的方式,而下面的代码则更为简洁:

[Test] 
[ExpectedException(typeof(System.DivideByZeroException))]//发生DivideByZeroException时,测试通过 
public void Test1() 
{ 
    int a = 0; 
    int b = 1 / a; 
    PrintConsole<object>.Print(b); 
} 
 
[Test] 
[ExpectedException( "System.DivideByZeroException")]//发生DivideByZeroException时,测试通过
public void Test2() 
{ 
    int a = 0; 
    int b = 1 / a; 
    PrintConsole<object>.Print(b); 
}

[Maxtime]/ [Timeout]: 测试用例的最大执行时间,前者超时时不取消测试,而后者会强行中断,用法如下

[Test, Maxtime(2000)] 
[Test, Timeout(2000)]

[Repeat]: 重复执行多少次,用法如下

[Test, Repeat(100)]

[RequiresMTA]/ [RequiresSTA]/ [RequiresThread]:测试用例必须的在多线程、单线程、独立的线程状态下运行。

8.对比MS Test与NUnit Test框架

一、简介

MS Test框架是Visual Studio自带的测试框架,可以通过新建一个Unit Test Project工程,也可以建一个Class Libary,然后添加对Microsoft.VisualStudio.QualityTools.UnitTestFramework.dll的引用。然后就是创建测试用例,进行测试即可。

NUnit Test框架是一个xUnit家族种的第4个主打产品,完全由C#语言来编写,支持所有的.Net语言。

使用NUnit框架,我们需要下载安装包,安装后使用独立客户端进行使用。使用方法与MS Test类似,新建一个Class Libary,然后添加对nunit.framework.dll的引用,最后创建测试用例,在客户端中进行测试。

二、特性标签对比

所有的测试框架都共享相同的核心特性:Test Declaration, Test Execution, and Assertions.

在.Net中一般使用特性标签来添加额外的信息,下面就是MS Test和NUnit在特性标签上不同的地方。

MS Test Attribute

NUnit Attribute

用途

[TestClass]

[TestFixture]

定义一个测试类,里面可以包含很多测试函数和初始化、销毁函数(以下所有标签和其他断言)。

[TestMethod]

[Test]

定义一个独立的测试函数。

[ClassInitialize]

[TestFixtureSetUp]

定义一个测试类初始化函数,每当运行测试类中的一个或多个测试函数时,这个函数将会在测试函数被调用前被调用一次(在第一个测试函数运行前会被调用)。

[ClassCleanup]

[TestFixtureTearDown]

定义一个测试类销毁函数,每当测试类中的选中的测试函数全部运行结束后运行(在最后一个测试函数运行结束后运行)。

[TestInitialize]

[SetUp]

定义测试函数初始化函数,每个测试函数运行前都会被调用一次。

[TestCleanup]

[TearDown]

定义测试函数销毁函数,每个测试函数执行完后都会被调用一次。

[AssemblyInitialize]

--

定义测试Assembly初始化函数,每当这个Assembly中的有测试函数被运行前,会被调用一次(在Assembly中第一个测试函数运行前会被调用)。

[AssemblyCleanup]

--

定义测试Assembly销毁函数,当Assembly中所有测试函数运行结束后,运行一次。(在Assembly中所有测试函数运行结束后被调用)

[DescriptionAttribute]

[Category]

定义标识分组。

三、同时支持NUnit和MSTest

通过上面的介绍,我们可以看到这两个框架只是标签的写法不一样,它们的具体功能都是一样的。

所以可以通过宏判断来同时支持这两个框架。

1、添加两种框架的DLL

     MSTest: Microsoft.VisualStudio.QualityTools.UnitTestFramework.dll

     NUnit   : nunit.framework.dll

2、在测试前添加以下代码:

#if !NUNIT 
using Microsoft.VisualStudio.TestTools.UnitTesting; 
using Category = Microsoft.VisualStudio.TestTools.UnitTesting.DescriptionAttribute; 
#else 
using NUnit.Framework; 
using TestClass = NUnit.Framework.TestFixtureAttribute; 
using TestMethod = NUnit.Framework.TestAttribute; 
using TestInitialize = NUnit.Framework.SetUpAttribute; 
using TestCleanup = NUnit.Framework.TearDownAttribute; 
using TestContext = System.Object; 
using ClassCleanup = NUnit.Framework.TestFixtureTearDownAttribute; 
using ClassInitialize = NUnit.Framework.TestFixtureSetUpAttribute; 
#endif

四、断言对比

同上,大部分都是一样的,有些地方不太一样而已。

MS Test Assert

NUnit Assert

用途

AreEqual

 

验证值相等

AreNotEqual

 

验证值不相等

AreSame

 

验证引用相等

AreNotSame

 

验证引用不相等

Inconclusive

 

暗示条件还未被验证

IsTrue

 

验证条件为真

IsFalse

 

验证条件为假

IsInstanceOfType

Assert.IsInstanceOf<>

验证实例匹配类型

IsNotInstanceOfType

Assert.IsNotInstanceOf<>

验证实例不匹配类型

IsNotNull

 

验证条件为NULL

IsNull

 

验证条件不为NULL

Fail

 

验证失败

另外还有一些是NUnit中的,但是MS Test框架中是没有的:

  • Assert.IsNaN
  • Assert.IsEmpty
  • Assert.IsNotEmpty
  • Assert.Greater
  • Assert.GreaterOrEqual
  • Assert.Less
  • Assert.LessOrEqual
  • Assert.IsAssignableFrom
  • Assert.IsNotAssignableFrom
  • Assert.Igore
  • CollectionAssert.IsEmpty
  • CollectionAssert.IsNotEmpty
  • StringAssert.AreEqualIgnoringCase
  • StringAssert.IsMatch
  • FileAssert.AreEqual
  • FileAssert.AreNotEqual

9.测试技巧

  有六个值得测试的具体部位,它们能够提高你的测试水平。这六个方面可以统称为Right_BICEP:
  Right : 结果是否正确(Right)
  对于测试而言,首要的也是最明显的任务就是查看所期望的结果是否正确-验证结果。这里的结果是指确认代码所做的和你的期望是一致的。
   B: 边界(boundary)条件是否正确
  边界条件包括许多内容,将在下一节(2.3.边界条件)中集中描述。
  I: 是否可以检查反向(inverse)关联
  对一些方法,可以用反向的逻辑关系来验证它们。例如,为了检查某条记录是否成功的插入了数据库,可以通过查询这条记录来验证,等等。
  值得注意的是,当同时编写原方法和它的反向测试时,一些BUG可能会被两者中都有的错误所掩饰。在可能的情况下,应该用不同的原理来实现反向测试。
  C: 是否可以使用其它方法来跨检查(cross-check)结果
   E: 错误条件(error condition)是否可以重现
  应该能够通过强制引发真实世界中的错误-网络断开、程序崩溃等-来测试代码如何处理这些问题。简单的无效参数之类的错误会很简单,但要模拟复杂的错误就需要一些特殊的技术。在下面的文字中,将讨论使用Mock技术来解决如何强制产生错误的问题。
  P: 性能(performance)方面是否满足条件
  这里的性能特征并不是指程序的性能本身,而是指性能的那种“随着规模增大,问题越来越复杂”的趋势。我们应该使用一个性能特性的快速回归测试,避免出现某些修改使得程序变得很慢却无法对其进行定位的情况。

10.网站推荐

1. 对比MS Test与NUnit Test框架

http://www.cnblogs.com/ColdJokeLife/p/3158812.html

2. 浅谈NUnit单元测试方法小总结

http://www.ltesting.net/ceshi/ceshijishu/dycs/2011/1230/203848.html

3.NUnit之Assertion详解

http://www.51testing.com/?uid-116228-action-viewspace-itemid-807305

posted @ 2013-10-14 20:59 Jara 阅读(...) 评论(...) 编辑 收藏