入门前端自动化测试-jest-基础

jest基础

  • jest官网

  • 匹配器

  • 命令行工具

  • 测试异步

  • 钩子函数

  • mock

  • 快照

  • dom

匹配器

jest默认环境是node

如果想在jest环境使用esmodule,需要借助@babel/core转化工具, @babel/preset-env指明如何转化

{
    "presets": [
        [
            "@babel/preset-env",
            {
                "targets": {
                    "node": "current"
                }
            }
        ]
    ]
}
只需要一个项目中安装jest,然后执行 npx jest --watch,jest就会自动将所有的.test.js结尾的文件中的测试用例执行。

jest提供了很多匹配器,最基本的使用

test('测试加法', ()=>{
     // toBe 类似 Object.is(a,b),适用于普通值
    expect(10).toBe(10)
 })


 test('测试内容', ()=>{
   // 匹配对象使用toEqual,递归遍历使用toBe, 加上.not就是取反
   expect({a: 1}).toEqual({a: 1})
 })

如toBe, toEqual,等等还有很多。当执行npx jest --watch的时候,jest就会执行这些测试用例并返回结果。

PASS  ./9.test.js
  √ 测试加法 (2ms)测试内容 (1ms)

Snapshot Summary
 › 1 snapshot file obsolete from 1 test suite. To remove it, press `u`.
   ↳   • __snapshots__学习\demo.test.js.snap

Test Suites: 1 passed, 1 total
Tests:       2 passed, 2 total
Snapshots:   1 file obsolete, 0 total
Time:        0.987s
Ran all test suites related to changed files.

测试通过。

还有更多的适配器如:

 expect(10).toBeGreaterThan(9) //大于9
 expect( 0.1 + 0.2).toBeCloseTo(0.3) //解决精度问题
 expect('abcd').toMatch(/ab/) //匹配字符串
 expect([1,2]).toContaine(1) //数组或者集合是否包含某个数
 expect(()=>{throw new Error('123')}).toThrow() //抛出异常, 没有toThrow不会抛出异常
 expect(()=>{throw new Error('123')}).toThrow('123') //是否抛出123异常,是的话测试通过

更多适配器可以查看官网。

命令行工具

Test Suites: 1 passed, 1 total
Tests:       1 passed, 1 total
Snapshots:   1 file obsolete, 0 total
Time:        2.302s, estimated 3s
Ran all test suites related to changed files.

Watch Usage: Press w to show more.

执行完后,下面会提示输入W显示更多指令

Watch Usage
 › Press a to run all tests.
 › Press f to run only failed tests.
 › Press p to filter by a filename regex pattern.
 › Press t to filter by a test name regex pattern.
 › Press u to update failing snapshots.
 › Press q to quit watch mode.
 › Press Enter to trigger a test run.
  • a执行所有测试 默认 jest --watchAll 就是打开了a模式

  • f只测试失败的测试

  • p 根据文件名字正则表达式,匹配更改的test

  • t 根据文件名字,过滤某些test

  • u 更改失败的快照(后面会讲)

  • o 配合git,只执行有修改的文件 默认–watch就是打开了。

测试异步

// 默契请求的方法
const fetchData = () =>
  new Promise((resolve, reject) => {
    setTimeout(() => {
        resolve({ success: true });
    }, 2000);
  });

export { fetchData };

因为默认情况下,Jest 测试在执行结束后完成。

test('the data is peanut butter', () => {
  fetchData().then(res=>{
      expect(res).toEqual({success: true})
  })
});

那么其实不管输入什么,该test就会执行正确。

正确测试异步方法:

1 done

test("fetchData 返回结果为{success: true}", (done) => {
  fetchData()
    .then((res) => {
      expect(res).toEqual({ success: true });
      done();
    })
    .catch((err) => {
      done(err);
    });
});

只有调用done,才会正确测试该test。

2 promise

正常使用promise.then,但是一定要return。而且可以使用expect.assertions(1)表示至少执行一次expect。

因为有些场景,我们只俘获了catch,并无俘获then,那么Promsie成功的时候,这个测试将会没有执行。所以需要这个来确定至少执行一次。否则失败。

test("fetchData 返回结果为{success: true}", () => {
  expect.assertions(1) //至少执行一次expect,如果没跑也会报错
  return fetchData()
    .then((res) => {
      expect(res).toEqual({ success: true });
    })
    .catch((err) => {
      expect(err).toMatch("error");
    });
});

3 resolves/reject

