React Native測試小調(diào)研

測試

隨著代碼量的增長 細(xì)小的錯誤和一些沒能夠預(yù)料到的邊界條件能夠造成大的問題
bug可以造成糟糕的用戶體驗 甚至是業(yè)務(wù)的損失
一個有效的監(jiān)測方式是在發(fā)布前對代碼進行測試

為什么要做測試

是人都會犯錯。測試可以幫助我們發(fā)現(xiàn)問題并驗證代碼的正確與否。測試可以保證代碼在新增功能、重構(gòu)代碼或者升級了依賴后繼續(xù)正常工作

測試的價值遠(yuǎn)比我們意識到的要多很多。最好的修復(fù)bug的方式就是寫一個失敗的用例來報錯出來 在修復(fù)bug的過程中重新跑測試用例,如果它通過了意味著bug修復(fù)了

測試用例還能作為團隊的文檔,幫助未見過代碼庫的人來理解現(xiàn)有代碼是如何工作的。
更多的自動化測試意味著花費在人工測試的時間上更少,從而釋放出寶貴的時間

一. 策略

1. 單元測試

1.1 概念

單元測試(unit testing),是指對軟件中的最小可測試單元進行檢查和驗證。
最小的可測但愿可以是一個模塊、一個函數(shù)或者一個類

單元測試從長期來看,可以提高代碼質(zhì)量,減少維護成本,降低重構(gòu)難度。但是從短期來看,加大了工作量,對于進度緊張的項目中的開發(fā)人員來說,可能會成為不少的負(fù)擔(dān)

1.2 哪些代碼需要有單元測試覆蓋

  1. 邏輯復(fù)雜的
  2. 容易出錯的
  3. 不易理解的,即使是自己過段時間也會遺忘的,看不懂自己的代碼,單元測試代碼有助于理解代碼的功能和需求
  4. 公共代碼。比如自定義的所有http請求都會經(jīng)過的攔截器;工具類等。
  5. 核心業(yè)務(wù)代碼。一個產(chǎn)品里最核心最有業(yè)務(wù)價值的代碼應(yīng)該要有較高的單元測試覆蓋率。

1.3 何時寫

  1. 在具體實現(xiàn)代碼之前,這是測試驅(qū)動開發(fā)(TDD)所提倡的;

  2. 與具體實現(xiàn)代碼同步進行。先寫少量功能代碼,緊接著寫單元測試(重復(fù)這兩個過程,直到完成功能代碼開發(fā))。其實這種方案跟第一種已經(jīng)很接近,基本上功能代碼開發(fā)完,單元測試也差不多完成了。

  3. 編寫完功能代碼再寫單元測試

1.4 為什么要寫單元測試

  • 修改代碼時,可以很快的驗證正確性
  • 重構(gòu)代碼時,只要原有測試用例全部通過,則代碼重構(gòu)完成,當(dāng)然需要原有測試用例設(shè)計合理
  • 當(dāng)功能持續(xù)增加時,如果影響到原有功能,可以很快發(fā)現(xiàn)

1.5小結(jié)

  • 單元測試可以有效地測試某個程序模塊的行為,是未來重構(gòu)代碼的信心保證。
  • 單元測試的測試用例要覆蓋常用的輸入組合、邊界條件和異常。
  • 單元測試代碼要非常簡單,如果測試代碼太復(fù)雜,那么測試代碼本身就可能有bug。
  • 單元測試通過了并不意味著程序就沒有bug了,但是不通過程序肯定有bug。

集成測試

接下來是集成測試。集成測試是一組不同的單元被當(dāng)作一個整體來進行測試的階段。

又稱組裝測試,即對程序模塊采用一次性或增值方式組裝起來,對系統(tǒng)的接口進行正確性檢驗的測試工作。整合測試一般在單元測試之后、系統(tǒng)測試之前進行。實踐表明,有時模塊雖然可以單獨工作,但是并不能保證組裝起來也可以同時工作。

