51Testing软件测试论坛

 找回密码
 (注-册)加入51Testing

QQ登录

只需一步,快速开始

微信登录,快人一步

手机号码,快捷登录

查看: 2145|回复: 0
打印 上一主题 下一主题

[转贴] 编写综合的单元测试

[复制链接]
  • TA的每日心情
    无聊
    前天 09:05
  • 签到天数: 1050 天

    连续签到: 1 天

    [LV.10]测试总司令

    跳转到指定楼层
    1#
    发表于 2016-5-25 14:15:13 | 只看该作者 回帖奖励 |正序浏览 |阅读模式

    每个用例编写一到二个断言是单元测试最佳实践的常见内容。那如此认为的是极少和只展示一个单元测试的人。因此如果你采纳他们的建议,为一个很小的运算你都需要大量的单元测试去保证质量。这篇文章意图通过例子展示,一个测试用例多个断言是有必要和有价值的。

    Person这个对象在数据绑定场景中经常出现,我们来看下。

    测试FirstName

    第一个来测试FirstName这个属性的设置,开始如下:

    public void Person_FirstName_Set()
    {
    varperson = new Person("Adam", "Smith");
    person.FirstName = "Bob";
    Assert.AreEqual("Bob", person.FirstName);
    }

    接下来我们来测试FirstName的改变通知。

    public void Person_FirstName_Set_PropertyChanged()
    {
    var person = new Person("Adam", "Smith");
    var eventAssert = new Granite.Testing.PropertyChangedEventAssert(person);
    person.FirstName = "Bob";
    eventAssert.Expect("FirstName");
    }

    当我们执行这个测试时,会得到一个失败提示信息“期望的属性名‘FirstName’,但接收到的是’IsChanged’”。显然,设置FirstName的属性触发了“IsChanged”标记,我们需要把它考虑在内。因此我们把它加入:

    public void Person_FirstName_Set_PropertyChanged()
    {
    var person = new Person("Adam", "Smith");
    var eventAssert = new Granite.Testing.PropertyChangedEventAssert(person);
    person.FirstName = "Bob";
    eventAssert.SkipEvent();
    //this was IsChanged
    eventAssert.Expect("FirstName");
    }

    鉴于以上两个测试,我们考虑当FirstName被修改时还有其他什么属性会改变。查看API,IsChanged和FullName属性会变化。

    public void Person_FullName_Changed_By_Setting_FirstName()
    {
    var person = new Person("Adam", "Smith");
    person.FirstName = "Bob";
    Assert.AreEqual("Bob Smith", person.FullName);
    }

    public void Person_IsChanged_Changed_By_Setting_FirstName()
    {
    var person = new Person("Adam", "Smith");
    person.FirstName = "Bob";
    Assert.IsTrue(person.IsChanged);
    }

    当然,如果这些属性改变了,我们需要获取到属性改变通知:

    public void Person_IsChanged_Property_Change_Notification_By_Setting_FirstName()
    {
    var person = new Person("Adam", "Smith");
    var eventAssert = new PropertyChangedEventAssert(person);
    person.FirstName = "Bob";
    eventAssert.Expect("IsChanged");
    }

    public void Person_FullName_Property_Change_Notification_By_Setting_FirstName()
    {
    var person = new Person("Adam", "Smith");
    var eventAssert = new PropertyChangedEventAssert(person);
    person.FirstName = "Bob";
    eventAssert.SkipEvent();
    //this was IsChanged
    eventAssert.SkipEvent();
    //this was FirstName
    eventAssert.Expect("FullName");
    }

    接下来两个测试针对HasErrors这个属性和ErrorsChanged事件。

    public void Person_FirstName_Set_HasErrorsIsFalse()
    {
    var person = new Person("Adam", "Smith");
    person.FirstName = "Bob";
    Assert.IsFalse(person.HasErrors);
    }

    public void Person_FirstName_Set_ErrorsChanged_Did_Not_Fire()
    {
    var person = new Person("Adam", "Smith");
    var errorsChangedAssert = new ErrorsChangedEventAssert(person);
    person.FirstName = "Bob";
    errorsChangedAssert.ExpectNothing();
    }

    目前我们有8个测试了,这意味着当我们修改FirstName的属性值,我们要考虑会发生改变的每件事。但是这不算完。我们还需要确保没有别的会被意外改变。理论上说,这意味着更多的断言和相当数量的测试,但是,接下来我们采用取巧的方法,用ChangeAssert方法来替代HasErrors测试。

    public void Person_FirstName_Set_Nothing_Unexpected_Changed()
    {
    var person = new Person("Adam", "Smith");
    var changeAssert = new ChangeAssert(person);
    person.FirstName = "Bob";
    changeAssert.AssertOnlyChangesAre("FirstName", "FullName", "IsChanged");
    }

    ChangeAssert简单地通过映射获取对象的状态,因此,稍后你可以断言到除了你指出的几个具体属性其他的没变。

    恭喜,你完成了你的第一个测试用例。完成一个,还有很多很多等着。

    为什么说是“一个”测试用例?

    那8个测试只是完成了覆盖FirstName属性从“Adam”修改成“Bob”这一个场景,在其他的值没有在错误状态、LastName不为null或空的情况下。让我们看看测试用例的完整清单:

    将FirstName值设置为“Adam”

    将FirstName值设置为null

    将FirstName 设为空串

    在LastName值为null的情况下,执行case1-3

    在LastName 为空串的情况下,执行case1-3

    在FirstName值以null开头的情况下,执行case1-5

    在FirstName值以空串开头的情况下,执行case1-5

    目前我们看到了27个不同的场景。如果每个场景需要8个不同测试,仅仅为这一个属性,我们需要执行至多216个测试。根据这种思路,这是相当琐碎的一段代码。因此我们该怎么做呢?

    测试也有代码味道

    回看第一个测试用例的8个测试,它们都有同样的设置和运算。唯一的不同是我们写的断言。在业界这个被称为一个代码味道。事实上,根据维基百科所列的这里应该有两个代码味道:

    1.Duplicated code

    2.重复的代码

    3.Excessively long identifiers

    4.过长的标识符

    我们可以通过将断言合并到一个测试来轻松地消除这两个代码味道:

    public void Person_FirstName_Set()
    {
    var person = new Person("Adam", "Smith");
    var eventAssert = new PropertyChangedEventAssert(person);
    var errorsChangedAssert = new ErrorsChangedEventAssert(person);
    var changeAssert = new ChangeAssert(person);
    person.FirstName = "Bob";
    Assert.AreEqual("Bob", person.FirstName, "FirstName setter failed");
    Assert.AreEqual("Bob Smith", person.FullName, "FullName not updated with FirstName changed");
    Assert.IsTrue(person.IsChanged, "IsChanged flag was not set when FirstName changed");
    eventAssert.Expect("IsChanged");
    eventAssert.Expect("FirstName");
    eventAssert.Expect("FullName");
    errorsChangedAssert.ExpectNothing("Expected no ErrorsChanged events");
    changeAssert.AssertOnlyChangesAre("FirstName", "FullName", "IsChanged");
    }

    知道什么导致测试失败很重要,因此我们在断言里添加失败的信息提示。

    单元测试和代码重用

    回看那27个测试用例,我们可以断定设置FirstName为null或者空串应该也需求同样的测试。因此我们可以扩展成:

    public void Person_FirstName_Set_Empty()
    {
    Person_FirstName_Set_Invalid(String.Empty);
    }

    public void Person_FirstName_Set_Null()
    {
    Person_FirstName_Set_Invalid(null);
    }
    public void Person_FirstName_Set_Invalid(string firstName)
    {
    var person = new Person("Adam", "Smith");
    var eventAssert = new PropertyChangedEventAssert(person);
    var errorsChangedAssert = new ErrorsChangedEventAssert(person);
    var changeAssert = new ChangeAssert(person);
    Assert.IsFalse(person.IsChanged, "Test setup failed, IsChanged is not false");
    Assert.AreEqual("Adam", person.FirstName, "Test setup failed, FirstName is not Adam");
    Assert.AreEqual("Smith", person.LastName, "Test setup failed, LastName is not Smith");
    person.FirstName = firstName;
    Assert.AreEqual(firstName , person.FirstName, "FirstName setter failed");
    Assert.AreEqual("Smith", person.FullName, "FullName not updated with FirstName changed");
    Assert.IsTrue(person.IsChanged, "IsChanged flag was not set when FirstName changed");
    eventAssert.Expect("IsChanged");
    eventAssert.Expect("FirstName");
    eventAssert.Expect("FullName");
    Assert.IsTrue(person.HasErrors, "HasErrors should have remained false");
    errorsChangedAssert.ExpectCountEquals(1, "Expected an ErrorsChanged event");
    changeAssert.AssertOnlyChangesAre("FirstName", "FullName", "IsChanged", "HasErrors");
    }

    可以发现Person_FirstName_Set和Person_FirstName_Set_Invalid的差异很小,我们可以进一步试着通用化:

    public void Person_FirstName_Set_Valid()
    {
    Person_FirstName_Set("Bob", false);
    }

    public void Person_FirstName_Set_Empty()
    {
    Person_FirstName_Set(String.Empty, true);
    }

    public void Person_FirstName_Set_Null()
    {
    Person_FirstName_Set(null, true);
    }
    public void Person_FirstName_Set(string firstName, bool shouldHaveErrors)
    {
    var person = new Person("Adam", "Smith");
    var eventAssert = new PropertyChangedEventAssert(person);
    var errorsChangedAssert = new ErrorsChangedEventAssert(person);
    var changeAssert = new ChangeAssert(person);
    Assert.IsFalse(person.IsChanged, "Test setup failed, IsChanged is not false");
    Assert.AreEqual("Adam", person.FirstName, "Test setup failed, FirstName is not Adam");
    Assert.AreEqual("Smith", person.LastName, "Test setup failed, LastName is not Smith");
    person.FirstName = firstName;
    Assert.AreEqual(firstName, person.FirstName, "FirstName setter failed");
    Assert.AreEqual((firstName + " Smith").Trim(), person.FullName, "FullName not updated with FirstName changed");
    Assert.AreEqual(true, person.IsChanged, "IsChanged flag was not set when FirstName changed");
    eventAssert.Expect("IsChanged");
    eventAssert.Expect("FirstName");
    eventAssert.Expect("FullName");
    if (shouldHaveErrors)
    {
    Assert.IsTrue(person.HasErrors, "HasErrors should have remained false");
    errorsChangedAssert.ExpectCountEquals(1, "Expected an ErrorsChanged event");
    changeAssert.AssertOnlyChangesAre("FirstName", "FullName", "IsChanged", "HasErrors");
    }
    else
    {
    errorsChangedAssert.ExpectNothing("Expected no ErrorsChanged events");
    changeAssert.AssertOnlyChangesAre("FirstName", "FullName", "IsChanged");
    }
    }

    在测试代码变得令人迷惑之前,我们可以把它通用化什么程度,这里绝对有个限制。但是一个有意义的测试名称,并给每个断言配一个好的描述可以让你的测试更加容易让人理解。

    控制变量

    目前所有的断言都只考虑到了测试用例的输出。他们假设每个Person对象初始状态已知,然后从此出发进行其他操作。但是如果我们想让测试更具科学性,必须确保我们能控制变量。或者换句话说,我们需要保证,一切在掌握之中。

    请看下面一组断言:

    Assert.IsFalse(person.HasErrors, "Test setup failed, HasErrors is not false");
    Assert.IsFalse(person.IsChanged, "Test setup failed, IsChanged is not false");
    Assert.AreEqual("Adam", person.FirstName, "Test setup failed, FirstName is not Adam");
    Assert.AreEqual("Smith", person.LastName, "Test setup failed, LastName is not Smith");

    由于我们不想在每个测试的开始重复这些断言,我们可以选择把他们移到一个工厂方法中,这样我们可以保证总是拿到一个干净的对象。这个同样适用于重用这些设置去测试其他属性的测试用例。

    public void Person_FirstName_Set()
    {
    var person = GetAdamSmith();
    ...

    表格式的测试

    之所以走到这一步,是因为“测试方法”的数量跟测试的完善程度没有关系。它们只是组织和执行测试用例一种比较方便的方式。

    另一个组织大量测试用例的方法是表格驱动测试法。不能执行单个测试,但是仅用一行代码就可以增加新的测试用例。表格式测试里的表格可以来源于XML的文件,数据库表,写死在数组里或者只是使用同一个函数用不同的值反复调用。一些框架如MBTest甚至可以让你用属性给出测试用例,但是为了让例子轻便,我们还是坚持保持最低的共同部分。

    public void Person_FullName_Tests()
    {
    Person_FullName_Test("Bob", "Jones", "Bob Jones");
    Person_FullName_Test("Bob ", "Jones", "Bob Jones");
    Person_FullName_Test(" Bob", "Jones", "Bob Jones");
    Person_FullName_Test("Bob", " Jones", "Bob Jones");
    Person_FullName_Test("Bob", "Jones ", "Bob Jones");
    Person_FullName_Test(null, "Jones", "Jones");
    Person_FullName_Test(string.Empty, "Jones", "Jones");
    Person_FullName_Test(" ", "Jones", "Jones");
    Person_FullName_Test("Bob", "", "Bob");
    Person_FullName_Test("Bob", null, "Bob");
    Person_FullName_Test("Bob", string.Empty, "Bob");
    Person_FullName_Test("Bob", " ", "Bob");
    }
    private void Person_FullName_Test(string firstName, string lastName, string expectedFullName)
    {
    var person = GetAdamSmith();
    person.FirstName = firstName;
    person.LastName = lastName;
    Assert.AreEqual(expectedFullName, person.FullName,
    string.Format("Incorrect full name when first name is '{0}' and last name is '{1}'"
    firstName ?? "", lastName ?? ""));
    }

    在运用这个技巧时,要使用带参数的错误信息,这很重要。如果不加,你会发现在定位哪些参数组合不对时,还得一步一步调试代码。

    结论

    在为任何变量编写单元测试时,最好尝试最大化以下几个因素:

    1.有意义的单位工作量测试覆盖率

    2.面对变动的代码基线时,保证可维护性

    3.测试套件的性能

    4.明确说明测试什么以及为什么

    鉴于这些因素往往会冲突,谨慎地运用单个用例多重断言可提升上述四个方面,具体做法是: + 减少需要编写的样板代码量 + 减少因API更改而需要更新的样板代码量 + 减少每个断言需要执行的样板代码数量 + 将某一操作的所有断言,用文档记录在同一个地方。

    转自:http://www.uml.org.cn/Test/201508194.asp


    分享到:  QQ好友和群QQ好友和群 QQ空间QQ空间 腾讯微博腾讯微博 腾讯朋友腾讯朋友
    收藏收藏
    回复

    使用道具 举报

    本版积分规则

    关闭

    站长推荐上一条 /1 下一条

    小黑屋|手机版|Archiver|51Testing软件测试网 ( 沪ICP备05003035号 关于我们

    GMT+8, 2024-11-23 03:29 , Processed in 0.067910 second(s), 23 queries .

    Powered by Discuz! X3.2

    © 2001-2024 Comsenz Inc.

    快速回复 返回顶部 返回列表