前端-自动化测试react项目-TDD

TDD

何为tdd (测试驱动的开发)
  • 流程: 1 编写测试用例 2 运行测试,测试用力无法通过测试 3 编写代码,使测试用例通过测试 4 优化代码,完成开发。 5 新增功能,重复以上步骤。
  • 以测试为驱动流程的开发
  • 好处: 1 长期减少回归bug 2 代码指令更好(组织,可维护性) 3 测试覆盖率高 4 错误测试代码不高
基本环境配置

脚手架create-react-app已经内置了jest。自己搭配的项目需要安装,

yarn add jest  ts-jest babel-jest @types/jest -D

初始化git,使用git管理项目。
然后在项目下面配置jest.config.js文件,
在这里插入图片描述
在这里插入图片描述
执行yarn test的时候,jest就会通过文件的配置进行测试。
因为我们是react项目,所以需要使用Enzyme。

Enzyme 加强jest测试React组件的功能

enzyme 官网https://enzymejs.github.io/enzyme/

  • 编写最基本的测试用例
test("renders without crashing", () => {
  const div = document.createElement("div");
  // 测试App的挂载
  ReactDOM.render(<App />, div);
  const container = div.getElementsByClassName('app')
  console.log('container', container.length);
  expect(container.lenght).toBe(2)
});

如上,通过ReactDOM挂载App后,可以判断app是否被挂载上去。

  • 像这种简单的测试组件的挂载,比较简单,但是单元测试的时候,希望可以测试组件里的状态等等,那么就比较麻烦了,Enzyme提供了这些功能。
    在这里插入图片描述
// 安装依赖
yarn add enzyme enzyme-adapter-react-16 jest-enzyme @types/enzyme  @types/enzyme-adapter-react-16 -D
// 测试组件的状态等,就得使用enzyme来测试
import {  shallow }from 'enzyme
test("renders without crashing", () => {
  // 对App做一层浅渲染,shallow只对一个组件做测试的时候使用,并不关心他的子组件。
  // 还可以使用mount,mount会把子组件也渲染出来,做集成测试的时候适合。
  const wrapper = shallow(<App />); //浅渲染App渲染到wrapper
  const continer = wrapper.find('[data-test="app"]');
  // wrapper.find(selector) 当前包装器的渲染树中查找与提供的选择器匹配的每个节点,如类选择器,id选择器等等。
  // 用了enzymen后可以安装jest-enzyme使用其他的匹配器。
  (expect(continer) as any).toExist();
  (expect(continer) as any).toHaveProp("title", "haha,test");
});

如上,shallow只会对App测试的时候使用,他不关心他的子组件。
也可以使用mount,moUnt会将子组件也渲染出来,做集成测试的时候适合。
通过shallwo渲染的组件wrapper用法很像jq,如上就是判断App组件是否存在,并且存在特定的属性。
可以看到wrapper长这样

<div className="app1" title="haha,test" data-test="app">
        <TodoList />
      </div>

还可以使用mount结合快照的形式,

//使用Mount结合快照
test.only("mounter snapshot", () => {
  const wrapper = mount(<App />); //浅渲染App渲染到wrapper
  console.log("wrapper", wrapper.debug());
  expect(wrapper).toMatchSnapshot(); //第一次会生成快照,第二次会比对,适合一些Ui不能随便改动的场景、
});

如上,会生成快照,如果第二次App的ui改动了,就会报错,看mount帮我们渲染出来的wrapper

<App>
        <div className="app1" title="haha,test" data-test="app">
          <TodoList>
            <div>
              <Headers onAddItem={[Function (anonymous)]}>
                <div className="Header">
                  <input type="text" data-test="input" value="" onChange={[Function: onChange]} onKeyUp={[Function: onKeyUp]} />
                </div>
              </Headers>
              <div>
                ------------
              </div>
            </div>
          </TodoList>
        </div>
      </App>

可以看到,把子组件也渲染出来了。
ui快照保存
在这里插入图片描述
这就是enzyme的基本用法了。

TDD开发todoList

效果就是输入框输入东西,在List中显示。
准备一个Header组件,用来输入内容。然后显示到List组件去。
那么测试用例应该如下:

test("header 组件包含一个input框", () => {
// 单独测试一个组件,所以使用shallow
  const wrapper = shallow(<Headers/>)
  const inputElem = wrapper.find("[data-test='input']")
  expect(inputElem.length).toBe(1)
});

it("header组件 input框初始化应该是''", () => {
  const wrapper = shallow(<Headers/>)
  const inputElem = wrapper.find("[data-test='input']")
  expect(inputElem.prop('value')).toEqual('')
})


it("header组件 input框内容,当用户输入时,会跟随变化''", () => {
  const wrapper = shallow(<Headers/>)
  const inputElem = wrapper.find("[data-test='input']") //找到输入框
  const userInput = '学习jest'
  // simulate触发事件
  inputElem.simulate('change', {
    target: {
      value: userInput
    }
  })
  // 数据改变
  const newInputElm =  wrapper.find("[data-test='input']")
  expect(newInputElm.prop('value')).toBe(userInput)
})