功能測試

對系統(tǒng)的功能及性能的總體測試。

是一種黑盒測試的類型,其主要關(guān)注于用戶需求和交互。功能測試會從整體上覆蓋所有的底層軟件,所有的用戶交互和應(yīng)用。

測試金字塔

在金字塔模型之前,流行的是冰淇淋模型。包含了大量的手工測試、端到端的自動化測試及少量的單元測試。造成的后果是,隨著產(chǎn)品壯大,手工回歸測試時間越來越長,質(zhì)量很難把控;自動化case頻頻失敗,每一個失敗對應(yīng)著一個長長的函數(shù)調(diào)用,到底哪里出了問題?單元測試少的可憐,基本沒作用。

冰淇淋模型

)

Mike Cohn 在他的著作《Succeeding with Agile》一書中提出了“測試金字塔”這個概念。這個比喻非常形象,它讓你一眼就知道測試是需要分層的。它還告訴你每一層需要寫多少測試。 測試金字塔本身是一條很好的經(jīng)驗法則,我們最好記住Cohn在金字塔模型中提到的兩件事:

測試金字塔


是目前比較流行的一種設(shè)計自動化測試的思路,核心觀點如下:

  1. 越下層的測試效率越高, 覆蓋率也越高, 開發(fā)維護成本越低
  2. 更上層的測試集成性更好, 但維護成本更高
  3. 大量的Unit測試, 少量的集成測試和更少的E2E測試是比較合理的平衡點(Google在blog中推薦70/20/10的測試用例個數(shù)比例)

React Native

端到端測試 End-to-End Tests

從用戶的視角驗證app功能的正確性
這是在編譯app包之后,對安裝包進行測試,此時不再是考慮React組件 React Native API或者是Redux store或者業(yè)務(wù)邏輯。
E2E測試庫允許找出屏幕上的控件 并且可以與其進行交互

E2E測試可以給app最大的信心,與其它測試相比

  • 編寫測試用例需要花費更多的時間
  • 運行更慢
  • 更容易不穩(wěn)定("不穩(wěn)定"是指在沒有任何更改的情況下隨機通過和失敗的測試)

在React Native中, Detox是一個流行的框架,因為它是為React Native應(yīng)用量身定制的。在iOS和Android應(yīng)用中,Appium也是一個流行框架

組件測試

React組件負(fù)責(zé)渲染app,用戶直接和組件進行交互。及時app業(yè)務(wù)邏輯有很高的測試覆蓋率并且是正常工作的,沒有組件測試,可能還是會給用戶展示一個有問題的頁面。組件測試可以分成單元測試和集成測試,它們是React Native核心部分,我們分別來講
對于React組件 有兩個東西需要測試

  • 交互:確保組件和用戶交互時是正常運行的(比如 當(dāng)用戶點擊某個按鈕時)
  • 渲染:確保React組件顯示是正確的(比如 按鈕在頁面的外觀以及位置)

比如,想測試按鈕的顯示以及點擊時間是否正確被處理
React的Test Render可以將React組件轉(zhuǎn)化成純Javascript對象,不需要依賴DOM或者是移動手機環(huán)境
React Native Testing Library 基于React的Test Renderer還添加了fireEvent和query API

組件測試只是Javascript測試,任何iOS、Android或React Native的代碼并不能被測試。也就是說不能保證100%的可用性。如果iOS或者Android代碼里有bug,是測試不出問題的

單元測試 framework

框架 特點 支持 github stars
Jest 配置簡單 并行運行 React & React Native 33k
Mocha 靈活的測試框架,需要引入斷言庫(should.js, chai, expect.js, better-assert, unexpected等)、覆蓋統(tǒng)計等 node.js & browser 19.9k
Jasmine 內(nèi)置斷言expect, 需要全局聲明,且需要配置,相對來說使用更復(fù)雜、不夠靈活。 node.js & browser 15k
cypress 提供可交互頁面 支持Mac、Windows、Linux browser 24.3k

