在2018年里关于测试JavaScript的回顾

887 阅读8分钟

这篇文章的目的是想要让你对于在2018中测试Javascript最重要的原因、时期和工具还有方法保持了解。这篇文章的内容参考了许多文章(链接列在要文底下),还包含了我们自己多年在Welldone Software Solutions里不同产品中的实现的不同测试方案的经验。

阅读这篇文章的人,我们假设他们只知道在2018年的前端开发社区是怎么样测试Javascript的。这是他们分享给他们的同事、家人还有朋友的很大原因。

  • 我对这篇文章做了大量的调查,如果你发现任何错误,请在下面评论,我会第一时间纠正它。
  • 留意文章底下的链接,阅读它们会让你对整个大局有所了解并使你成功这方面的专家(理论上)。
  • 最好的实践这篇文章的方法是选择一个你所需要的测试类型,再选择若干看起来适合你的工具,然后再测试他们。我们正处于一个有大量工具和实践的环境。你的目标就是筛选它们然后对你的独立案例作最好的组合。

介绍

看一下Facebook的测试框架Jest的Logo:

正如你所看到的,它的口号是保证它是一个“不痛苦的”JavaScript测试框架,不过正如有些人在评论区中指出:

(没有什么测试是不痛苦的)

的确,Facebook使用这个口号有一个很重要的原因。通常JavaScript开发者对网站测试是非常痛苦的,JS测试总是受限制、难以实现,速度慢和有时非常昂贵。

不管怎么样,在对的策略和在对的工具组合下,一个几乎全覆盖的测试是可以实现的,而且成这个测试是非常容易管理、简单而且相对较快。

测试类型

你可以在这里这里还有这里更深入的了解不同的测试类型。通常情况下,对于网站来说最重要的测试类型是:

  • 单元测试:提供一些输入数据来测试每个独立的函数或者类,并且保证他们的输出是符合预期的。
  • 集成测试:测试流程或者组件的表现是符合预期的,包含副作用。
  • UI测试:(A.K.A 功能测试)在不管内部结构实现的情况下,通过控制浏览器或者网站,测试产品本身的流程是否可以达到预期表现。

测试工具类型

测试工具可以分为以下几类。有一些工具仅仅只提供一个功能,有一些则提供了一个包含许多功能的工具包。

为了后期可以实现更复杂的功能,我们常常会使用工具包,哪怕我们一开始只是使用他其中的一个功能。

  1. 提供测试结构(Testing structure)(Mocha, Jasmine, Jest, Cucumber)
  2. 提供断言函数(Assertion functions)(Chai, Jasmine, Jest, Unexpected)
  3. 创建、显示和监听测试结果(Mocha, Jasmine, Jest, Karma)
  4. 创建和对比组件和数据结构的快照,从而保证对比上一次运行的改变是符合预期的(Jest, Ava)
  5. 提供mocks, spies, and stubs (Sinon, Jasmine, enzyme, Jest, testdouble)
  6. 创建代码覆盖率报告(Istanbul, Jest, Blanket)
  7. 提供可以执行用户行为的浏览器环境或类浏览器环境 (Protractor, Nightwatch, Phantom, Casper)

让我们来解释一下上面提到的一些东西:

测试结构(Testing structure)是指你的测试行为。通常测试都是运行在行为驱动开发的BDD的模式下的。他通常看起来像这样子:

describe('calculator', function() {
  // describes a module with nested "describe" functions
  describe('add', function() {
    // specify the expected behavior
    it('should add 2 numbers', function() {
       //Use assertion functions to test the expected behavior
       ...  
    })
  })
})

断言函数(Assertion functions)是指保证测试变量经过断言函数后会返回期望值。他通常看起来像这样,最常见的是第一次个和第二个例子:

// Chai expect (popular)
expect(foo).to.be.a('string')
expect(foo).to.equal('bar')

// Jasmine expect (popular)
expect(foo).toBeString()
expect(foo).toEqual('bar')

// Chai assert
assert.typeOf(foo, 'string')
assert.equal(foo, 'bar')

// Unexpected expect
expect(foo, 'to be a', 'string')
expect(foo, 'to be', 'bar')

提示:这里有一篇很棒的文章讲解了关于Jasmine的高级断言。

Spies为我们提供了关于函数的信息——比如说,这个函数被调用了多少次,在什么情况下调用,被谁调用等等。

他们通常用在集成测试里,保证含有副作用的流程可以被正常测试出期望结果。这个例子,这个计算函数在某些流程里被调用了多少次?

it('should call method once with the argument 3', () => {
  
  // create a sinon spy to spy on object.method
  const spy = sinon.spy(object, 'method')
  
  // call the method with the argument "3"
  object.method(3)

  // make sure the object.method was called once, with the right arguments
  assert(spy.withArgs(3).calledOnce)
  
})

