React Native單元測試基礎知識及常見問題

基本用法

匹配器

  • toBe

    test('two plus two is four', () => {
      expect(2 + 2).toBe(4);
    });
    
  • toEqual (檢查兩個對象或集合是否完全相等)

  • toBeNull、toBeUndefined乔宿、toBeDefined访雪、toBeTruthy冬阳、toBeFalsy

  • toBeGreaterThan肝陪、toBeLessThan

  • toBeCloseTo(浮點數(shù)判斷)

  • toMatch(字符串判斷)

    test('there is no I in team', () => {
      expect('team').not.toMatch(/I/);
    });   
    
  • toContain (集合判斷)

  • toThrow

    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/);
    });
    

測試異步代碼

  • done()

    test('the data is peanut butter', done => {
      function callback(data) {
        try {
          expect(data).toBe('peanut butter');
          done();
        } catch (error) {
          done(error);
        }
      }
    
      fetchData(callback);
    });
    
    //  如果未調(diào)用done氯窍,則默認會輸出‘Error: thrown: "Exceeded timeout of 5000 ms for a test.’,想要詳細的錯誤信息贝淤,可以使用try/catach
    
  • promise

    test('the data is peanut butter', () => {
      return fetchData().then(data => {
        expect(data).toBe('peanut butter');
      });
    });
    
    test('the fetch fails with an error', () => {
      expect.assertions(1);
      return fetchData().catch(e => expect(e).toMatch('error'));
    });
    
    // return Promise,jest 會自動處理resolve 和 reject 情況
    
  • .resolves/.rejects

    test('the data is peanut butter', () => {
      return expect(fetchData()).resolves.toBe('peanut butter');
    });
    
    test('the fetch fails with an error', () => {
      return expect(fetchData()).rejects.toMatch('error');
    });
    
  • async/await

    test('the data is peanut butter', async () => {
      const data = await fetchData();
      expect(data).toBe('peanut butter');
    });
    
    test('the fetch fails with an error', async () => {
      expect.assertions(1);
      try {
        await fetchData();
      } catch (e) {
        expect(e).toMatch('error');
      }
    });
    
  • combine async/await with .resolves/.rejects

    test('the data is peanut butter', async () => {
      await expect(fetchData()).resolves.toBe('peanut butter');
    });
    
    test('the fetch fails with an error', async () => {
      await expect(fetchData()).rejects.toMatch('error');
    });
    

初始化和銷毀

  • beforeEach / afterEach

  • beforeAll / afterAll

  • describe

    /*Jest executes all describe handlers in a test file before it executes any of the actual tests. This is another reason to do setup and teardown inside before* and after* handlers rather than inside the describe blocks. Once the describe blocks are complete, by default Jest runs all the tests serially in the order they were encountered in the collection phase, waiting for each to finish and be tidied up before moving on.*/
    
    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
    

模擬函數(shù)

  • 什么是mock functions

    Mock functions allow you to test the links between code by erasing the actual implementation of a function, capturing calls to the function (and the parameters passed in those calls), capturing instances of constructor functions when instantiated with new, and allowing test-time configuration of return values.

    The goal for mocking is to replace something we don’t control with something we do

    mock函數(shù)提供的特性如下:

    • 捕獲調(diào)用
    • 設置返回值
    • 改變實現(xiàn)
  • 創(chuàng)建模擬函數(shù)

    // 通過jest.fn() 創(chuàng)建模擬函數(shù)布隔,可以測試函數(shù)調(diào)用狀態(tài)和調(diào)用參數(shù)等信息
    
    function forEach(items, callback) {
      for (let index = 0; index < items.length; index++) {
        callback(items[index]);
      }
    }
    
    const mockCallback = jest.fn(x => 42 + x);
    forEach([0, 1], mockCallback);
    
    // The mock function is called twice
    expect(mockCallback.mock.calls.length).toBe(2);
    
    // The first argument of the first call to the function was 0
    expect(mockCallback.mock.calls[0][0]).toBe(0);
    
    // The first argument of the second call to the function was 1
    expect(mockCallback.mock.calls[1][0]).toBe(1);
    
    // The return value of the first call to the function was 42
    expect(mockCallback.mock.results[0].value).toBe(42);
    
  • 模擬返回值

    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;
    ---------------------------------------------------
    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));
    });
    
    • jest.spyOn()

      jest.mock()通常無法訪問到原始實現(xiàn)招刨,spyOn可以修改原始實現(xiàn)沉眶,且稍后可以恢復