Jest

FaceBook出品的前端測試框架,適合用于React和React Native的單元測試。

有以下幾個特點:

  • 簡單易用:易配置,自帶斷言庫和mock庫。

  • 快照測試:能夠創(chuàng)造一個當(dāng)前組件的渲染快照,通過和上次保存的快照進行比較,如果兩者不匹配說明測試失敗。

  • 測試報告:內(nèi)置了Istanbul,通過一定配置可以測試代碼覆蓋率,生成測試報告。

Enzyme

Enzyme是AirBnb開源的React測試工具庫,通過一套簡潔的api,可以渲染一個或多個組件,查找元素,模擬元素交互(如點擊,觸摸),通過和Jest相互配合可以提供完整的React組件測試能力。

初始化配置

jest --init

常用方法

  • describe:創(chuàng)造一個塊,將一組相關(guān)的測試用例組合在一起

  • test:也可以用it,測試用例

  • expect:使用該函數(shù)斷言某個值

生命周期

beforeAll
afterAll

beforeEach
afterEach

當(dāng)before和after在describe方法中時 作用域在describe里


beforeAll(() => console.log('1 - beforeAll'));
afterAll(() => console.log('1 - afterAll'));
beforeEach(() => console.log('1 - beforeEach'));
afterEach(() => console.log('1 - afterEach'));
test('', () => console.log('1 - test'));

describe('Scoped / Nested block', () => {
  beforeAll(() => console.log('2 - beforeAll'));
  afterAll(() => console.log('2 - afterAll'));
  beforeEach(() => console.log('2 - beforeEach'));
  afterEach(() => console.log('2 - afterEach'));
  test('', () => console.log('2 - test'));
});

// 1 - beforeAll
// 1 - beforeEach
// 1 - test
// 1 - afterEach
// 2 - beforeAll
// 1 - beforeEach
// 2 - beforeEach
// 2 - test
// 2 - afterEach
// 1 - afterEach
// 2 - afterAll
// 1 - afterAll

describe執(zhí)行順序

describe('outer', () => {
  console.log('describe outer-a');

  describe('describe inner 1', () => {
    console.log('describe inner 1');
    test('test 1', () => {
      console.log('test for describe inner 1');
      expect(true).toEqual(true);
    });
  });

  console.log('describe outer-b');

  test('test 1', () => {
    console.log('test for describe outer');
    expect(true).toEqual(true);
  });

  describe('describe inner 2', () => {
    console.log('describe inner 2');
    test('test for describe inner 2', () => {
      console.log('test for describe inner 2');
      expect(false).toEqual(false);
    });
  });

  console.log('describe outer-c');
});

// describe outer-a
// describe inner 1
// describe outer-b
// describe inner 2
// describe outer-c
// test for describe inner 1
// test for describe outer
// test for describe inner 2

斷言

普通斷言

  • toBe
  • toEqual
  • .not.toBe
test('two plus two is four', () => {
  expect(2 + 2).toBe(4);
});

test('object assignment', () => {
  const data = {one: 1};
  data['two'] = 2;
  expect(data).toEqual({one: 1, two: 2});
});

test('adding positive numbers is not zero', () => {
for (let a = 1; a < 10; a++) {
for (let b = 1; b < 10; b++) {
expect(a + b).not.toBe(0);
}
}
})

  • expect(n).toBeNull();
  • expect(n).toBeDefined();
  • expect(n).not.toBeUndefined();
  • expect(n).not.toBeTruthy();
  • expect(n).toBeFalsy();

數(shù)字相關(guān)的斷言

  • expect(value).toBeGreaterThan(3);

  • expect(value).toBeGreaterThanOrEqual(3.5);

  • expect(value).toBeLessThan(5);

  • expect(value).toBeLessThanOrEqual(4.5);

    //toBe and toEqual are equivalent for numbers

  • expect(value).toBe(4);

  • expect(value).toEqual(4);

