Blog

Blog

PHODAL

如何用测试驱动出100%测试覆盖率的代码

本文以DDM为例,简单地介绍一下如何用测试驱动开发(TDD, Test-Driven Development)的方法来驱动出这个函数库。

DDM简介

DDM是一个简洁的前端领域模型库,如我在《DDM: 一个简洁的前端领域模型库》一文中所说,它是我对于DDD在前端领域中使用的一个探索。

简单地来说,这个库就是对一个数据模型的操作——增、删 、改,然后生成另外一个数据模型。

DDM

如以Blog模型,删除Author,我们就可以得到一个新的模型。而实现上是因为我们需要RSS模型,我们才需要对原有的模型进行修改。

预先式设计

如果你对TDD有点了解的话,那么你可能会预先式设计有点疑问。

等等,什么是测试驱动开发?

测试驱动开发,英文全称Test-Driven Development,简称TDD,是一种不同于传统软件开发流程的新型的开发方法。它要求在编写某个功能的代码之前先编写测试代码,然后只编写使测试通过的功能代码,通过测试来推动整个开发的进行。这有助于编写简洁可用和高质量的代码,并加速开发过程。

流程大概就是这样的,先写个测试 -> 然后运行测试,测试失败 -> 让测试通过 -> 重构。 enter image description here

换句简单的话来说,就是 红 -> 绿 -> 重构。

在DDM项目里,就是一个比较适合TDD的场景。我们知道我们所有的功能,我们也知道我们需要对什么内容进行测试,并且它很简单。因为这个场景下,我们已经知道了我们所需要的功能,所以我们就可以直接设计主要的函数:

export class DDM {
  constructor() {}
  from() {};
  get(array) {};
  to() {};
  handle() {};
  add() {};
  remove(field) {};
}

上面的就是我们的需要函数,不过在后来因为需要就添加了replacereplaceWithHandle方法。

然后,我们就可以编写我们的第一个测试了。

第一个驱动开发的测试

我们的第一个测试,比较简单,但是也比较麻烦——我们需要构建出基本的轮廓。我们的第一个测试就是要测试我们可以从原来的对象中取出title的值:

let ddm = new DDM();
var originObject = {
  title: 'hello',
  blog: 'fdsf asdf fadsf ',
  author: 'phodal'
};

var newObject = {};
ddm
  .get(['title'])
  .from(originObject)
  .to(newObject);

expect(newObject.title).toBe("hello");

对应的,为了实现这个需要基本的功能,我们就可以写一个简单的return来通过测试。

  from(originObject) {
    return this;
  };

  get(array) {
    return this;
  };

  to(newObject) {
    newObject.title = 'hello';
    return this;
  };

但是这个功能在我们写下一个测试的时候,它就会出错。

ddm
  .get(['title', 'phodal'])
  .from(originObject)
  .to(newObject);

expect(newObject.title).toBe("hello");
expect(newObject.author).toBe("phodal");

但是这也是我们实现功能要做的一步,下一步我们就可以实现真正的功能:

  • 在from函数里,复制originObject
  • 在get函数里,获取新的对象所需要的key
  • 最后,在to函数里,进行复制处理
  from(originObject) {
    this.originObject = originObject;
    return this;
  };

  get(array) {
    this.newObjectKey = array;
    return this;
  };

  to(newObject) {
    for (var key of this.newObjectKey) {
      newObject[key] = this.originObject[key];
    }
    return this;
  };

现在,我们已经完成了基本的功能。

一个意外的情况

在我实现的过程中,我发现如果我传给get函数的array如果是空的话,那么就不work了。于是,就针对这个情况写了个测试,然后实现了这个功能:

  get(keyArray) {
    if(keyArray) {
      this.newObjectKey = keyArray;
    } else {
      this.newObjectKey = [];
    }
    return this;
  };

  to(newObject) {
    if(this.newObjectKey.length > 0){
      for (var key of this.newObjectKey) {
        newObject[key] = this.originObject[key];
      }
    } else {
      // Clone each property.
      for (var prop in this.originObject) {
        newObject[prop] = clone(this.originObject[prop]);
      }
    }
    return this;
  };

在这个过程中,我还找到了一个clone函数,来替换from中的"="。

  from(originObject) {
    this.originObject = clone(originObject);
    return this;
  };

第三个驱动开发的测试
---

因为有了第一个测试的基础,我们要写下一测试变得非常简单:

```javascript
dlm.get(['title'])
  .from(originObject)
  .add('tag', 'hello,world,linux')
  .to(newObject);

expect(newObject.tag).toBe("hello,world,linux");
expect(newObject.title).toBe("hello");
expect(newObject.author).toBe(undefined);

在实现的过程中,我又投机取巧了,我创建了一个对象来存储新的对象的key和value:

  add(field, value) {
    this.objectForAddRemove[field] = value;
    return this;
  };

同样的,在to方法里,对其进行处理:

  to(newObject) {
    function cloneObjectForAddRemove() {
      for (var prop in this.objectForAddRemove) {
        newObject[prop] = this.objectForAddRemove[prop];
      }
    }

    function cloneToNewObjectByKey() {
      for (var key of this.newObjectKey) {
        newObject[key] = this.originObject[key];
      }
    }

    function deepCloneObject() {
      // Clone each property.
      for (var prop in this.originObject) {
        newObject[prop] = clone(this.originObject[prop]);
      }
    }

    cloneObjectForAddRemove.call(this);
    if (this.newObjectKey.length > 0) {
      cloneToNewObjectByKey.call(this);
    } else {
      deepCloneObject.call(this);
    }
    return this;
  };

在这个函数里,我们用cloneObjectForAddRemove函数来复制将要添加的key和value到新的对象里。

remove和handle函数

对于剩下的remove和handle来说,他们实现起来都是类似的:

  • 存储相应的对象操作
  • 然后在to函数里进行处理

编写测试:

    function handler(blog) {
      return blog[0];
    }

    ddm.get(['title', 'blog', 'author'])
      .from(originObject)
      .handle("blog", handler)
      .to(newObject);
    expect(newObject.blog).toBe('A');

然后实现功能:

  remove(field) {
    this.objectKeyForRemove.push(field);
    return this;
  };

  handle(field, handle) {
    this.handleFunction.push({
      field: field,
      handle: handle
    });
    return this;
  }

这一切看上去都很自然,然后我们就可以对其进行重构了。

100%的测试覆盖率

由于,我们先编写了测试,再实现代码,所以我们编写的代码都有对应的测试。因此,我们可以轻松实现相当高的测试覆盖率。

在这个Case下,由于业务场景比较简单,要实现100%的测试覆盖率就是一件很简单的事。

(PS: 我不是TDD的死忠,只是有时候它真的很美。)

关于我

Github: @phodal     微博:@phodal     知乎:@phodal    

微信公众号(Phodal)

围观我的Github Idea墙, 也许,你会遇到心仪的项目

QQ技术交流群: 321689806
comment

Feeds

RSS / Atom

最近文章

关于作者

Phodal Huang

Engineer, Consultant, Writer, Designer

ThoughtWorks 技术专家

工程师 / 咨询师 / 作家 / 设计学徒

开源深度爱好者

出版有《前端架构:从入门到微前端》、《自己动手设计物联网》、《全栈应用开发:精益实践》

联系我: h@phodal.com

微信公众号: 最新技术分享

标签