前端单元测试-jest入门实践

前言

单元测试是用来测试程序中一小块功能的,比如说一个函数、一个类。它能很显著地提高项目的代码质量,降低出现 Bug 的频率,并且也利于维护代码。

话虽如此,但是绝大多数人是不愿意去写单测的。。不过不影响,多学点不是坏事。

背景就不多做介绍,主要是实践。

正文

本次项目中,我用的框架是Jest。用法比较简单,这里简单介绍一下:

什么是单元测试?

单元测试(Unit Testing)又称为模块测试,是针对程序模块(软件设计的最小单位)来进行正确性检验的测试工作。

TDD:测试驱动开发

先编写测试用例,在测试用例的指导下去完善功能,当测试用例编写完并且都通过测试之后,相应的功能也就做完了。例如函数和组件库。但通常在代码发生变化的时候,测试用例也要进行相应的调整。

BDD:行为驱动开发

测试用例模拟用户的操作行为,通常在完成业务代码开发之后,以用户的操作为指导编写测试代码。当测试用例跑通之后,就可以认为系统的整体流程已经流畅。BDD的模式适用于平时的业务代码开发,因为业务的需求有可能变更频繁,但操作流程有可能不会变化,当业务代码发生变化的时候,可以使用原来的测试用例继续跑代码,节省了开发时间。
TDD负责方法类、独立组件的测试。BDD则负责整体业务模块的测试。

为什么需要单测?

现状:
缺乏意识:没有单测意识,很多代码没有单测
缺乏设计:模块缺乏设计,相互耦合,写单测困难
测试困难:升级公共 API,人工测试验证困难
测试不全面:部分代码分支众多,人工测试没办法覆盖所有分支

怎么衡量单测完善度?

单测完善程度的衡量用覆盖率来衡量

前端单元测试-jest入门实践_第1张图片

单测的意义

能力建设:一个具备开发经验的开发人员,基本上都会编写单元测试。即便不会,可以通过培训来快速达成。从学习曲线上看,单元测试很容易上手。
提升效率:能够通过 mock 数据,及早发现问题,而越早发现Bug,造成的浪费就会越小。
追求卓越:单元测试可以充当一个设计工具,它有助于开发人员去思考代码结构的设计,让代码更加有利于测试。
测试更全面:能够覆盖 QA 测试覆盖不到的情况,比如各种 if 分支、异常处理。
更有信心:升级公共 API 时,如果依赖这个 API 的所有代码单测都能通过,那我们对这次代码升级是更有信心的。

理念介绍到此结束,接下来是相关实践

1. 断言

在Jest中使用最多的就是断言库,我们可以用它来测试目标函数的输出是否与预期一致。

test("测试 2 + 2 = 4",()=>{
  expect(sum(2,2)).toBe(4)
})
test("测试函数返回对象为 {name: 'zhi'}",()=>{
  expect(getName()).toEqual({name:"zhi"})
})

更多可以查看官方文档

2. 异步代码测试

通常会有两种写法:

  • 回调函数
  • 函数返回promise

在测试异步代码的时候,通常返回的数据是不确定的,因此我们只需要测试异步代码是否正常返回数据即可。

对于回调函数,如果像同步函数一样测试,是没有办法获取正确的断言结果

export const funcA = (callback: (data: number) => void): void => {
  setTimeout(() => {
    callback(1);
  }, 1000);
};
​
test('funcA', () => {
  funcA((data) => expect(data).toEqual(2));
});
​

即使是这样,我们的单测也可以通过。

这是因为jest在运行完funcA后就直接结束了,不会等待setTimeout的回调,自然也就没有执行expect断言。正确的做法是,传入一个done参数:

test('funcA', (done) => {
  funcA((data) => {
    expect(data).toEqual(2);
    done();
  });
});

在回调执行完之后显式地告诉jest异步函数执行完毕,jest会等到执行了done()之后再结束,这样就能得到预期的结果了。

对于promise,只需要在test用例结束时,把Promise返回即可。