浮點數(shù)

test('adding floating point numbers', () => {
  const value = 0.1 + 0.2;
  //expect(value).toBe(0.3);           This won't work because of rounding error
  expect(value).toBeCloseTo(0.3); // This works.
});

字符串

可使用正則表達(dá)式

test('there is no I in team', () => {
  expect('team').not.toMatch(/I/);
});

test('but there is a "stop" in Christoph', () => {
  expect('Christoph').toMatch(/stop/);
});

數(shù)組

  • toContain

異常

function compileAndroidCode() {
  throw new Error('you are using the wrong JDK');
}

test('compiling android goes as expected', () => {
  expect(() => compileAndroidCode()).toThrow();
  expect(() => compileAndroidCode()).toThrow(Error);

  // You can also use the exact error message or a regexp
  expect(() => compileAndroidCode()).toThrow('you are using the wrong JDK');
  expect(() => compileAndroidCode()).toThrow(/JDK/);
});

Mock 函數(shù)

  • 替換真實的函數(shù)實現(xiàn)
  • 檢測函數(shù)調(diào)用以及傳入的參數(shù)
  • 測試時配置返回值
  • 監(jiān)測實例的初始化

1. 在測試代碼中創(chuàng)建一個mock函數(shù)

const myMock = jest.fn();
console.log(myMock());
// > undefined

myMock.mockReturnValueOnce(10).mockReturnValueOnce('x').mockReturnValue(true);

console.log(myMock(), myMock(), myMock(), myMock());
// > 10, 'x', true, true
// users.js
import axios from 'axios';

class Users {
  static all() {
    return axios.get('/users.json').then(resp => resp.data);
  }
}

export default Users;

// users.test.js
import axios from 'axios';
import Users from './users';

jest.mock('axios');

test('should fetch users', () => {
  const users = [{name: 'Bob'}];
  const resp = {data: users};
  axios.get.mockResolvedValue(resp);

  // or you could use the following depending on your use case:
  // axios.get.mockImplementation(() => Promise.resolve(resp))

  return Users.all().then(data => expect(data).toEqual(users));
});
// foo.js
module.exports = function () {
  // some implementation;
};

// test.js
jest.mock('../foo'); // this happens automatically with automocking
const foo = require('../foo');

// foo is a mock function
foo.mockImplementation(() => 42);
foo();
// > 42

2. 手動Mock

mocks下實現(xiàn)一份mock代碼

.
├── config
├── __mocks__
│   └── fs.js
├── models
│   ├── __mocks__
│   │   └── user.js
│   └── user.js
├── node_modules
└── views

3. require actual implement

jest.mock('node-fetch');
import fetch from 'node-fetch';
const {Response} = jest.requireActual('node-fetch');

Snapshot Test

初衷 測試React組件

確保UI不會意外更改時,是非常有用的工具
典型的例子是渲染UI組件后后,獲得組件快照,然后和之前存儲的snapshot文件進行比較

snapshot測試是傳統(tǒng)測試的一個補充而非替代

如何做snapshot測試