// 可以.resolves在您的期望语句中使用匹配器,Jest 将等待该承诺解决。如果 promise 被拒绝,测试将自动失败。
test("fetchData 返回结果为{success: true}", () => {
  return expect(fetchData()).resolves.toMatchObject({
    success: true,
  });
  // 期望一个 Promise 被拒绝,使用.rejects匹配器。如果promise被拒绝,就执行,如果promise成功执行,则该jest默认失败
  return expect(fetchData()).rejects.toThrow()
});	

4 asycn/await

记得错误处理

test("fetchData 返回结果为{success: true}", async () => {
  try {
    const data = await fetchData();
    expect(data).toEqual({
      success: true,
    });
  } catch (err) {
    // 抛出异常
    expect(err).toThrow()
  }
});

钩子函数

jest在执行test用例的时候,提供了很多钩子,用来初始化一些东西或者做一些销毁工作,如执行前调用,执行后调用等等。如

beforeAll(()=>{
    console.log('beforeAll');
})

afterAll(()=>{
    console.log('afterAll');
})

他们分别会在所有的钩子执行前执行和执行后执行。

beforeEach(()=>{
    console.log('beforeEach');
})

afterEach(()=>{
    console.log('afterEach');
})

beforeEach和afterEach分别会在每个test执行之前和之后运行。

分组

相同的意义的测试用例可以写在同一个分组中,相当于ts的namespace,jest提供了describe

describe("测试加法", () => {
  beforeAll(() => {
    console.log("内部 eforeAll");
  });
  afterAll(() => {
    console.log("内部 afterAll");
  });
  afterEach(() => {
    console.log("内部 afterEach");
  });
  beforeEach(() => {
    console.log("测试加法 beforeEach");
    counter = new Counter();
  });

  test("test Counter addOne", () => {
    counter.addOne();
    expect(counter.number).toBe(1);
    console.log("addOne");
  });
});

