• 自动化测试框架总结2


    4.React中的TDD和单元测试

    4.1 什么是TDD

    TDD的开发流程:(Red-Green development)

    1.编写测试用例;

    2.运行测试,测试用例无法通过测试;

    3.编写代码,使测试用例通过测试

    4.优化代码,完成开发

    5.重复上述步骤

    TDD优势

    1.长期减少回归bug;

    2.代码质量良好(组织,可维护性)

    3.测试覆盖率高,一般测试覆盖率在80%,90%,不能做到100%

    4.错误测试代码不容易出现

    接下来通过一个TodoList项目来了解TDD的流程

    4.2 React环境中配置Jest

    执行下面 命令

    指定这个版本会有问题,npm install create-react-app@3.0.1 -g

    npm install create-react-app
    create-react-app jest-react
    进入jest-react目录执行
    npm install 
    
    

    jest-react目录下面有个隐藏的git文件夹,我们可以使用git来管理代码。

    通过下面的命令创建一个分支

    git branch lesson1
    
    git checkout lesson1 //切换到lesson1这个分支
    
    npm run start  //启动项目
    

    执行npm run eject命令之前执行下面命令,不然会报错

    git add . 
    
    gti commint -m 'add lock file'
    

    其实脚手架已经集成了jest命令,执行npm run eject命令会把隐藏的配置项都弹射出来

    jest有2个要求,有一个是通过git来管理,还有一个是要安装jest,这里已经都满足了。

    jest的配置项但可以写在文件 jest.config.js中,也可以写在package.json文件中(该文件中有配置项叫'jest")

    关于jest配置项的介绍后面再补充。

    使用默认保存的格式化工具保存jsx形式的react代码,格式会有问题,

    解决方法:右下角,把语言模式 JavaScript 改成 JavaScript React

    4.3 Enzyme的配置

    删除脚手架里面的冗余代码,只剩下App.js代码,index.js代码和App.test.js文件。

    App.js代码如下

    import React from 'react';
    
    function App() {
      return (
        <div className="App">
          hello world2
        </div>
      );
    }
    
    export default App;
    
    

    App.test.js文件代码如下:

    import React from 'react';
    import ReactDOM from 'react-dom';
    import {
      render
    } from '@testing-library/react';
    import App from './App';
    
    test('renders learn react link', () => {
      // const {
      //   getByText
      // } = render( < App / > );
      // const linkElement = getByText(/learn react/i);
      // expect(linkElement).toBeInTheDocument();
    
      expect(2).toBe(2);
    
    });
    test('renders without crashing', () => {
      const div = document.createElement('div');
      ReactDOM.render( < App / > , div);
      ReactDOM.unmountComponentAtNode(div);
    });
    
    
    

    如下代码,可以测试渲染的组件的内容元素

    import React from 'react';
    import ReactDOM from 'react-dom';
    import {
      render
    } from '@testing-library/react';
    import App from './App';
    
    test('renders without crashing', () => {
      const div = document.createElement('div');
      ReactDOM.render(< App />, div);
      // ReactDOM.unmountComponentAtNode(div);
      //如果没有抛出异常的话,则测试用例通过
    
      // 如下:抛出异常的话,测试用例不通过,
      // throw new Error();
      // 测试渲染的元素内容,找到className="App"的标签
      const container = div.getElementsByClassName('App');
      console.log(container)
      // HTMLCollection { }
      expect(container.length).toBe(1);
    });
    

    前端单元测试中如果直接去写这种面向DOM的测试用例,是有局限性的,在做前端单元测试的时候,有的时候想要测试组件的state和prop状态是否正确,不仅要测试DOM的展开,还要测试组件里面的数据细节,直接通过DOM做测试就没办法实现我们对组件内部数据做测试的需求了。

    面向DOM的测试用例有局限性,有时候要测试测试一个组件的prop和state(组件上的状态),DOM的测试只能让我们测试组件的渲染,所以airbnb公司的enzyme的引入就是为了解决这个问题。

    enzyme其实是对 ReactDOM.render做了一些包装,提供了一些额外的方法供我们调用,是我们能够对组件进行更灵活的测试。

    首先安装enzyme,可以去github上搜索airbnb公司的enzyme,(https://github.com/enzymejs/enzyme),查看相关介绍

    npm i --save-dev enzyme enzyme-adapter-react-16
    

    如何配置呢?在测试文件中添加下面的内容

    import Enzyme from 'enzyme';
    import Adapter from 'enzyme-adapter-react-16';
    
    Enzyme.configure({ adapter: new Adapter() });
    

    将这段代码直接复制到App.test.js文件中去,这样测试用例中就可以使用enzyme了。

    enzyme其实是对 ReactDOM.render做了一些包装,有了Enzyme,就不需要ReactDOM了,删除相关的代码

    如下代码:

    import React from 'react';
    import App from './App';
    
    import Enzyme, {
      shallow
    } from 'enzyme';
    import Adapter from 'enzyme-adapter-react-16';
    
    Enzyme.configure({
      adapter: new Adapter()
    });
    
    test('renders without crashing', () => {
      // const div = document.createElement('div');
      // ReactDOM.render(< App />, div);
      // const container = div.getElementsByClassName('App');
      // expect(container.length).toBe(1);
    
      //shallow是浅复制,或者浅渲染。App可能包含多个组件,shallow仅仅是对App组件进行完整的渲染,对于App内部的组件可能只有一个标记来代替
      //shallow只关注App这一组件,不关心下面的,只渲染这一层渲染速度比较快
      //适合于对一个组件做单元测试
      //如下shallow是用Enzyme生成的语法,所以可以用Enzyme里面的方法了,上面注释的代码就可以用下面简单的形式了
      const wrapper = shallow(< App />);
      //console.log(wrapper.find('.App').length);
      // 1,find函数里面是一个选择器,利用选择器就可以选择到div了
      expect(wrapper.find('.App').length).toBe(1);
    });
    

    接下来试试shallow的其他方法,修改App.js内容,给div添加title属性,如下代码:

    import React from 'react';
    
    function App() {
      return (
        <div className="App" title="Dell">
          hello world2
        </div>
      );
    }
    
    export default App;
    
    

    如何测试呢?

    import React from 'react';
    import App from './App';
    
    import Enzyme, {
      shallow
    } from 'enzyme';
    import Adapter from 'enzyme-adapter-react-16';
    
    Enzyme.configure({
      adapter: new Adapter()
    });
    
    test('renders without crashing', () => {
      const wrapper = shallow(< App />);
      // console.log(wrapper.find('.App').prop('title'));
      //"Dell"
      //测试用例通过
      expect(wrapper.find('.App').prop('title')).toBe("Dell");
    
      //测试用例通不过
      // expect(wrapper.find('.App').prop('name')).toBe("Dell");
      //如果有时候测试的时候发现测试用例没通过,但是不知道为什么,可以开启enzyme的调试模式
      // console.log(wrapper.debug()),可以将渲染的内容打印输出,如下
    
      console.log(wrapper.debug());
        
     {/* <div className="App" title="Dell" data-test="container">
      hello world2
        </div> */}
      //修改之后,,测试用例可以通过了
      expect(wrapper.find('.App').prop('title')).toBe("Dell");
    });
    
    

    上面的测试代码有个小问题,就是测试用例的代码和要测试代码耦合很高,上面是通过div的className属性来获取元素的,如果我们觉得div的className不太合适的,会对其进行修改,修改之后测试代码也要跟着修改,代码是耦合的,这样就会比较麻烦。可以用下面的方式来解决这个问题。给要测试的div添加一个专门的测试属性:

    App.js代码如下:

    import React from 'react';
    
    function App() {
      return (
        <div className="App" title="Dell" data-test='container'>
          hello world2
        </div>
      );
    }
    
    export default App;
    
    

    App.test.js代码如下:

    import React from 'react';
    import App from './App';
    
    import Enzyme, {
      shallow
    } from 'enzyme';
    import Adapter from 'enzyme-adapter-react-16';
    
    Enzyme.configure({
      adapter: new Adapter()
    });
    
    test('renders without crashing', () => {
      const wrapper = shallow(< App />);
      console.log(wrapper.debug())
      expect(wrapper.find('[data-test="container"]').length).toBe(1);
      expect(wrapper.find('[data-test="container"]').prop('title')).toBe("Dell");
    });
    
    

    这样的过程就是测试代码和要测试代码解耦的过程。

    Enzyme里面有jest-enzyme,下面链接里面可以看到一些API方法

    https://github.com/FormidableLabs/enzyme-matchers/tree/master/packages/jest-enzyme

    这个链接里面有很多可以使用的比较简单的API方法,如下所示,之前写的代码,可以简化成下面的形式,但是发现运行的时候报错了,toExist未定义

      const wrapper = shallow(< App />);
      expect(wrapper.find('[data-test="container"]').length).toBe(1);
      expect(wrapper.find('[data-test="container"]')).toExist();
    

    这时需要安装Jest-enzyme

    npm install jest-enzyme --save-dev
    

    在使用的时候,还需要在package.json文件中引入jest-enzyme,如下所示,github连接里面也有介绍

    "setupFilesAfterEnv": [
    
       "./node_modules/jest-enzyme/lib/index.js"
    
      ],
    

    重启启动npm run test命令后,就可以使用这些简化语法的API了。

    Jest-enzyme 里面是有很多匹配器的,可以使用这个里面的匹配器

    import React from 'react';
    import App from './App';
    
    import Enzyme, {
      shallow
    } from 'enzyme';
    import Adapter from 'enzyme-adapter-react-16';
    
    Enzyme.configure({
      adapter: new Adapter()
    });
    
    test('renders without crashing', () => {
      const wrapper = shallow(< App />);
      console.log(wrapper.debug())
      // expect(wrapper.find('[data-test="container"]').length).toBe(1);
      //expect(wrapper.find('[data-test="container"]').prop('title')).toBe("Dell");
    
      //引入jest-enzyme之后,就可以使用下面的匹配器了,和上面相比,比较简单
      expect(wrapper.find('[data-test="container"]')).toExist();
      expect(wrapper.find('[data-test="container"]')).toHaveProp('title', 'Dell');
    
    });
    
    

    可以优化为下面的形式

    import React from 'react';
    // import {
    //   render
    // } from '@testing-library/react';
    import App from './App';
    
    import Enzyme, {
      shallow
    } from 'enzyme';
    import Adapter from 'enzyme-adapter-react-16';
    
    Enzyme.configure({
      adapter: new Adapter()
    });
    
    test('renders without crashing', () => {
      const wrapper = shallow(< App />);
      const continer = wrapper.find('[data-test="container"]');
      expect(continer).toExist();
      expect(continer).toHaveProp('title', 'Dell');
    });
    
    

    接下来我们看看mount方法

    //mount和shallow是对应的,当App组件有子组件时候,mount会把子组件也渲染出来
    //在做集成测试的时候,需要测试一堆组件的时候,使用mount比较合适
    //单元测试适合用shallow,集成测试适合用mount

    import React from 'react';
    import App from './App';
    
    import Enzyme, {
      mount
    } from 'enzyme';
    import Adapter from 'enzyme-adapter-react-16';
    
    Enzyme.configure({
      adapter: new Adapter()
    });
    
    test('renders without crashing', () => {
      const wrapper = mount(< App />);
      console.log(wrapper.debug())
      // < App >
      // <div className="App" title="Dell" data-test="container">
      //   hello world2
      //   </div>
      // </App >
      const continer = wrapper.find('[data-test="container"]');
      expect(continer).toExist();
      expect(continer).toHaveProp('title', 'Dell');
    });
    
    

    其实这里组件测试也可以使用toMatchSnapshot() 匹配器,这个匹配器适用于测试组件内容不发生改变的组件,当组件内容更新之后,再更新snapshot。

    expect(wrapper).toMatchSnapshot();
    

    4.4 利用TDD的方式来开发项目

    接下来通过一个TodoList的实例来理解TDD的开发流程

    4.4.1 利用TDD的方式开发Header组件

    在项目的src目录下新建containers文件夹,在containers文件夹下面新建TodoList文件夹,在其目录下新建index.js文件,index.js文件代码如下

    import React from 'react';
    
    function TodoList() {
        return (
            <div>TodoList</div>
        );
    }
    
    export default TodoList;
    
    

    App.js文件代码如下:

    import React from 'react';
    import TodoList from './containers/TodoList'
    
    function App() {
      return (
        <div>
          <TodoList />
        </div>
      );
    }
    
    export default App;
    
    

    一般测试文件会在同级目录下面建立,一般我们会在containers/TodoList同级目录下建立__ tests __文件夹,然后其目录下建立unit文件夹,表示是该组件的单元测试文件代码。

    在TodoList文件夹下面建立components文件夹,文件目录下放与TodoList相关的代码,在这个目录下面新建Header.js文件,代码如下

    import React, { Component } from 'react';
    
    class Header extends Component {
        render() {
            return (<div>Header</div>);
        }
    }
    
    export default Header;
    

    在unit目录下建立Header.js文件,表示是Header.js文件的测试用例。

    Header组件实现了输入文本框的内容,按下回车键后,可以追加到代办列表中

    TDD的开发流程是写测试用例,测试用例是失败的,然后写代码让测试用例通过,最后再优化代码的过程。

    首先来开发测试用例,Header组件里面有一个input组件,Header组件的测试代码如下:

    __ tests __文件夹下面的Header.js文件代码如下:

    import React from 'react';
    import Header from '../../components/Header'
    
    import Enzyme, {
        shallow
    } from 'enzyme';
    import Adapter from 'enzyme-adapter-react-16';
    
    Enzyme.configure({
        adapter: new Adapter()
    });
    
    test('header组件包含一个input框', () => {
        //单元测试适合用shallow
        const wrapper = shallow(< Header />);
        const inputElme = wrapper.find("[data-test='input']");
        expect(inputElme.length).toBe(1);
    });
    

    执行npm run test命令发现测试用例没有通过,然后我们去写源代码让测试用例通过,这就是TDD的开发流程

    修改components目录下面的Header.js文件代码,代码如下,这样我们编写的测试用例就可以通过了。

    import React, { Component } from 'react';
    
    class Header extends Component {
        render() {
            return (
                <div>
                    <input data-test='input' />
                </div>);
        }
    }
    
    export default Header;
    

    第2个测试用例是往input框里面输入内容时,input框里面的内容会发生变化。下面代码的第2个测试用例执行会报错,

    import React from 'react';
    import Header from '../../components/Header'
    
    import Enzyme, {
        shallow
    } from 'enzyme';
    import Adapter from 'enzyme-adapter-react-16';
    
    Enzyme.configure({
        adapter: new Adapter()
    });
    
    test('header组件包含一个input框', () => {
        //单元测试适合用shallow
        const wrapper = shallow(< Header />);
        const inputElme = wrapper.find("[data-test='input']");
        expect(inputElme.length).toBe(1);
    });
    
    test('header组件 input框 内容,初始化应该为空', () => {
        const wrapper = shallow(< Header />);
        const inputElme = wrapper.find("[data-test='input']");
        expect(inputElme.prop('value')).toEqual('');
    });
    
    
    

    Header是一个受控组件,所以其value值是输入值来决定的,修改Header.js文件内容,使测试用例通过

    import React, { Component } from 'react';
    
    class Header extends Component {
        constructor(props) {
            super(props);
            this.state = {
                value: ''
            }
        }
        render() {
            const { value } = this.state;
            return (
                <div>
                    <input data-test='input' value={value} />
                </div>);
        }
    }
    
    export default Header;
    

    第3个测试用例是往input框里面输入内容时,input框里面的内容会发生变化,如下的测试用例,第3个测试用例未通过,

    import React from 'react';
    import Header from '../../components/Header'
    
    import Enzyme, {
        shallow
    } from 'enzyme';
    import Adapter from 'enzyme-adapter-react-16';
    
    Enzyme.configure({
        adapter: new Adapter()
    });
    
    test('header组件包含一个input框', () => {
        //单元测试适合用shallow
        const wrapper = shallow(< Header />);
        const inputElme = wrapper.find("[data-test='input']");
        expect(inputElme.length).toBe(1);
    });
    
    test('header组件 input框 内容,初始化应该为空', () => {
        const wrapper = shallow(< Header />);
        const inputElme = wrapper.find("[data-test='input']");
        expect(inputElme.prop('value')).toEqual('');
    });
    
    //测试用例没通过
    test('header组件 input框 内容,当用户输入时,会跟随变化', () => {
        //模拟用户输入的动作,通过simulate方法可以模拟这个事件,这个方法里面有e,e里面有target
        const wrapper = shallow(< Header />);
        const inputElme = wrapper.find("[data-test='input']");
        const userInput = '今天要学习Jest';
        inputElme.simulate('change', {
            target: { value: userInput }
        })
        expect(wrapper.state('value')).toEqual(userInput);
    });
    
    

    修改Header.js文件,添加Input控件的onChange方法,使测试用例通过。

    import React, { Component } from 'react';
    
    class Header extends Component {
        constructor(props) {
            super(props);
            this.handleInputChange = this.handleInputChange.bind(this);
            this.state = {
                value: ''
            }
        }
    
        handleInputChange(e) {
            this.setState({
                value: e.target.value
            })
        }
    
        render() {
            const { value } = this.state;
            return (
                <div>
                    <input data-test='input' value={value} onChange={this.handleInputChange} />
                </div>);
        }
    }
    
    export default Header;
    

    我们不仅可以测试state的内容,还可以测试input标签的属性等等。如下的代码:

    test('header组件 input框 内容,当用户输入时,会跟随变化', () => {
        //模拟用户输入的动作,通过simulate方法可以模拟这个事件,这个方法里面有e,e里面有target
        const wrapper = shallow(< Header />);
        const inputElme = wrapper.find("[data-test='input']");
        const userInput = '今天要学习Jest';
        //对用户的动作做测试,单元测试里面一般是面向数据的测试,一般是这种测试
        inputElme.simulate('change', {
            target: { value: userInput }
        })
        expect(wrapper.state('value')).toEqual(userInput);
    
        //当用户操作后,对组件的DOM属性做测试,继承测试中一般是对DOM属性做测试
        //组件属性发生变化之后,一般都要重新获取,要不然取到的还是老的组件
        // const newInputElme = wrapper.find("[data-test='input']");
        // expect(newInputElme.prop('value')).toBe(userInput);
    });
    
    

    接下来编写其他的测试用例,当在input框里面输入内容时候,点击回车,我们希望把input框里面的内容存入到最外层的TodoList组件中,这个测试用例怎么写呢?如下代码

    test('header组件 input框 输入回车时,如果input 无内容,无操作', () => {
        //当在input框中添加内容时,会调用外层的方法,把内容添加到TodoList组件中
        const fn = jest.fn();
    
        const wrapper = shallow(< Header addUndoItem={fn} />);
        const inputElme = wrapper.find("[data-test='input']");
        //确保内容是空
        wrapper.setState({
            value: ''
        })
        //keyUp为13时,表示输入回车键
        inputElme.simulate('keyUp', {
            keyCode: 13
        })
        expect(fn).not.toHaveBeenCalled();
    });
    
    //这个测试用例没有通过,因为代码没写,回车的时候什么都没有执行
    test('header组件 input框 输入回车时,如果input 无内容,函数应该被调用', () => {
        //当在input框中添加内容时,会调用外层的方法,把内容添加到TodoList组件中
        const fn = jest.fn();
    
        const wrapper = shallow(< Header addUndoItem={fn} />);
        const inputElme = wrapper.find("[data-test='input']");
        //确保内容是空
        wrapper.setState({ value: '学习React' })
        //keyUp为13时,表示输入回车键
        inputElme.simulate('keyUp', {
            keyCode: 13
        })
        expect(fn).toHaveBeenCalled();
    });
    

    接着我们补充没有实现的功能,让测试用例通过,Header.js代码如下:

    import React, { Component } from 'react';
    
    class Header extends Component {
        //addUndoItem
        constructor(props) {
            super(props);
            this.handleInputChange = this.handleInputChange.bind(this);
            this.handleInputKeyUp = this.handleInputKeyUp.bind(this);
            this.state = {
                value: ''
            }
        }
    
        handleInputKeyUp(e) {
            const { value } = this.state;
            if (e.keyCode === 13 && value) {
                this.props.addUndoItem(value)
            }
        }
    
        handleInputChange(e) {
            this.setState({
                value: e.target.value
            })
        }
    
        render() {
            const { value } = this.state;
            return (
                <div>
                    <input data-test='input'
                        value={value}
                        onChange={this.handleInputChange}
                        onKeyUp={this.handleInputKeyUp}
                    />
                </div>);
        }
    }
    
    export default Header;
    

    测试代码还可以进一步优化,使用 toHaveBeenLastCalledWith,如下代码所示:

    test('header组件 input框 输入回车时,如果input 无内容,函数应该被调用', () => {
        //当在input框中添加内容时,会调用外层的方法,把内容添加到TodoList组件中
        const fn = jest.fn();
    
        const wrapper = shallow(< Header addUndoItem={fn} />);
        const inputElme = wrapper.find("[data-test='input']");
        //确保内容是空
        wrapper.setState({ value: '学习React' })
        //keyUp为13时,表示输入回车键
        inputElme.simulate('keyUp', {
            keyCode: 13
        })
        expect(fn).toHaveBeenCalled();
        //不但有测试函数是否被调用过的API函数,还可以测试函数被调用的参数
        expect(fn).toHaveBeenLastCalledWith('学习React');
    });
    

    最后写完的Header.js测试代码如下

    import React from 'react';
    import Header from '../../components/Header'
    
    import Enzyme, {
        shallow
    } from 'enzyme';
    import Adapter from 'enzyme-adapter-react-16';
    
    Enzyme.configure({
        adapter: new Adapter()
    });
    
    test('header组件包含一个input框', () => {
        //单元测试适合用shallow
        const wrapper = shallow(< Header />);
        const inputElme = wrapper.find("[data-test='input']");
        expect(inputElme.length).toBe(1);
    });
    
    test('header组件 input框 内容,初始化应该为空', () => {
        const wrapper = shallow(< Header />);
        const inputElme = wrapper.find("[data-test='input']");
        expect(inputElme.prop('value')).toEqual('');
    });
    
    
    test('header组件 input框 内容,当用户输入时,会跟随变化', () => {
        //模拟用户输入的动作,通过simulate方法可以模拟这个事件,这个方法里面有e,e里面有target
        const wrapper = shallow(< Header />);
        const inputElme = wrapper.find("[data-test='input']");
        const userInput = '今天要学习Jest';
        //对用户的动作做测试,单元测试里面一般是面向数据的测试,一般是这种测试,下面模拟e
        inputElme.simulate('change', {
            target: { value: userInput }
        })
        expect(wrapper.state('value')).toEqual(userInput);
    
        //当用户操作后,对组件的DOM属性做测试,继承测试中一般是对DOM属性做测试
        //组件属性发生变化之后,一般都要重新获取,要不然取到的还是老的组件
        // const newInputElme = wrapper.find("[data-test='input']");
        // expect(newInputElme.prop('value')).toBe(userInput);
    });
    
    
    test('header组件 input框 输入回车时,如果input 无内容,无操作', () => {
        //当在input框中添加内容时,会调用外层的方法,把内容添加到TodoList组件中
        const fn = jest.fn();
    
        const wrapper = shallow(< Header addUndoItem={fn} />);
        const inputElme = wrapper.find("[data-test='input']");
        //确保内容是空
        wrapper.setState({
            value: ''
        })
        //keyUp为13时,表示输入回车键
        inputElme.simulate('keyUp', {
            keyCode: 13
        })
        expect(fn).not.toHaveBeenCalled();
    });
    
    
    test('header组件 input框 输入回车时,如果input 有内容,函数应该被调用', () => {
        //当在input框中添加内容时,会调用外层的方法,把内容添加到TodoList组件中
        const fn = jest.fn();
    
        const wrapper = shallow(< Header addUndoItem={fn} />);
        const inputElme = wrapper.find("[data-test='input']");
        //确保内容是空
        wrapper.setState({ value: '学习React' })
        //keyUp为13时,表示输入回车键
        inputElme.simulate('keyUp', {
            keyCode: 13
        })
        expect(fn).toHaveBeenCalled();
        //不但有测试函数是否被调用过的API函数,还可以测试函数被调用的参数
        expect(fn).toHaveBeenLastCalledWith('学习React');
    });
    
    
    test('header组件 input框 输入回车时,如果input 有内容,最后应该被清除', () => {
        const fn = jest.fn();
    
        const wrapper = shallow(< Header addUndoItem={fn} />);
        const inputElme = wrapper.find("[data-test='input']");
        //确保内容是空
        wrapper.setState({ value: '学习React' })
        //keyUp为13时,表示输入回车键
        inputElme.simulate('keyUp', {
            keyCode: 13
        })
    
        //当输入回车后,清空input框里面的内容
        //这里要等回车之后再次获取,如果直接用inputElme还是,初始化的inputElme,本来是空的
        const newInputElme = wrapper.find("[data-test='input']");
        expect(newInputElme.prop('value')).toBe('');
    });
    
    

    Header.js的源代码如下

    import React, { Component } from 'react';
    
    class Header extends Component {
        //addUndoItem
        constructor(props) {
            super(props);
            this.handleInputChange = this.handleInputChange.bind(this);
            this.handleInputKeyUp = this.handleInputKeyUp.bind(this);
            this.state = {
                value: ''
            }
        }
    
        handleInputKeyUp(e) {
            const { value } = this.state;
            if (e.keyCode === 13 && value) {
                this.props.addUndoItem(value);
                this.setState({
                    value: ''
                })
            }
        }
    
        handleInputChange(e) {
            this.setState({
                value: e.target.value
            })
        }
    
        render() {
            const { value } = this.state;
            return (
                <div>
                    <input data-test='input'
                        value={value}
                        onChange={this.handleInputChange}
                        onKeyUp={this.handleInputKeyUp}
                    />
                </div>);
        }
    }
    
    export default Header;
    

    4.4.2 TodoList的测试代码编写

    在unit目录下新建文件TodoList.js,来编写TodoList的单元测试

    TodoList.js的测试文件代码如下:通过一个个编写测试用例,让测试用例通过完成我们的代码开发过程

    import React from 'react';
    import TodoList from '../../index'
    
    import Enzyme, {
        shallow
    } from 'enzyme';
    import Adapter from 'enzyme-adapter-react-16';
    
    Enzyme.configure({
        adapter: new Adapter()
    });
    
    test('TodoList 初始化列表为空', () => {
        //TodoList里面undoList为空
        const wrapper = shallow(< TodoList />);
        expect(wrapper.state('undoList')).toEqual([]);
    });
    
    test('TodoList 应该给 Header组件传递一个增加undoList的方法', () => {
        const wrapper = shallow(< TodoList />);
        const Header = wrapper.find('Header');
        //TodoList组件里面给Header组件传递了一个方法,叫做addUndoItem
        //wrapper.instance().addUndoItem表示这个方法是TodoList实例上的一个方法,是类里面的一个方法(this.addUndoItem)
        expect(Header.prop('addUndoItem')).toBe(wrapper.instance().addUndoItem)
    });
    
    test('当Header 回车时,TodoList应该新增内容', () => {
        //这是个单元测试,因此不要考虑Header点击回车的方法,尽量让TodoList和Header完全解耦
        //实际上当Header回车的时候,实际上调用Header上的addUndoItem方法
        //在做TodoList单元测试的时候,不要想着Header里面的东西,能在这个组件内部完成的东西,直接在这个组件上完成
        const wrapper = shallow(< TodoList />);
        const Header = wrapper.find('Header');
        const addFunc = Header.prop('addUndoItem');
        addFunc('学习React')
        expect(wrapper.state('undoList').length).toBe(1);
        expect(wrapper.state('undoList')[0]).toBe('学习React');
    });
    
    

    index.js的源代码如下:

    import React, { Component } from 'react';
    import Header from './components/Header'
    
    class TodoList extends Component {
        constructor(props) {
            super(props);
            this.addUndoItem = this.addUndoItem.bind(this);
            this.state = {
                undoList: []
            }
        }
    
        addUndoItem(value) {
            this.setState({
                undoList: [...this.state.undoList, value]
            })
        }
    
        render() {
            return (
                <div>
                    <Header addUndoItem={this.addUndoItem} />
                </div>
            );
        }
    }
    
    export default TodoList;
    

    测试代码通过之后,我们也不知道界面UI写的是否正确,修改index.js代码如下:

    import React, { Component } from 'react';
    import Header from './components/Header'
    
    class TodoList extends Component {
        constructor(props) {
            super(props);
            this.addUndoItem = this.addUndoItem.bind(this);
            this.state = {
                undoList: []
            }
        }
    
        addUndoItem(value) {
            this.setState({
                undoList: [...this.state.undoList, value]
            })
        }
    
        render() {
            return (
                <div>
                    <Header addUndoItem={this.addUndoItem} />
                    {
                        this.state.undoList.map((item, index) => {
                            return <div key={index}>{item}</div>
                        })
                    }
                </div>
            );
        }
    }
    
    export default TodoList;
    

    以上过程可以看出,利用TDD的方式编写TodoList组件,能够发现代码中的大部分bug。

    4.4.3 Header 组件样式新增及快照测试

    在TodoList文件夹下面新建文件style.css,代码内容如下:

    * {
        margin: 0;
        padding: 0
    }
    
    .header {
        line-height: 60px;
        background: #333;
        font-size: 24px;
        color: #fff;
    }
    
    .header-content {
         600px;
        margin: 0 auto;
        font-size: 24px;
        color: #fff;
    }
    
    .header-input {
        outline: none;
         360px;
        margin-top: 15px;
        float: right;
        line-height: 24px;
        border-radius: 5px;
        padding: 0 10px;
    }
    

    在index.js文件里引入css文件

    import React, { Component } from 'react';

    import Header from './components/Header'

    import './style.css'

    在Header.js文件里面添加一些样式

    import React, { Component } from 'react';
    
    class Header extends Component {
        //addUndoItem
        constructor(props) {
            super(props);
            this.handleInputChange = this.handleInputChange.bind(this);
            this.handleInputKeyUp = this.handleInputKeyUp.bind(this);
            this.state = {
                value: ''
            }
        }
    
        handleInputKeyUp(e) {
            const { value } = this.state;
            if (e.keyCode === 13 && value) {
                this.props.addUndoItem(value);
                this.setState({
                    value: ''
                })
            }
        }
    
        handleInputChange(e) {
            this.setState({
                value: e.target.value
            })
        }
    
        render() {
            const { value } = this.state;
            return (
                <div className="header">
                    <div className="header-content">
                        TodoList
                    <input
                            placeholder="Todo"
                            className="header-input"
                            data-test='input'
                            value={value}
                            onChange={this.handleInputChange}
                            onKeyUp={this.handleInputKeyUp}
                        />
                    </div>
                </div>);
        }
    }
    
    export default Header;
    

    当Header组件的样式写完之后,我们不希望它做频繁的变化,可以写个快照测试,如下代码

    test('header渲染样式正常', () => {
        //单元测试适合用shallow
        const wrapper = shallow(< Header />);
        expect(wrapper).toMatchSnapshot();
    });
    
    

    之后UI发生变化之后,快照测试不会通过,提醒我们验证一下修改后的样式是否正确。

    4.4.4 通用的测试代码提取封装

    1.相似代码提取

    2.enzyme引入文件封装到一个文件,这个文件配置到package.json文件中的setUpFilesAferEnv配置项里面

    上面的例子中虽然额外写了一些测试代码,但是当项目里面新添加一个功能时,需要验证以前的老代码,如果有自动化测试代码,只要确保这些测试用例通过就可以了,如果没有自动化测试代码的话,老代码手动点击回归测试的时间会比较多,非常耗费人力。

    4.4.5 UndoList的实现

    当实现了文本框输入,输入回车之后,需要将内容添加到undoList中,当点击确认后,内容添加到已经完成项。

    接下来我们通过TDD的形式来 实现UndoList。

    在components文件目录下新建文件UndoList.js文件,在测试目录unit目录下新建文件UndoList.js,测试文件代码如下所示,每编写一个测试用例,然后再写源代码,

    import React from 'react';
    import UndoList from '../../components/UndoList'
    
    import Enzyme, {
        shallow
    } from 'enzyme';
    import Adapter from 'enzyme-adapter-react-16';
    
    Enzyme.configure({
        adapter: new Adapter()
    });
    
    test('未完成列表当数据为空数组时 count数目为0,,列表无内容', () => {
        const wrapper = shallow(< UndoList list={[]} />);
        const countElem = wrapper.find("[data-test='count']");
        const listItems = wrapper.find("[data-test='list-item']");
        expect(countElem.text()).toEqual("0");
        expect(listItems.length).toEqual(0);
    });
    
    test('未完成列表当数据有内容时 count数目显示数据长度,,列表不为空', () => {
        const listData = ['学习Jest', '学习TDD', '学习单元测试'];
        const wrapper = shallow(< UndoList list={listData} />);
        const countElem = wrapper.find("[data-test='count']");
        const listItems = wrapper.find("[data-test='list-item']");
        expect(countElem.text()).toEqual("3");
        expect(listItems.length).toEqual(3);
    });
    
    test('未完成列表当数据有内容时 要存在删除按钮', () => {
        const listData = ['学习Jest', '学习TDD', '学习单元测试'];
        const wrapper = shallow(< UndoList list={listData} />);
        const deleteItems = wrapper.find("[data-test='delete-item']");
        expect(deleteItems.length).toEqual(3);
    });
    
    test('未完成列表当数据有内容时 点击某个删除按钮,,会调用删除方法', () => {
        const listData = ['学习Jest', '学习TDD', '学习单元测试'];
        const fn = jest.fn();
        const index = 1;
        const wrapper = shallow(< UndoList deleteItem={fn} list={listData} />);
        const deleteItems = wrapper.find("[data-test='delete-item']");
        //jest里面先通过数组找某一项,不能通过下标的形式,而是要通过at()方法
        deleteItems.at(index).simulate('click');
        expect(fn).toHaveBeenLastCalledWith(index);
    });
    
    
    

    UndoList.js代码如下:

    import React, { Component } from 'react';
    
    class UndoList extends Component {
        render() {
            const { list, deleteItem } = this.props;
            return (
                <div>
                    <div data-test="count">{list.length}</div>
                    <ul>
                        {
                            list.map((item, index) => {
                                return (
                                    <li
                                        data-test='list-item'
                                        key={`${item}-${index}`}
                                    >
                                        {item}
                                        <span
                                            data-test='delete-item'
                                            onClick={() => { deleteItem(index) }}>
                                            -
                                        </span>
                                    </li>)
                            })
                        }
                    </ul>
    
                </div>
            )
    
        }
    }
    
    export default UndoList;
    

    上面的测试代码就实现了UndoList内部的单元测试,删除的功能就放在TodoList来做,测试代码如下

    import React from 'react';
    import TodoList from '../../index'
    
    import Enzyme, {
        shallow
    } from 'enzyme';
    import Adapter from 'enzyme-adapter-react-16';
    
    Enzyme.configure({
        adapter: new Adapter()
    });
    
    test('TodoList 初始化列表为空', () => {
        //TodoList里面undoList为空
        const wrapper = shallow(< TodoList />);
        expect(wrapper.state('undoList')).toEqual([]);
    });
    
    test('TodoList 应该给 Header组件传递一个增加undoList的方法', () => {
        const wrapper = shallow(< TodoList />);
        const Header = wrapper.find('Header');
        //TodoList组件里面给Header组件传递了一个方法,叫做addUndoItem
        //wrapper.instance().addUndoItem表示这个方法是TodoList实例上的一个方法,是类里面的一个方法(this.addUndoItem)
        expect(Header.prop('addUndoItem')).toBeTruthy();
    });
    
    test('当addItem 被执行时,undoList应该新增内容', () => {
        //这是个单元测试,因此不要考虑Header点击回车的方法,尽量让TodoList和Header完全解耦
        //实际上当Header回车的时候,实际上调用Header上的addUndoItem方法
        //在做TodoList单元测试的时候,不要想着Header里面的东西,能在这个组件内部完成的东西,直接在这个组件上完成
        const wrapper = shallow(< TodoList />);
        wrapper.instance().addUndoItem('学习React')
        expect(wrapper.state('undoList').length).toBe(1);
        expect(wrapper.state('undoList')[0]).toBe('学习React');
    });
    
    
    test('TodoList 应该给 UndoList组件传递list数据,以及deleteItem方法', () => {
        const wrapper = shallow(< TodoList />);
        const UndoList = wrapper.find('UndoList');
        //传递的属性数据
        expect(UndoList.prop('list')).toBeTruthy();
        expect(UndoList.prop('deleteItem')).toBeTruthy();
    });
    
    
    test('当 deleteItem方法被执行时,undoList应该删除内容', () => {
        const wrapper = shallow(< TodoList />);
        wrapper.setState({
            undoList: ['学习Jest', 'dell', 'lee']
        })
        wrapper.instance().deleteItem(1);
        expect(wrapper.state('undoList')).toEqual(['学习Jest', 'lee']);
        //上面的addItem已经
    });
    
    

    index.jsx代码如下:

    import React, { Component } from 'react';
    import Header from './components/Header'
    import './style.css'
    import UndoList from './components/UndoList';
    
    class TodoList extends Component {
        constructor(props) {
            super(props);
            this.addUndoItem = this.addUndoItem.bind(this);
            this.deleteItem = this.deleteItem.bind(this);
            this.state = {
                undoList: []
            }
        }
    
        addUndoItem(value) {
            this.setState({
                undoList: [...this.state.undoList, value]
            })
        }
    
        deleteItem(index) {
            const newList = [...this.state.undoList];
            newList.splice(index, 1);
            this.setState({
                undoList: newList
            })
        }
    
        render() {
            const { undoList } = this.state;
            return (
                <div>
                    <Header addUndoItem={this.addUndoItem} />
                    <UndoList list={undoList} deleteItem={this.deleteItem} />
                </div>
            );
        }
    }
    
    export default TodoList;
    

    4.4.6 给UndoList添加样式

    4.4.7 测试代码优化

    每个组件测试用例的描述都比较长,可以将每个test测试用例放到describe里面,describe的名称是组件的名称,这样测试代码看起来,可读性就会更高一些。

    4.4.8 UndoList编辑功能实现

    当编辑undoList的每项时,可以让其变成编辑状态,当失焦或者按下回车时,保存为修改后的名称。

    我们存储的undoList数据结构是一个数组,只用于展示,并不是识别input框,所以这里要改变其数据结构,识别是不是input框的状态。

    这样添加数据项的时候也应该修改一下数据结构,index.jsx代码如下

    import React, { Component } from 'react';
    import Header from './components/Header'
    import './style.css'
    import UndoList from './components/UndoList';
    
    class TodoList extends Component {
        constructor(props) {
            super(props);
            this.addUndoItem = this.addUndoItem.bind(this);
            this.deleteItem = this.deleteItem.bind(this);
            this.changeStatus = this.changeStatus.bind(this);
            this.state = {
                undoList: []
            }
        }
    
        addUndoItem(value) {
            this.setState({
                undoList: [...this.state.undoList, {
                    status: 'div',
                    value
                }]
            })
        }
    
        deleteItem(index) {
            const newList = [...this.state.undoList];
            newList.splice(index, 1);
            this.setState({
                undoList: newList
            })
        }
    
        changeStatus(index) {
            console.log(index)
        }
    
        render() {
            const { undoList } = this.state;
            return (
                <div>
                    <Header addUndoItem={this.addUndoItem} />
                    <UndoList list={undoList} deleteItem={this.deleteItem} changeStatus={this.changeStatus} />
                </div>
            );
        }
    }
    
    export default TodoList;
    

    修改代码的数据结构之后,UndoList的测试文件代码也要跟着修改,如下代码

    import React from 'react';
    import TodoList from '../../index'
    
    import Enzyme, {
        shallow
    } from 'enzyme';
    import Adapter from 'enzyme-adapter-react-16';
    
    Enzyme.configure({
        adapter: new Adapter()
    });
    
    test('TodoList 初始化列表为空', () => {
        //TodoList里面undoList为空
        const wrapper = shallow(< TodoList />);
        expect(wrapper.state('undoList')).toEqual([]);
    });
    
    test('TodoList 应该给 Header组件传递一个增加undoList的方法', () => {
        const wrapper = shallow(< TodoList />);
        const Header = wrapper.find('Header');
        //TodoList组件里面给Header组件传递了一个方法,叫做addUndoItem
        //wrapper.instance().addUndoItem表示这个方法是TodoList实例上的一个方法,是类里面的一个方法(this.addUndoItem)
        expect(Header.prop('addUndoItem')).toBeTruthy();
    });
    
    test('当addItem 被执行时,undoList应该新增内容', () => {
        //这是个单元测试,因此不要考虑Header点击回车的方法,尽量让TodoList和Header完全解耦
        //实际上当Header回车的时候,实际上调用Header上的addUndoItem方法
        //在做TodoList单元测试的时候,不要想着Header里面的东西,能在这个组件内部完成的东西,直接在这个组件上完成
        const wrapper = shallow(< TodoList />);
        wrapper.instance().addUndoItem('学习React')
        expect(wrapper.state('undoList').length).toBe(1);
        expect(wrapper.state('undoList')[0]).toEqual({
            status: 'div',
            value: '学习React'
        });
    });
    
    
    test('TodoList 应该给 UndoList组件传递list数据,以及deleteItem以及changeStatus方法', () => {
        const wrapper = shallow(< TodoList />);
        const UndoList = wrapper.find('UndoList');
        //传递的属性数据
        expect(UndoList.prop('list')).toBeTruthy();
        expect(UndoList.prop('deleteItem')).toBeTruthy();
        expect(UndoList.prop('changeStatus')).toBeTruthy();
    });
    
    
    test('当 deleteItem方法被执行时,undoList应该删除内容', () => {
        const wrapper = shallow(< TodoList />);
        wrapper.setState({
            undoList: ['学习Jest', 'dell', 'lee']
        })
        wrapper.instance().deleteItem(1);
        expect(wrapper.state('undoList')).toEqual(['学习Jest', 'lee']);
        //上面的addItem已经
    });
    
    

    UndoList.js代码如下

    import React, { Component } from 'react';
    
    class UndoList extends Component {
        render() {
            const { list, deleteItem, changeStatus } = this.props;
            return (
                <div className='undo-list'>
                    <div className="undo-list-title">
                        正在进行
                        <div data-test="count" className="undo-list-count">{list.length}</div>
                    </div>
    
                    <ul className="undo-list-content">
                        {
                            list.map((item, index) => {
                                return (
                                    <li className='undo-list-item'
                                        data-test='list-item'
                                        key={index}
                                        onClick={() => changeStatus(index)}
                                    >
                                        {item.value}
                                        <span
                                            className="undo-list-delete"
                                            data-test='delete-item'
                                            onClick={() => { deleteItem(index) }}>
                                            -
                                        </span>
                                    </li>)
                            })
                        }
                    </ul>
    
                </div>
            )
    
        }
    }
    
    export default UndoList;
    

    修改数据结构后,修改相关报错代码,当单元测试全部通过的时候,页面却是挂的;

    使用TDD加上单元测试方式的问题:真正的数据结构或者组件内容发生变化的时候,需要回头重新修改测试用例;因为测试用例里面用了大量耦合的数据;当有需求变更的时候,会导致之前的测试用例不可用。

    即便所有的单元测试用例都通过了测试,也无法保证项目在浏览器上可以正确无误地运行,因为单元测试测试的是每个组件,并没有将每个组件集成在一起做测试,这样每个组件是好用的,但是合在一起是否好用不知道。

    4.4.9 UndoList编辑功能实现2

    该小节实现当输入框失去焦点时,就 不是输入框,而是显示状态了。通过先写测试用例,然后写代码的方式实现这个功能。

    4.4.10 CodeCoverage代码覆盖率

    看看测试代码覆盖了多少业务逻辑代码

    在package.json文件里添加一个命令coverage,执行npm run coverage命令就可以看到测试用例的覆盖率了,可以index.html中详细看出,coverage这2行命令都可以被成功执行。

      "scripts": {
        "start": "node scripts/start.js",
        "build": "node scripts/build.js",
        "test": "node scripts/test.js",
        "coverage": "node scripts/test.js --coverage --watchAll=false"
        //"coverage": "jest --coverage --watchAll=false"
      },
    

    4.5 TDD和单元测试总结

    TDD和单元测试是2个不同的概念,TDD也可以和集成测试在一起。

    TDD的好处:代码质量提高,在写代码之前,反复思考过代码和测试用例分别怎么写才合适
    
    单元测试:
    
    好处:测试覆盖率高;
    
    劣势:业务耦合度高;代码量大(测试代码比源代码还要多); 过于独立(单元测试通过,项目不一定运行正常)
    

    当写函数库的时候,非常适合用单元测试来写。

    业务场景下,单元测试的劣势很明显,业务代码使用集成测试更好地保证项目的质量。

    5.BDD和集成测试

    BDD(Behavaior Driven Development) 行为驱动开发

    先写代码,再写测试代码

    开发模式 介绍
    TDD 1.先写测试再写代码;

    TDD

    1.先写测试再写代码;

    2一般结合单元测试使用,是白盒测试;

    3.测试重点在代码;

    4.安全感第;

    5.速度快;

    BDD

    1.先写代码再写测试;

    2.一般结合集成测试使用,是黑盒测试;

    3.测试重点在于UI(DOM)

    4.安全感高

    5.速度慢

  • 相关阅读:
    关于微信最新推出的应用号的看法
    HTML常见标签
    重读《从菜鸟到测试架构师》-- 模拟客户的访问行为(上)
    重读《从菜鸟到测试架构师》-- 大促带来的灾难
    重读《从菜鸟到测试架构师》-- 功能测试之百种变身
    重读《从菜鸟到测试架构师》-- 对黑盒子的全方位照明
    重读《从菜鸟到测试架构师》-- 如何把黑盒子分块
    重读《从菜鸟到测试架构师》-- 黑色的盒子里有什么(下)
    重读《从菜鸟到测试架构师》--黑色的盒子里有什么(中)
    重读《从菜鸟到测试架构师》-- 黑色的盒子里面有什么(上)
  • 原文地址:https://www.cnblogs.com/zdjBlog/p/13157326.html
Copyright © 2020-2023  润新知