单元测试方法论(clean code阅读笔记之八)

单元测试

注:正文中的引用是直接引用作者的话,两条横线中间的段落的是我自己的观点,其他大约都可以算是笔记了。


对于很多人来说,所谓「单元测试」只是在我们艰难地写完一些类或者方法——甚至一个工程——后,写下的一些「专用代码」来测试它们,而且这些代码往往都是一些简单的驱动程序来帮助我们和我们刚写好的程序进行交互。比如某些人写的单元测试会是这样的,单元测试跑起来后会监听键盘,然后我们键入某一个字母或者直接点击回车来执行某种测试步骤。

随着「敏捷开发」和「测试驱动开发」的流行,渐渐地人们开始转变写单元测试的方法,现在的很多人会把单元测试当做系统正常运行的保证——首先这些单元测试要保证覆盖了程序的每一个角落,然后每次这些单元测试通过,就表示我们的程序完全没有问题了。

但是随着越来越多的人急于把单元测试作为规范而去实践它,许多开发者忽略了一些非常微妙但是十分重要的点,而这些点才是你写出优雅的测试的重要因素。

测试驱动开发的3大定律

在做软件开发的时候:

  1. 在编写正式代码之前,必须写出其相对应的单元测试;
    这就是说一定要先写出一个单元测试(因为还没有编写正式代码,所以它肯定会失败),再编写正式代码。
  2. 只要一个单元测试失败了,就不要再编写任何更多的单元测试了;
    而是要去编写相应的的可以使之通过的正是代码,编译失败也算;
  3. 只要正式代码可以使单元测试通过,就不要再编写更多的正式代码了;
    正式代码需要满足的唯一目标就是通过单元测试,只要通过单元测试,就表示我们此部分的代码已经写完了。

详细解读可以参考Bob大叔的博客

保持单元测试的整洁

有些人坚信「有总比没有强」,他们并没有像对待正式代码那样对待测试代码,对测试代码的要求就是「快」,往往写出来的代码却十分丑陋。比如变量并没有被良好得命名,测试代码中的方法又冗长又难懂,他们坚信测试代码不需要花那么多心思去设计,只要它们能工作就足够好了。

但是,质量差的测试代码等同于没有测试(或者甚至比没有测试还要糟糕)。因为我们的测试代码往往需要随着正式代码的变化而变化,如果你的测试代码写的很烂,那么你将来就要花更多的时间来修改它们,慢慢的这些写得很烂的测试代码将会成为沉重的负担。慢慢的越来越多的人开始抱怨测试代码成拖累了开发进度,进而最后放弃这些测试。

结论就是:测试代码和正式代码同样重要,我们应该像对待正式代码那样对待测试代码。

测试可以带来各种好处

如前面几章中提到的,有了覆盖所有正式代码的单元测试,你就可以放心大胆地对系统进行修改。这就会带来许多好处:灵活性、可维护性和可重用性
而丑陋的测试代码会让你的代码丧失这些好处。

什么是整洁的单元测试

好的测试有3个要素:可读性、可读性、可读性

这里有一个FitNesse中的单元测试的例子,如代码8-1中所示的单元测试,其中包含了大量的细节实现,可读性极差,读者想要读懂这些测试是干什么的时候就会发现自己被淹没在了无止境的细节中。

//代码8-1
public void testGetPageHieratchyAsXml() throws Exception{
    crawler.addPage(root, PathParser.parse("PageOne"));
    crawler.addPage(root, PathParser.parse("PageOne.ChildOne"));
    crawler.addPage(root, PathParser.parse("PageTwo"));

    request.setResource("root");
    request.addInput("type", "pages");
    Responder responder = new SerializedPageResponder();
    SimpleResponse response =
        (SimpleResponse) responder.makeResponse(
            new FitNesseContext(root), request);
    String xml = response.getContent();

    assertEquals("text/xml", response.getContentType());
    assertSubString("<name>PageOne</name>", xml);
    assertSubString("<name>PageTwo</name>", xml);
    assertSubString("<name>ChildOne</name>", xml);
}

public void testGetPageHieratchyAsXmlDoesntContainSymbolicLinks()throws Exception
{
    WikiPage pageOne = crawler.addPage(root, PathParser.parse("PageOne"));
    crawler.addPage(root, PathParser.parse("PageOne.ChildOne"));
    crawler.addPage(root, PathParser.parse("PageTwo"));

    PageData data = pageOne.getData();
    WikiPageProperties properties = data.getProperties();
    WikiPageProperty symLinks = properties.set(SymbolicPage.PROPERTY_NAME);
    symLinks.set("SymPage", "PageTwo");
    pageOne.commit(data);

    request.setResource("root");
    request.addInput("type", "pages");
    Responder responder = new SerializedPageResponder();
    SimpleResponse response =
        (SimpleResponse) responder.makeResponse(
            new FitNesseContext(root), request);
    String xml = response.getContent();

    assertEquals("text/xml", response.getContentType());
    assertSubString("<name>PageOne</name>", xml);
    assertSubString("<name>PageTwo</name>", xml);
    assertSubString("<name>ChildOne</name>", xml);
    assertNotSubString("SymPage", xml);
}

public void testGetDataAsHtml() throws Exception{
    crawler.addPage(root, PathParser.parse("TestPageOne"), "test page");

    request.setResource("TestPageOne");
    request.addInput("type", "data");
    Responder responder = new SerializedPageResponder();
    SimpleResponse response =
       (SimpleResponse) responder.makeResponse(
            new FitNesseContext(root), request);
    String xml = response.getContent();

    assertEquals("text/xml", response.getContentType());
    assertSubString("test page", xml);
    assertSubString("<Test", xml);
}