如,describe内部的钩子,只会在内部执行。而整个test文件,可以看作是外层包装了一个descrbie(‘xx’,()=>{// test文件的内容})

那么desceibe内部的钩子和外部的钩子,执行顺序是怎样呢?

以下面的为例子

beforeAll(() => {
    console.log("外部 beforeAll");
  });
  
  afterAll(() => {
    console.log("afterAll");
  });
  beforeEach(() => {
    console.log("外部 beforeEach");
  });
  afterEach(() => {
    console.log("外部 afterEach");
  });
  describe("测试加法", () => {
    beforeAll(() => {
      console.log("内部 eforeAll");
    });
    afterAll(() => {
      console.log("内部 afterAll");
    });
    afterEach(() => {
      console.log("内部 afterEach");
    });
    beforeEach(() => {
      console.log("测试加法 beforeEach");
    });
  
    test("test Counter addOne", () => {
      console.log("descrbie内部的测试 addOne");
    });
  });

执行顺序就是

外部 beforeAll
内部 beforeAll
外部 beforeEach
测试加法 beforeEach
descrbie内部的测试 addOne
内部 afterEach
外部 afterEach
内部 afterAll
外部 afterAll

技巧就是:外部beforeAl > 内部beforeAll > 外部beforeEach > 内部beforeEach > test执行 > 内部afterEach > 外部 afterEach > 内部 afterAll > 外部 afterAll

mock

jest提供了mock的功能,可以模拟请求数据的函数,模拟返回的数据。

其中

jest.fn 返回一个mock函数,1 捕获函数的调用和返回结果, 2 可以自由地设置返回结果 3模拟axios请求返回特定数据
test("测试", () => {
  const func = jest.fn((a) => {
    return a + 4;
  });

  // func.xxx有很多语法。
  // func.mockImplementtation((a)=>{return a+4})跟jest.fn(fn)的fn一样,函数执行的时候的逻辑
  // func.mockImplementationOnce(()=>{}) 只执行一次。

  // mockReturnValue('a') 每次调用都返回a
  func.mockReturnValueOnce("11111").mockReturnValueOnce("2222"); //第1,2次执行func的时候,返回了1111, 2222
  func(1);
  func(2);
  func(3);
  func(4);

  // func是否被执行过
  //expect(func).toBeCalled()
  //expect(func.mock.calls[0]).toEqual([1])
  expect(func.mock.results);
  console.log(func.mock);

  /** 
     console.log(func.mock);
     {可以通过func.mock.calls.length判断调用多少次,以及传参, fn.results判断返回值
      calls: [ [ 1 ], [ 2 ], [ 3 ], [ 4 ] ],
      instances: [ undefined, undefined, undefined, undefined ], func运行的时候,this指向
      invocationCallOrder: [ 1, 2, 3, 4 ],
      results: [
        { type: 'return', value: '11111' },
        { type: 'return', value: '2222' },
        { type: 'return', value: 7 },
        { type: 'return', value: 8 }
      ]
    }

     
});

func执行的时候,实际上是jest.fn()在执行,func.mock提供了很多内部,包括调用多少次,每次的入参,每次的返回等等。并且可以通过func.mockReturnValueOnce(‘xx’)默契一次func调用的时候的返回。

jest.mock可以改变函数的实现
// 3 改变函数的内部实现
import axios from 'axios'
jest.mock('axios')
test.only('测试 axios',async()=>{
    axios.get.mockResolvedValue({data: '123'}) //模式请求返回数据
    console.log('axios', axios);
    await axios.get('xxx').then(data=>{
        console.log(data);
        expect(data).toEqual({data: '123'}) //断言
    })
})
jest.mock(‘axios’)可以将axios上的所有属性变成jest.fn。打印出来的axios是
 console.log 9.test.js:5
    axios <ref *1> [Function: wrap] {
      _isMockFunction: true,
      getMockImplementation: [Function (anonymous)],
      mock: [Getter/Setter],
      mockClear: [Function (anonymous)],
      mockReset: [Function (anonymous)],
      mockRestore: [Function (anonymous)],
      mockReturnValueOnce: [Function (anonymous)],
      mockResolvedValueOnce: [Function (anonymous)],
      mockRejectedValueOnce: [Function (anonymous)],
      ....
    }

axios已经被改造了。可以通过axios.get.mockResolvedValue({data: ‘123’})模式get请求返回的数据,测试如下:

 PASS  ./9.test.js
  √ 测试 axios (10ms)

  console.log 9.test.js:6
    { data: '123' }

Snapshot Summary
 › 1 snapshot file obsolete from 1 test suite. To remove it, press `u`.
   ↳   • __snapshots__学习\demo.test.js.snap

Test Suites: 1 passed, 1 total
Tests:       1 passed, 1 total
Snapshots:   1 file obsolete, 0 total
Time:        2.568s, estimated 3s
Ran all test suites related to changed files.

测试通过,get请求返回的data就是{data: ‘123’}

自己改造函数

jest执行测试用例的时候,并不希望真正的发送请求。所以可以通过jest.mock模拟。

假设fetchData是代码中真正发送请求的函数,我们想验证这个函数的正确性,又不想发送请求。可以在根目录下创建__mock__文件夹,命名相同的文件名字,如demo.js然后实现

// __mock__/demo.js
const fetchData = () =>
  new Promise((resolve, reject) => {
    setTimeout(() => {
        resolve({ success: true });
    }, 2000);
  });

export {fetchData  }

接着引入fetchData的时候

import {fetchData} from './demo'
jest.mock('./demo') //该语句的用途是去__mock__下面找demo文件,然后看有没有导出的方法,这样上一条语句导入的fetchData就变成了从__mock__/demo.js中导入的我们自己模拟的fetchData,然后去执行。
// 有多个引入的时候,可以通过const {aa} = jest.requireActual('./demo'),这样,aa就回去真正的demo文件下引入了。
test('xxx',()=>{
    return fetchData().then(res=>.....)
})

其次,如果jest.mock(’./demo’)找不到__mock__下的文件的时候,jest内部就会自动模拟,如上面的axios。

mock模拟类
// util.js
class Util {
    a(){
        setTimeout(()=>{
            console.log(123);
        }, 3000)
    }
    b(){
        setTimeout(()=>{
            console.log(123);
        }, 5000)
    }
}
export default  Util

// test.js
import Util from './util'
jest.mock('./util') //因为我们没有在__mock__下定义该文件, jest.mock发现util是一个类,就自动把类的构造函数和方法变成Jest.fn()
// const Util = jest.fn()
// Util.prototype.a = jest.fn()
// Util.prototype.b = jest.fn()

我们也可以自己定义

// __mock__/util.js
 const Util = jest.fn()
 Util.prototype.a = jest.fn()
 Util.prototype.b = jest.fn()

这样jest.mock(’./util’)就会来这里找。

甚至可以

// test.js
jest.mock('./util', ()=>{
 const Util = jest.fn()
 Util.prototype.a = jest.fn()
 Util.prototype.b = jest.fn()
 return Util
})

作为第二个参数,在里面定义并且返回。这就是jest.mock的用法。

定时器

除了可以模拟方法,数据,还可以模拟定时器,因为真正的测试不可能等代码中所有的定时器都执行完,那会花费大量的时间。

  • jest.useFakeTimers(); //遇到setTimoeut等定时器就是用模拟的timers
  • 在调用有timer的函数的时候加上jest.runAllTimers(),它会立即将timer立即完成。
  • runAllTimers会将所有的timer都立即完成,而runOnlyPedingTimers(),将当前处于队列中的timer完成。
  • jest.advanceTimersByTime(3000) 让时间快进3s
const fetchData = () =>
  new Promise((resolve, reject) => {
    setTimeout(() => {
      resolve({ success: true });
    }, 5000);
  });

// 多个test,每个执行前都初始化,每个test都隔离。定时器之间就不会相互影响。
beforeEach(()=>{
  jest.useFakeTimers(); //遇到setTimoeut等定时器就是用模拟的timers
})

afterEach(() => {
  // 结束后需要手动清理
  jest.useRealTimers();
});

如上,在每一个test执行之前,就需要初始化使用假的timer。然后结束的时候需要手动清理。

test("模拟定时器", (done) => {
  fetchData()
    .then((res) => {
      expect(res).toEqual({ success: true });
      done();
    })
    .catch((err) => {
      done(err);
    });
  jest.runAllTimers() //将所有定时器立即执行完,无需等待,但是如果遇到定时器内嵌定时器的,会将内嵌的定时器也完成
  jest.runOnlyPedingTimers() // 只执行当前在等待队列的定时器,内嵌的定时器不会立即完成。
  jest.advanceTimersByTime(10000); //快进10s,可以定义多个。
});

快照

快照,顾名思义,就是现在拍照,然后过一阵时间再拍照,对比两个的差异。jest的快照也是如此

假设有个方法

const generateConfig = () => {
  return {
    server: "http://localhost",
    port: 8080,
    //time: new Date()
  };
};

传统的测试

test('测试generateConfig',()=>{
    expect(generateConfig()).toEqual({
        server: 'http://localhost',
        port:8080
    })
})

快照测试

test('测试generateConfig',()=>{
     // 匹配快照
     expect(generateConfig()).toMatchSnapshot({
         time: expect.any(Date)
     });
 })

第一次执行这个test的时候,会在根目录下创建_snapshots_文件,保存着你的快照

// __snapshots__/demo.test.js.snap 快照文件

// Jest Snapshot v1, https://goo.gl/fbAQLP

exports[`测试generateConfig 1`] = `
Object {
  "port": 8080,
  "server": "http://localhost",
}
`;

第二次执行的时候,就会跟之前保存的快照进行比较。如果不同就报错。

但是如上,time: new Date()等动态数据怎么执行都不可能跟上次一样,所以jest提供了参数,如

expect(generateConfig()).toMatchSnapshot({
         time: expect.any(Date)
     });

只要time匹配到一个Date的类型就可以。

此外,如果遇到不一样的快照,可以输入命令行工具

u 可更新快照全部
i 交互式的一个一个快照进行更新
toMatchInlineSnapshot行内快照,即不生成snapshot文件夹,而是将快照内嵌在test.js文件中,需要搭配安装[email protected]。如
test("测试generateConfig", () => {
  // 匹配快照
  expect(generateConfig()).toMatchInlineSnapshot(
    {
      time: expect.any(Date)
    },
  );
});

第一次执行之后

test("测试generateConfig", () => {
  // 匹配快照
  expect(generateConfig()).toMatchInlineSnapshot(
    {
      time: expect.any(Date)
    },
    // 执行test生成的快照
    `
    Object {
      "port": 8080,
      "server": "http://localhost",
      "time": Any,
    }
  `
  );
});

这就是快照的基本用法。

dom

import * as jsdom from 'jsdom'
var $ = require('jquery')(new jsdom.JSDOM().window);
const addDivToBody = () => {
  $("body").append("
123
"
); }; // node不具备dom,而jest在node环境下自己模拟了一套dom的api, jsdom test("测试 addDivToBody", () => { addDivToBody(); addDivToBody(); expect($("body").find('div').length).toBe(2) });

jest在node环境下自己模拟了一套dom的api, jsdom,所以直接使用dom的一些操作。

你可能感兴趣的:(运维,前端,自动化,typescript)