React Native测试小调研

测试

随着代码量的增长 细小的错误和一些没能够预料到的边界条件能够造成大的问题
bug可以造成糟糕的用户体验 甚至是业务的损失
一个有效的监测方式是在发布前对代码进行测试

为什么要做测试

是人都会犯错。测试可以帮助我们发现问题并验证代码的正确与否。测试可以保证代码在新增功能、重构代码或者升级了依赖后继续正常工作

测试的价值远比我们意识到的要多很多。最好的修复bug的方式就是写一个失败的用例来报错出来 在修复bug的过程中重新跑测试用例,如果它通过了意味着bug修复了

测试用例还能作为团队的文档,帮助未见过代码库的人来理解现有代码是如何工作的。
更多的自动化测试意味着花费在人工测试的时间上更少,从而释放出宝贵的时间

一. 策略

1. 单元测试

1.1 概念

单元测试(unit testing),是指对软件中的最小可测试单元进行检查和验证。
最小的可测但愿可以是一个模块、一个函数或者一个类

单元测试从长期来看,可以提高代码质量,减少维护成本,降低重构难度。但是从短期来看,加大了工作量,对于进度紧张的项目中的开发人员来说,可能会成为不少的负担

1.2 哪些代码需要有单元测试覆盖

  1. 逻辑复杂的
  2. 容易出错的
  3. 不易理解的,即使是自己过段时间也会遗忘的,看不懂自己的代码,单元测试代码有助于理解代码的功能和需求
  4. 公共代码。比如自定义的所有http请求都会经过的拦截器;工具类等。
  5. 核心业务代码。一个产品里最核心最有业务价值的代码应该要有较高的单元测试覆盖率。

1.3 何时写

  1. 在具体实现代码之前,这是测试驱动开发(TDD)所提倡的;

  2. 与具体实现代码同步进行。先写少量功能代码,紧接着写单元测试(重复这两个过程,直到完成功能代码开发)。其实这种方案跟第一种已经很接近,基本上功能代码开发完,单元测试也差不多完成了。

  3. 编写完功能代码再写单元测试

1.4 为什么要写单元测试

  • 修改代码时,可以很快的验证正确性
  • 重构代码时,只要原有测试用例全部通过,则代码重构完成,当然需要原有测试用例设计合理
  • 当功能持续增加时,如果影响到原有功能,可以很快发现

1.5小结

  • 单元测试可以有效地测试某个程序模块的行为,是未来重构代码的信心保证。
  • 单元测试的测试用例要覆盖常用的输入组合、边界条件和异常。
  • 单元测试代码要非常简单,如果测试代码太复杂,那么测试代码本身就可能有bug。
  • 单元测试通过了并不意味着程序就没有bug了,但是不通过程序肯定有bug。

集成测试

接下来是集成测试。集成测试是一组不同的单元被当作一个整体来进行测试的阶段。

又称组装测试,即对程序模块采用一次性或增值方式组装起来,对系统的接口进行正确性检验的测试工作。整合测试一般在单元测试之后、系统测试之前进行。实践表明,有时模块虽然可以单独工作,但是并不能保证组装起来也可以同时工作。

功能测试

对系统的功能及性能的总体测试。

是一种黑盒测试的类型,其主要关注于用户需求和交互。功能测试会从整体上覆盖所有的底层软件,所有的用户交互和应用。

测试金字塔

在金字塔模型之前,流行的是冰淇淋模型。包含了大量的手工测试、端到端的自动化测试及少量的单元测试。造成的后果是,随着产品壮大,手工回归测试时间越来越长,质量很难把控;自动化case频频失败,每一个失败对应着一个长长的函数调用,到底哪里出了问题?单元测试少的可怜,基本没作用。

冰淇淋模型

)

Mike Cohn 在他的着作《Succeeding with Agile》一书中提出了“测试金字塔”这个概念。这个比喻非常形象,它让你一眼就知道测试是需要分层的。它还告诉你每一层需要写多少测试。 测试金字塔本身是一条很好的经验法则,我们最好记住Cohn在金字塔模型中提到的两件事:

测试金字塔


是目前比较流行的一种设计自动化测试的思路,核心观点如下:

  1. 越下层的测试效率越高, 覆盖率也越高, 开发维护成本越低
  2. 更上层的测试集成性更好, 但维护成本更高
  3. 大量的Unit测试, 少量的集成测试和更少的E2E测试是比较合理的平衡点(Google在blog中推荐70/20/10的测试用例个数比例)

React Native

端到端测试 End-to-End Tests

从用户的视角验证app功能的正确性
这是在编译app包之后,对安装包进行测试,此时不再是考虑React组件 React Native API或者是Redux store或者业务逻辑。
E2E测试库允许找出屏幕上的控件 并且可以与其进行交互