如果渲染真實的UI界面,需要編譯一個完整的app
如果想要單獨的測試一個組件 可以使用測試渲染工具來快速生成React樹的可序列化的值

  • demo test

    // __tests__/Intro-test.js
    import React from 'react';
    import renderer from 'react-test-renderer';
    import Intro from '../Intro';
    
    test('renders correctly', () => {
      const tree = renderer.create(<Intro />).toJSON();
      expect(tree).toMatchSnapshot();
    });
    
  • Demo result

    // __tests__/__snapshots__/Intro-test.js.snap
    exports[`Intro renders correctly 1`] = `
    <View
      style={
        Object {
          "alignItems": "center",
          "backgroundColor": "#F5FCFF",
          "flex": 1,
          "justifyContent": "center",
        }
      }>
      <Text
        style={
          Object {
            "fontSize": 20,
            "margin": 10,
            "textAlign": "center",
          }
        }>
        Welcome to React Native!
      </Text>
      <Text
        style={
          Object {
            "color": "#333333",
            "marginBottom": 5,
            "textAlign": "center",
          }
        }>
        This is a React Native snapshot test.
      </Text>
    </View>
    `;
    
  • .snap如何產(chǎn)生
    .snap

    組件快照是一串類似JSX的字符串,由Jest內(nèi)置的React序列化器生成
    將React組件樹轉(zhuǎn)換成方便人閱讀的形式,也就是說組件快照是是組件渲染結(jié)果的文本形式

    第一次運行測試的時候會將當(dāng)前渲染結(jié)果生成snapshot 之后每次運行測試都是與此次的snap文件進行比較

  • CI里是否可以自動生成

    Jest執(zhí)行測試是不會自動生成 通過--updateSnapshot來強制生成

  • snapshot文件是否放在git等版本管理中
    是的

Jest使用pretty-format來生成可讀的snapshots文件,使用 snapshot-diff來比較組件snapshot文件的差異

Snapshot 測試失敗

snapshoterror
  1. 界面出問題了
  2. 新的snapshot是所希望的結(jié)果 則需要更新snapshot

此時測試結(jié)果里會展示新舊.snap的不同之處

異步測試

簡單的API測試

測試覆蓋率

coverage

class Mock


describe('DestinationClass', () => {
  it('mock function of the instance of destination class', () => {
    const instanceFunction = jest.fn()
    DestinationClass.prototype.instanceFunction = instanceFunction
    const destinationInstance = new DestinationClass()

    destinationInstance.instanceFunction()

    expect(instanceFunction).toHaveBeenCalledTimes(1)
  })

  it('mock static function of destination class', () => {
    const classFunction = jest.fn()
    DestinationClass.classFunction = classFunction

    DestinationClass.classFunction()

    expect(classFunction).toHaveBeenCalledTimes(1)
  })
})

單純網(wǎng)絡(luò)API功能測試

  • SoapUI
  • JMeter
  • PostMan
  • 自己寫代碼

app內(nèi)mock參考

測試結(jié)果的可視化

jest-html-reporter

npm install jest-html-reporter --save-dev
reporters: [
    'default',
    [
      './node_modules/jest-html-reporter',
      {
        pageTitle: 'Test Report',
      },
    ],
  ],

默認(rèn)輸出結(jié)果在./test-report.html

測試報告與Sonarquebe集成

Jest 實操

全局mock

jestSetupFiles.js

import mockAsyncStorage from '@react-native-community/async-storage/jest/async-storage-mock'

jest.mock('@react-native-community/async-storage', () => mockAsyncStorage)

實踐中遇到的問題

  • test時控制臺有l(wèi)og, 如何取消log
global.console = {
  log: jest.fn(), // console.log are ignored in tests
  // Keep native behaviour for other methods, use those to print out things in your own tests, not `console.log`
  error: console.error,
  warn: console.warn,
  info: console.info,
  debug: console.debug,
}
  • Native module cannot be null

    jest.mock('react-native-device-info')
    
  • Cannot find module './images/ic_default_avatar.png'

    require('./images/ic_default_avatar.png')
    

    *require('img1.png') becomes Object { "testUri": 'path/to/img1.png' } in the Jest snapshot.

    增加assetFileTransformer.js文件并在jest.config.js中添加

      moduleNameMapper: {
      // '\\.(jpg|ico|jpeg|png|gif|eot|otf|webp|svg|ttf|woff|woff2|mp4|webm|wav|mp3|m4a|aac|oga)$': '<rootDir>/__tests__/__mocks__/assetFileTransformer.js',
    },
    

參考鏈接

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容