export const funcB = (): Promise => {
  return new Promise((resolve) => {
    setTimeout(() => {
      resolve(1);
    }, 1000);
  });
};
​
test('funcB', () => {
  return funcB().then((data) =>   expect(data).toEqual(1));
});
// 也可以使用await
test('funcB', async () => {
  const data = await funcB();
  expect(data).toEqual(1);
});

对于promise抛出异常,则需要使用 .catch 方法。这里有个需要注意的地方。添加 expect.assertions 来验证一定数量的断言被调用。 否则,一个fulfilled状态的Promise不会让测试用例失败。

test('the fetch fails with an error', () => {
  expect.assertionsassertions(1);
  return fetchData().catch(e => expect(e).toMatch('error'));
});

3. Mock

如果我们需要测试回调函数是否执行,可以用mock。在这里想基于实践介绍一下mock的用法。

场景1:一个react hooks使用了react-router的useLocation。我们想模拟这个有没有调用

import { renderHook, act } from '@testing-library/react-hooks';

let mockLocation = jest.fn(() => {
    return {
        search: "test"
    }
})

jest.mock("react-router-dom", () => {
    return {
        useLocation: () => mockLocation()
    }
})

// 使用

test("useTabChange", () => {
    const { result, rerender } = renderHook(() => useTabChange());
    expect(mockLocation).toBeCalled()
})

我们甚至可以给任何方法内部的函数模拟返回

例子2: 在业务中用到了一个'opa-fe-base'的库,我们需要模拟一下返回

import { renderHook, act } from '@testing-library/react-hooks';
let mockFn = jest.fn()
jest.mock("opa-fe-base", () => ({ storeAPI: { getEntity: () => mockFn() } }))
import useCountry from '../useCountry'
​
describe("useCountry", () => {
    test("useCountry to return SG", () => {
        const { result, rerender } = renderHook(() => useCountry())
        expect(result.current).toBe('SG')
        mockFn = jest.fn(() => {
            return { country: "ID" }
        })
        rerender()
        act(() => {
            expect(result.current).toBe('ID')
        })
      
    })
})

上述例子可以看到,我们模拟了'opa-fe-base'这个库返回了一个包含

storeAPI的对象,对象内部还有个mock的方法。我们可以通过改变mock方法的返回来判断返回是否达到预期。

注意:需要测试的函数import写在我们mock的下面

最近在写单测的时候发现window.URL.createObjectURL事件可以直接使用jest.fn进行模拟。但是document的事件却模拟不成功,查询文档发现可以使用jest.spyOn。

场景3:通过jest.spyOn 模拟document事件

const spyFn = jest.spyOn(document, 'createElement')
const mockRevoke = jest.fn();
​
describe("downloadBlobFile", () => {
    beforeEach(() => {
        window.URL.createObjectURL = jest.fn((blob) => {
            return blob
        })
        window.URL.revokeObjectURL = mockRevoke;
    })
    test("downloadBlobFile must be called", () => {
        downloadBlobFile({});
        expect(spyFn).toBeCalled()
        expect(mockRevoke).toBeCalled();
    })
​
    test("download createObjectUrl use", () => {
        downloadBlobFile("test");
        expect(window.URL.createObjectURL.mock.calls[0][0]).toEqual('test')
    })
})

4. 快照

帮助我们确保在维护代码的过程中不会对组件的 UI 进行改变。

expect(组件实例).toMatchSnapshot()

在第一次执行时会生成快照,之后就会去对比每次的快照是否一样。

5. 推荐一个vs code jest 插件

Jest Runner

前端单元测试-jest入门实践_第2张图片

这个插件可以在我们的test文件中渲染出按钮,帮助我们单独的运行或者调试某一个test或者describe。不需要全局运行。提升效率。

前端单元测试-jest入门实践_第3张图片

最后

单测的编写是有意义的,但是往往在工作中囿于业务,没有机会写,且行且珍惜~~

你可能感兴趣的:(前端单元测试-jest入门实践)