E2E测试可以给app最大的信心,与其它测试相比

  • 编写测试用例需要花费更多的时间
  • 运行更慢
  • 更容易不稳定("不稳定"是指在没有任何更改的情况下随机通过和失败的测试)

在React Native中, Detox是一个流行的框架,因为它是为React Native应用量身定制的。在iOS和Android应用中,Appium也是一个流行框架

组件测试

React组件负责渲染app,用户直接和组件进行交互。及时app业务逻辑有很高的测试覆盖率并且是正常工作的,没有组件测试,可能还是会给用户展示一个有问题的页面。组件测试可以分成单元测试和集成测试,它们是React Native核心部分,我们分别来讲
对于React组件 有两个东西需要测试

  • 交互:确保组件和用户交互时是正常运行的(比如 当用户点击某个按钮时)
  • 渲染:确保React组件显示是正确的(比如 按钮在页面的外观以及位置)

比如,想测试按钮的显示以及点击时间是否正确被处理
React的Test Render可以将React组件转化成纯Javascript对象,不需要依赖DOM或者是移动手机环境
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等)、覆盖统计等 node.js & browser 19.9k
Jasmine 内置断言expect, 需要全局声明,且需要配置,相对来说使用更复杂、不够灵活。 node.js & browser 15k
cypress 提供可交互页面 支持Mac、Windows、Linux browser 24.3k

Jest

FaceBook出品的前端测试框架,适合用于React和React Native的单元测试。

有以下几个特点:

  • 简单易用:易配置,自带断言库和mock库。

  • 快照测试:能够创造一个当前组件的渲染快照,通过和上次保存的快照进行比较,如果两者不匹配说明测试失败。

  • 测试报告:内置了Istanbul,通过一定配置可以测试代码覆盖率,生成测试报告。

Enzyme

Enzyme是AirBnb开源的React测试工具库,通过一套简洁的api,可以渲染一个或多个组件,查找元素,模拟元素交互(如点击,触摸),通过和Jest相互配合可以提供完整的React组件测试能力。

初始化配置

jest --init

常用方法

  • describe:创造一个块,将一组相关的测试用例组合在一起

  • test:也可以用it,测试用例

  • expect:使用该函数断言某个值

生命周期

beforeAll
afterAll

beforeEach
afterEach

当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执行顺序

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();

数字相关的断言

  • 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);

浮点数

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

字符串

可使用正则表达式

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

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

数组

  • 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 函数

  • 替换真实的函数实现
  • 检测函数调用以及传入的参数
  • 测试时配置返回值
  • 监测实例的初始化

1. 在测试代码中创建一个mock函数

  • Mock返回值
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
  • Mock Modules
// 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));
});
  • Mock 实现
// 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下实现一份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测试是传统测试的一个补充而非替代

如何做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().toJSON();
      expect(tree).toMatchSnapshot();
    });
    
  • Demo result

    // __tests__/__snapshots__/Intro-test.js.snap
    exports[`Intro renders correctly 1`] = `
    
      
        Welcome to React Native!
      
      
        This is a React Native snapshot test.
      
    
    `;
    
  • .snap如何产生
    .snap

    组件快照是一串类似JSX的字符串,由Jest内置的React序列化器生成
    将React组件树转换成方便人阅读的形式,也就是说组件快照是是组件渲染结果的文本形式

    第一次运行测试的时候会将当前渲染结果生成snapshot 之后每次运行测试都是与此次的snap文件进行比较

  • CI里是否可以自动生成

    Jest执行测试是不会自动生成 通过--updateSnapshot来强制生成

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

Jest使用pretty-format来生成可读的snapshots文件,使用 snapshot-diff来比较组件snapshot文件的差异

Snapshot 测试失败

snapshoterror
  1. 界面出问题了
  2. 新的snapshot是所希望的结果 则需要更新snapshot

此时测试结果里会展示新旧.snap的不同之处

异步测试

简单的API测试

测试覆盖率

coverage

class Mock

  • 实例方法mock

  • 类方法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)
  })
})

单纯网络API功能测试

  • SoapUI
  • JMeter
  • PostMan
  • 自己写代码

app内mock参考

测试结果的可视化

jest-html-reporter

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

默认输出结果在./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时控制台有log, 如何取消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)$': '/__tests__/__mocks__/assetFileTransformer.js',
    },
    

参考链接

  • Testing with Jest Snapshots: First Impressions

  • Snapshot Testing: Use With Care

  • 单元测试系列一-为什么要写单元测试,何时写,写多细

  • 对 React 组件进行单元测试

  • https://www.zhihu.com/question/28729261/answer/1058317111

  • Testing

你可能感兴趣的:(React Native测试小调研)