在进行一番重构后,它长这个样子代码8-2。

public void testGetPageHierarchyAsXml() throws Exception {
    makePages("PageOne", "PageOne.ChildOne", "PageTwo");

    submitRequest("root", "type:pages");

    assertResponseIsXML();assertResponseContains(
        "<name>PageOne</name>", "<name>PageTwo</name>", "<name>ChildOne</name>"
    );
}

public void testSymbolicLinksAreNotInXmlPageHierarchy() throws Exception {
    WikiPage page = makePage("PageOne");
    makePages("PageOne.ChildOne", "PageTwo");

    addLinkTo(page, "PageTwo", "SymPage");
    submitRequest("root", "type:pages");

    assertResponseIsXML();
    assertResponseContains(
        "<name>PageOne</name>", "<name>PageTwo</name>", "<name>ChildOne</name>"
    );
    assertResponseDoesNotContain("SymPage");
}

public void testGetDataAsXml() throws Exception {
    makePageWithContent("TestPageOne", "test page");

    submitRequest("TestPageOne", "type:data");

    assertResponseIsXML();
    assertResponseContains("test page", "<Test");
}

就像代码8-2中所示那样,测试代码应该是一个3段式:

  1. 准备测试数据。
  2. 对测试数据进行操作。
  3. 进行判定。

领域专用语言

我们在编写测试的时候,最好不要直接使用正式代码中的API,而是应该在它们的基础上进行包装,从而创建专用于测试的API,就像代码8-2中所示。

双重标准

对测试代码的要求和正式代码并不需要完全相同,可以使用双重标准来对待它们。测试代码仍然必须要简洁而可读,但是不需要和正式代码那样拥有很高的运行效率(但同时测试也不能效率太低,跑一次测试要等很久才知道结果)。毕竟这些代码是跑在测试环境下,和正式环境有着千差万别。

效率并不是测试代码的首要目标,所以在编写测试代码时,可以牺牲一些我们在平时养成的良好习惯,而追求更好的可读性。比如在拼接字符串时(在测试代码中)就可以不使用StringBuilder,而直接使用+

一个测试只做一次断言

如果可能,请遵守这个规则,但它不应该成为你为了遵守它,写了一大堆冗余的代码。

在可读性良好的前提下,完全可以打破这条规则。

一个测试只有唯一的概念

相比上一条,更合理的规则应该是,一个测试不应该包含多个概念的测试。比如代码8-3中就明显违背了此原则,这个测试要做的事情太多了,超出了一个概念的范围,应该将它分拆成3个测试。

//代码8-3
/*** Miscellaneous tests for the addMonths() method.*/
public void testAddMonths() {
    SerialDate d1 = SerialDate.createInstance(31, 5, 2004);

    SerialDate d2 = SerialDate.addMonths(1, d1);
    assertEquals(30, d2.getDayOfMonth());
    assertEquals(6, d2.getMonth());
    assertEquals(2004, d2.getYYYY());

    SerialDate d3 = SerialDate.addMonths(2, d1);
    assertEquals(31, d3.getDayOfMonth());
    assertEquals(7, d3.getMonth());
    assertEquals(2004, d3.getYYYY());

    SerialDate d4 = SerialDate.addMonths(1, SerialDate.addMonths(1, d1));
    assertEquals(30, d4.getDayOfMonth());
    assertEquals(7, d4.getMonth());
    assertEquals(2004, d4.getYYYY());
}

TDD中的「FIRST」原则

Fast:测试不能每跑一次都要耗费大量的时间。
Independent: 测试与测试之间不应该存在依赖关系。
Repeatable: 测试应该在任何环境下都能运行,不论是生产环境、测试环境,或者是在家里的笔记本电脑上。
Self-Validating 测试的结果应该是显而易见的,不应该是靠人去查看它的输出才能判断测试的成功与失败。
Timely: 测试需要在正式代码之前就写好。

如果你的测试代码写的很烂,那么你的正式代码也将最终变得很烂。所以请保持你的测试代码整洁。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 194,457评论 5 459
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 81,837评论 2 371
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 141,696评论 0 319
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,183评论 1 263
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 61,057评论 4 355
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,105评论 1 272
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,520评论 3 381
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,211评论 0 253
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,482评论 1 290
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,574评论 2 309
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,353评论 1 326
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,213评论 3 312
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,576评论 3 298
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 28,897评论 0 17
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,174评论 1 250
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 41,489评论 2 341
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 40,683评论 2 335

推荐阅读更多精彩内容

  • Android 自定义View的各种姿势1 Activity的显示之ViewRootImpl详解 Activity...
    passiontim阅读 171,050评论 25 707
  • -----转载----- 1、问:你在测试中发现了一个bug,但是开发经理认为这不是一个bug,你应该怎样解决? ...
    花开沉浮阅读 7,325评论 4 88
  • 文章来自:http://blog.csdn.net/mj813/article/details/52451355 ...
    好大一只鹏阅读 9,181评论 2 126
  • 发现 关注 消息 iOS 第三方库、插件、知名博客总结 作者大灰狼的小绵羊哥哥关注 2017.06.26 09:4...
    肇东周阅读 11,945评论 4 60
  • 相识 人的一生大概会遇到2000万人,相识的人不过几千人。况且我们绝大多数人一生或许都只是待在一两个城市。所以对我...
    识得故人阅读 683评论 9 9