測試覆蓋率

yarn test --coverage

Image_20211222094834.png

Stats為語句測試覆蓋率谎倔。Branch為條件分支測試覆蓋率传藏。Funcs為函數(shù)覆蓋率彤守。Lines為行覆蓋率具垫。Uncovered Line為未覆蓋的行號

快照

Image_20211222155005.png
test('renders correctly', () => {
    const tree = renderer.create(<He/>).toJSON();
    expect(tree).toMatchSnapshot();
});

錯誤集錦

ES6 support

  • FAIL  src/__tests__/navigation.test.ts
      ● Test suite failed to run
    
        Jest encountered an unexpected token
    
        This usually means that you are trying to import a file which Jest cannot parse, e.g. it's not plain JavaScript.
    
        By default, if Jest sees a Babel config, it will use that to transform your files, ignoring "node_modules".
    
        Here's what you can do:
         ? To have some of your "node_modules" files transformed, you can specify a custom "transformIgnorePatterns" in your config.
         ? If you need a custom transformation specify a "transform" option in your config.
         ? If you simply want to mock your non-JS modules (e.g. binary assets) you can stub them out with the "moduleNameMapper" config option.
    
        You'll find more details and examples of these config options in the docs:
        https://jestjs.io/docs/en/configuration.html
    
        Details:
    
        /Users/jyurek/Development/react-navigation-error-replication/node_modules/react-navigation-stack/dist/index.js:2
        import { Platform } from 'react-native';
               ^
    
        SyntaxError: Unexpected token {
    
          2 | import {ExampleScreen} from './screens/ExampleScreen';
          3 |
        > 4 | const Navigation = createStackNavigator({
            |                    ^
          5 |   ExampleScreen: {
          6 |     screen: ExampleScreen,
          7 |   },
    
          at ScriptTransformer._transformAndBuildScript (node_modules/@jest/transform/build/ScriptTransformer.js:451:17)
          at ScriptTransformer.transform (node_modules/@jest/transform/build/ScriptTransformer.js:493:19)
          at Object.get createStackNavigator [as createStackNavigator] (node_modules/react-navigation/src/react-navigation.js:107:12)
          at Object.<anonymous> (src/navigation.ts:4:20)
    

    此類問題為native module使用了ES語法,而jest默認是不支持ES語法的洲胖。解決方法是使用babel, 自定義babel配置, jest自動使用babel做語法轉(zhuǎn)換坯沪。但是又因為jest默認會忽略'node_modules', 即'node_modules'下的內(nèi)容不會做語法轉(zhuǎn)換。解決方法為在jest配置中添加‘transformIgnorePatterns’屬性:

    {
      "transformIgnorePatterns": [
        "node_modules/(?!(react-native|react-native-button)/)"
      ]
    }
    

mock NativeModule

  • Test suite failed to run
    
        [@RNC/AsyncStorage]: NativeModule: AsyncStorage is null.
    
        To fix this issue try these steps:
    
          ? Run `react-native link @react-native-async-storage/async-storage` in the project root.
    
          ? Rebuild and restart the app.
    
          ? Run the packager with `--reset-cache` flag.
    
          ? If you are using CocoaPods on iOS, run `pod install` in the `ios` directory and then rebuild and re-run the app.
    
          ? If this happens while testing with Jest, check out docs how to integrate AsyncStorage with it: https://react-native-async-storage.github.io/async-storage/docs/advanced/jest
    
        If none of these fix the issue, please open an issue on the Github repository: https://github.com/react-native-async-storage/react-native-async-storage/issues
    

    此類問題為需要mock對應模塊丐一,一般對應模塊文檔會有相關mock實現(xiàn)

React is not defined

  • React is not defined
    ReferenceError: React is not defined
        at Object.<anonymous> (/Users/daniel/Desktop/EcovacsHomeRN/__tests__/RobotMsgTabPages.test.js:6:39)
        at Promise.then.completed (/Users/daniel/Desktop/EcovacsHomeRN/node_modules/jest-circus/build/utils.js:391:28)
        at new Promise (<anonymous>)
        at callAsyncCircusFn (/Users/daniel/Desktop/EcovacsHomeRN/node_modules/jest-circus/build/utils.js:316:10)
        at _callCircusTest (/Users/daniel/Desktop/EcovacsHomeRN/node_modules/jest-circus/build/run.js:218:40)
        at processTicksAndRejections (node:internal/process/task_queues:96:5)
        at _runTest (/Users/daniel/Desktop/EcovacsHomeRN/node_modules/jest-circus/build/run.js:155:3)
        at _runTestsForDescribeBlock (/Users/daniel/Desktop/EcovacsHomeRN/node_modules/jest-circus/build/run.js:66:9)
        at run (/Users/daniel/Desktop/EcovacsHomeRN/node_modules/jest-circus/build/run.js:25:3)
        at runAndTransformResultsToJestFormat (/Users/daniel/Desktop/EcovacsHomeRN/node_modules/jest-circus/build/legacy-code-todo-rewrite/jestAdapterInit.js:170:21)
    
    

    此情況一般為在用react-test-renderer做組件測試時,在import renderer from 'react-test-renderer';之前需要import React from "react";

nativeBridge

  • Cannot read property 'rnConnection' of undefined
    TypeError: Cannot read property 'rnConnection' of undefined
    

    在測試文件中mock, NativeModules.Bridge = {rnConnection: jest.fn()}

SyntaxError

  •  SyntaxError: /Users/daniel/Desktop/EcovacsHomeRN/node_modules/@react-native/polyfills/error-guard.js: Unexpected token, expected "," (51:22)
      
          49 |     _globalHandler && _globalHandler(error, true);
          50 |   },
        > 51 |   applyWithGuard<TArgs: $ReadOnlyArray<mixed>, TOut>(
             |                       ^
          52 |     fun: Fn<TArgs, TOut>,
          53 |     context?: ?mixed,
          54 |     args?: ?TArgs,
    

    此為babel配置了@babel/plugin-syntax-typescript插件導致babel只做TS語法解析柠衍,而不做語法轉(zhuǎn)換拧略,在jest組件測試會報語法錯誤瘪弓。解決方案為使用@babel/plugin-transform-typescript或者 @babel/preset-typescript 同時支持語法解析和轉(zhuǎn)化.文檔地址:https://babel.dev/docs/en/babel-plugin-syntax-typescript

小試牛刀

  • 測試組件方法調(diào)用

    import React from "react";
    import RobotMsgTabPages from "../js/AppCommon/robot/RobotMessage/RobotMsgTabPages";
    import renderer from 'react-test-renderer';
    import MessageCenterHomePage from "../js/AppCommon/messageCenter/MessageCenterHomePage";
    import {NativeModules} from "react-native";
    import RobotEmptyView from "../js/AppCommon/robot/RobotMessage/CommonComponent/RobotEmptyView";
    import {SwipeListView} from "react-native-swipe-list-view";
    
    it('should ropRobotTabMsgList callback', function () {
    
        const state = {routeName : '', params: {did : '13',title : '123', hasUnreadMsg : true,item : '',changeUnread : true}}
        const getParam = params => {return true};
        const addListener = (params,callback) => {}
    
        const component = renderer.create(<RobotMsgTabPages navigation={{"state":state, "getParam":getParam, "addListener" : addListener}}/>).root;
        const swipeListView = component.findByType(RobotEmptyView);
        console.log(swipeListView.props.onPress)
    });
    
  • 測試接口調(diào)用

    import { ropRobotProductMsgList} from '../js/InternalModule/api/MessageCenterApi'
    // import {fetch} from "@react-native-community/netinfo";
    // jest.mock('../js/InternalModule/api/MessageCenterApi')
    
    beforeAll(() => {
        RopConstants.iotServerUrl = "dadadadad"
    })
    
    test('mock modules', async () => {
        // fetch.mockResolvedValue('ddad');
        // fetch.mockReturnValue('dadaad');
        // ropRobotProductMsgList.mockImplementation(() => {
        //     return new Promise((resolve, reject) => {
        //        reject('dadaad')
        //     });
        // })
        await expect(ropRobotProductMsgList({'a' : 'ddaq'})).rejects.toEqual('dadaad');
    });
    
?著作權歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市虑乖,隨后出現(xiàn)的幾起案子疹味,更是在濱河造成了極大的恐慌帜篇,老刑警劉巖,帶你破解...
    沈念sama閱讀 221,198評論 6 514
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異竟痰,居然都是意外死亡坏快,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 94,334評論 3 398
  • 文/潘曉璐 我一進店門柠并,熙熙樓的掌柜王于貴愁眉苦臉地迎上來臼予,“玉大人啃沪,你說我怎么就攤上這事粘拾。” “怎么了创千?”我有些...
    開封第一講書人閱讀 167,643評論 0 360
  • 文/不壞的土叔 我叫張陵缰雇,是天一觀的道長。 經(jīng)常有香客問我追驴,道長械哟,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 59,495評論 1 296
  • 正文 為了忘掉前任殿雪,我火速辦了婚禮暇咆,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘丙曙。我一直安慰自己爸业,他們只是感情好,可當我...
    茶點故事閱讀 68,502評論 6 397
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著耸黑,像睡著了一般。 火紅的嫁衣襯著肌膚如雪洲拇。 梳的紋絲不亂的頭發(fā)上男翰,一...
    開封第一講書人閱讀 52,156評論 1 308
  • 那天,我揣著相機與錄音,去河邊找鬼纤泵。 笑死,一個胖子當著我的面吹牛公荧,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播晤揣,決...
    沈念sama閱讀 40,743評論 3 421
  • 文/蒼蘭香墨 我猛地睜開眼盗扒,長吁一口氣:“原來是場噩夢啊……” “哼甸祭!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起校焦,我...
    開封第一講書人閱讀 39,659評論 0 276
  • 序言:老撾萬榮一對情侶失蹤房匆,失蹤者是張志新(化名)和其女友劉穎弦追,沒想到半個月后骗卜,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 46,200評論 1 319
  • 正文 獨居荒郊野嶺守林人離奇死亡俭嘁,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 38,282評論 3 340
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片薇缅。...
    茶點故事閱讀 40,424評論 1 352
  • 序言:一個原本活蹦亂跳的男人離奇死亡灸撰,死狀恐怖浮毯,靈堂內(nèi)的尸體忽然破棺而出鸟顺,到底是詐尸還是另有隱情,我是刑警寧澤兆沙,帶...
    沈念sama閱讀 36,107評論 5 349
  • 正文 年R本政府宣布库正,位于F島的核電站,受9級特大地震影響喷楣,放射性物質(zhì)發(fā)生泄漏罕伯。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 41,789評論 3 333
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望昂利。 院中可真熱鬧,春花似錦硬萍、人聲如沸祖屏。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,264評論 0 23
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至圾浅,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背混槐。 一陣腳步聲響...
    開封第一講書人閱讀 33,390評論 1 271
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人坑质。 一個月前我還...
    沈念sama閱讀 48,798評論 3 376
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子荤傲,可洞房花燭夜當晚...
    茶點故事閱讀 45,435評論 2 359

推薦閱讀更多精彩內(nèi)容