1. 程式人生 > >在2018年裡關於測試JavaScript的回顧

在2018年裡關於測試JavaScript的回顧

這篇文章的目的是想要讓你對於在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來模擬和修改不是測試流程部分。