Stubbing or dubbing把某些函数替换成为特定的函数,在这个特定函数的作用下保证行为是正常表现的。

// Sinon
sinon.stub(user, 'isValid').returns(true)

// Jasmine stubs are actually spies with stubbing functionallity
spyOn(user, 'isValid').andReturns(true)

promises的情况下会像这样:

it('resolves with the right name', done => {
  
  // make sure User.fetch "responds" with our own value "David"
  const stub = sinon
    .stub(User.prototype, 'fetch')
    .resolves({ name: 'David' })
  
  User.fetch()
    .then(user => {
      expect(user.name).toBe('David')
      done()
    })
})

Mocks or Fakes 模拟一些特定模块或行为来测试不同情况下的流程。

据个例子,在测试中,Sinon可以通过模拟一个服务接口来保证在离线的情况下可以得到快速的期望响应。

it('returns an object containing all users', done => {
  
  // create and configure the fake server to replace the native network call
  const server = sinon.createFakeServer()
  server.respondWith('GET', '/users', [
    200,
    { 'Content-Type': 'application/json' },
    '[{ "id": 1, "name": "Gwen" },  { "id": 2, "name": "John" }]'
  ])

  // call a process that includes the network request that we mocked
  Users.all()
    .done(collection => {
      const expectedCollection = [
        { id: 1, name: 'Gwen' },
        { id: 2, name: 'John' }
      ]
      expect(collection.toJSON()).to.eql(expectedCollection)
      done()
    })
  
  // respond to the request
  server.respond()
  
  // remove the fake server
  server.restore()
})

快照测试是指你拿的一个数据和另一个期望的数据进行对比。

下面的例子来源于Jest的官方文档,他展示了的一个link组件的快照测试。

it('renders correctly', () => {
  
  // create an instance of the Link component with page and child text
  const linkInstance = (
    <Link page="http://www.facebook.com">Facebook</Link>
  )
  
  // create a data snapshot of the component
  const tree = renderer.create(linkInstance).toJSON()
  
  // compare the sata to the last snapshot
  expect(tree).toMatchSnapshot()
})

他不会为这个组件进行渲染并且保存成一张图片,但是它可以把它的内部结构保存在一个单独的文件中,像这样子:

exports[`renders correctly 1`] = `
<a
  className="normal"
  href="http://www.facebook.com"
  onMouseEnter={[Function]}
  onMouseLeave={[Function]}
>
  Facebook
</a>
`;

当测试运行时,并且有一个不同于上一次的快照,开发者可以及时的知道它们之间不同的地方。

注意:快照通常是用来对比组件的结构数据,但是他们也可以用来对比其他类型的数据,像redux stores 和应用中不同单元的内部结构。

浏览器或类浏览器环境可以是它三个中的其中一个:

  • jsdom——你的纯JavaScript的环境来模拟真实的浏览器。他没有界面并不渲染任何东西。它提供window、document,body,location,cookies,selectors和你在浏览器中运行JS时你想用到的任何东西。
  • 无头浏览器环境——一个浏览器在没有界面的情况下运行,目的是为了让浏览器的响应更快。
  • 真实的浏览器环境——打开一个真实的浏览器并运行你的测试

把所有东西组合在一起

我们建议尽可能地用同一套工具来执行所有的测试类型:相同的测试结构和语法,断言函数,测试报告,监听机制。

我们同样建议使用两个不同的测试流程。一个是单元和集成测试,另一个是UI测试。因为UI测试会耗费大量的时间,特别是测试不同的浏览器环境和不同的设备环境上的浏览器,它会相当耗费精力,所以你应该尽可能地少在首要的流程中运行它。几个例子:只有在合并新功能分支的情况下运行。

单元测试

应该覆盖应用中所有小的单元——utils,services和helpers。为所有这些单元提供简单的边缘情况下的输入,并使用断言函数来确保单元的输出是期望的。当然也要使用覆盖率报告工具来知道哪些单元是被测试覆盖的。

单元测试要尽可能的使用函数工编程和纯函数的原因之一是,你的应用越纯,你的测试就越简单。

集成测试

这种测试专注在单元测试和应用的结果,它是测试在应用许多单元是正常的,但是所有单元整全起来的流程却是失败的情况。

集成测试(包括快照),在另一方面,可以检测出许多因为你修改一个东西或者删除一个东西所造成的意外错误。

它也使我们记得在现实生活中,许多原因包括不完美的产品设计,大范围使用黑箱,不是所有单元都是纯的,不是所有单元都是可以测试等。一些单元只需要测试大流程的一部分。

集成测试可以覆盖重要的跨流程模块。相对于单元测试,你可以使用spies来替换副作用,从而保证输出可以被断言。你可以使用stubs来模拟和修改不是测试流程部分。