// 改变内容并且输入回车
test("header组件 input框输入回车的时候,如果input无内容,无操作", () => {
  const fn = jest.fn()
  const wrapper = shallow(<Headers onAddItem={fn}/>) // 传入回调函数
  const inputElem = wrapper.find("[data-test='input']")
  const userInput = '学习jest'
  // simulate触发事件
  inputElem.simulate('change', {
    target: {
      value: userInput
    }
  })
  inputElem.simulate('keyUp', {
   keyCode: 13 //回车事件
  })
  // 按下回车键,fn被调用,并且value被置为''
  expect(fn).toHaveBeenCalled()
  const newInputElm =  wrapper.find("[data-test='input']")
  expect(newInputElm.prop('value')).toBe('')
})

通过上面的测试用来来写组件,应该是这样的。

import React from "react";

interface Props {
  onAddItem?: (v: string) => void;
}

function Headers({ onAddItem }: Props) {
  const [value, setValue] = React.useState("");

  return (
    <div className="Header">
      <input
        type="text"
        data-test="input"
        value={value}
        onChange={(e) => {
          setValue(e.target.value);
        }}
        onKeyUp={(e) => {
          if (e.keyCode === 13) {
            onAddItem && onAddItem(value);
            setValue("");
          }
        }}
      />
    </div>
  );
}

export { Headers };

接着编写List组件内容。
先编写测试用例

import React from "react";
//import ReactDOM from "react-dom";

import Enzyme, { shallow, mount } from "enzyme";
import Adapter from "enzyme-adapter-react-16";
import TodoList from '../../index'
Enzyme.configure({adapter: new Adapter()})

初始化列表应该为空

test("toDoList 组件初始化列表为空", () => {
  const wrapper = shallow(<TodoList/>)
  const todoListItems = wrapper.find("[data-test='list-item']")
  expect(todoListItems.length).toBe(0)
});

lists组件应该给Header传递一个onAddItem方法

test("toDoList 组件应该给Headers传递一个onADdItem方法", () => {
    const wrapper = shallow(<TodoList/>)
    const Headers = wrapper.find('Headers')
    expect(Headers.length).toBe(1)
    // headers组件应该有一个Props为onAddItem
    expect(Headers.prop('onAddItem')).toHaveLength(1)
  });

当input回车键按下的时候,list组件应该新增内容

test.only("当Headers回车的时候, todoListItem应该新增内容", () => {
    const wrapper = shallow(<TodoList/>)
    const Headers = wrapper.find('Headers')
    const addFunc = Headers.prop('onAddItem')
    typeof addFunc === 'function' && addFunc('哈哈哈哈') //调用headers组建的addFunc
    const todoListItems = wrapper.find("[data-test='list-item']")
    expect(todoListItems.length).toBe(1)
    console.log('todoListItems', JSON.stringify(todoListItems));
    
  });

先这三个逻辑,然后根据测试用例开发组件

import React, { useState, useCallback } from "react";

import { Headers } from "./componnets/Headers";

function TodoList() {
  const [item, setItems] = useState<string[]>([]);

  const onAddItem = useCallback((e) => {
    setItems((pre) => {
      return [...pre].concat(e);
    });
  }, []);

  return (
    <div>
      <Headers onAddItem={onAddItem} />
      <div>------------</div>
      {item.map((ctem) => {
       return  <div data-test="list-item" key={ctem}>{ctem}</div>;
      })}
    </div>
  );
}

export default TodoList;

测试代码覆盖率

jest提供了测试代码覆盖率的问题:在package.json添加这句。

"covery": "jest --coverage --watchAll=false",

执行过后会成成这样一份文件供你分析,可以看到两个主要的组件的覆盖率是100%,但是
在这里插入图片描述
Header组建的分支是75%,也就是说有if else没被俘获的判断,如在这里插入图片描述
可以直接点进去打开查看是什么原因;
这里少了个else的判断,所以需要添加多一个测试用例

test("header组件 input框按键不是回车的时候", () => {
  const fn = jest.fn();
  const wrapper = shallow(<Headers onAddItem={fn} />);
  const inputElem = wrapper.find("[data-test='input']");
  const userInput = "学习jest";
  // simulate触发事件
  inputElem.simulate("change", {
    target: {
      value: userInput,
    },
  });
  inputElem.simulate("keyUp", { keyCode: 0 });
  const newInputElm = wrapper.find("[data-test='input']");
  expect(newInputElm.prop("value")).toBe(userInput);
  expect(fn).toHaveBeenCalledTimes(0);
});

测试组件按键不是回车事件的时候,然后在执行npm run covery。
在这里插入图片描述
这次可以看到百分之一百了。

总结

通过一个小的dmoe了解了tdd的流程,以测试用例驱动的开发,就是先写测试用例,再写组件内容。
好处:

  • 代码质量提升,开发之前已经想好怎么入手,根据测试用例一步一步开发。

单元测试的优劣:

  • 测试覆盖率高
  • 但是业务耦合度高 (依赖数据)
  • 代码量大 (高覆盖率往往伴随高代码量)
  • 过于独立 (无法保证跟其他组件结合不出错)
  • 适用于开发一些单独的函数库。保证入参出参等。不用关心其他函数。
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

coderlin_

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值