前端TDD 与 BDD

948 阅读5分钟

一 TDD 基本流程

1. Header.test.js

it('header 包含一个input框', ()=>{
    const wrapper = shallow(<Header/>)
    const inputElem = findTestWrapper(wrapper, 'input')
    expect(inputElem.length).toExist()
})

2.Header.js

class Header extends Component {
    render(){
        return (
            <div> <input data-test='input' /> </div>
        )
    }
}

3. Header.test.js

it('header input 初始化应为空', ()=>{
    expect(inputElem).toHaveValue('')
})

4. Header.js

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

5. Header.test.js

it('header input 用户输入时,变化', ()=>{
    const userInput = 'learning jest';
    inputElem.simulate('change', {
        target: {value: userInput}
    })
    expect(wrapper).toHaveState({value: userInput})
    expect(inputElem).toHaveValue(userInput)
})

6. Header.js

class Header extends Component {
    handleInputChange = (e)=>{
        this.setState({
            value: e.targe.value
        })
    }
    render(){
        return (
            <div> <input onChange={this.handleInputChange}/> </div>
        )
    }
}

keyCode

it('input 输入回车时,如input无内容,无操作'), ()=>{
    const fn = jest.fn()
    wrapper.setState({value: ''})
    inputElem.simulate('keyUp', {
        keyCode: 13
    })
    expect(fn).not.toHaveBeenCalled()
}
it('input 输入回车时,如input有内容,函数被调用'), ()=>{
    const fn = jest.fn()
    wrapper.setState({value: 'learn react'})
    inputElem.simulate('keyUp', {
        keyCode: 13
    })
    expect(fn).toHaveBeenCalled()
}

TDD 优势 -- 代码质量高 单元测试 -- 测试覆盖率高 业务耦合度高 -- 代码量大 过于独立

  1. 先写测试再写代码
  2. 一般结合单元测试使用,是白盒测试
  3. 测试重点在代码
  4. 安全感低
  5. 速度快

函数库,UI组件库适合使用单元测试

业务代码更适合集成测试

二 BDD (Behavior Driven Developmen)基本流程

基于用于行为测试

  1. 先写代码在写测试
  2. 一般结合集成测试使用,是黑合测试
  3. 测试重点在UI(DOM)
  4. 安全感高
  5. 速度慢

tests/integration/TodoList.js

it('
1. 输入内容
2. 点击回车
3. 列表中展示用户输入的内容项
', ()=>{
    const wrapper = mount(<TodoList/>)
    const inputElem = findTestWrapper(wrapper, 'header-input')
    const content = "aaa";
    inputElem.simulate('change', {
        target: {value: content}
    })
    inputElem.simulate('keyUp', {
        keyCode: 13
    })
    const listItem = findTestWrapper(wrapper, 'list-item');
    expect(listItem.length).toEqual(1);
    expect(listItem.text()).toContain(content);
})

01 使用BDD测试redux

TodoList.js

export class TodoList extends Component {
    //...
}
const mapDispatch = dispatch => ({
    handleInputChange(value){
        dispatch(actions.changeInputValue(value)
    }
})
export default connect(mapState, mapDispatch)(Header);

TodoList.test.js

import {Provider} from 'react-redux';
import store from '../../store/createStore';
it('', ()=>{
    const wrapper = mount(
        <Provider store={store}> <TodoList/></Provider>
    )
    const inputElem = findTestWrapper(wrapper, 'header-input')
    const content = "aaa";
    inputElem.simulate('change', {
        target: {value: content}
    })
    inputElem.simulate('keyUp', {
        keyCode: 13
    })
    const listItem = findTestWrapper(wrapper, 'list-item');
    expect(listItem.length).toEqual(1);
    expect(listItem.text()).toContain(content);
})

三 TDD 理论

TDD过程

  1. 快速新增一个测试
  2. 运行所有测试,发现最新的测试不能通过
  3. 做小小的改动
  4. 运行所有测试,且全部通过
  5. 重构refactor代码,以消除重复设计duplication,优化设计结构

小实战 多币种资金

假设我们有这样一个报表

为了变成一个多币种的报表,加上单位

思考哪些测试一旦通过,就能说明代码正确地计算出报表呢?

  • 假设已给定汇率情况下,要能对两种币种的金额相加,并将结果转为某一种币种
  • 要能将某一金额(每股股价)与某一个数(股数)相乘,并得到一个总金额

为此,我们要创建一个to-do list以提醒我们需要做哪些事情,保持注意力,告诉我们什么时候可完工。

当法郎与美元利率为2:1时,5美元+10法郎=10美元

5美元*2 = 10美元

将amount定义为私有

Dollar 有副作用吗?

钱数必须为整数?

首先我们从测试开始。从简单的开始,清单中第二个不过是实现乘法功能,就从它开始。

const amount = 10;
let amount = 5 * 2;

将5*2移至times()中

let amount;
let times = ()=>{
    amount = 5 * 2
};

如果你可以将代码分成一个个粒度比较小的任务,那么你自然可将它分得大小适当。但当你仅仅采用较大的步伐进行开发,那么你根本不会知道较小步伐是否合适。

let times = (amount)=>{
    amount = amount * 2
};
let times = (amount, multiplier)=>{
    amount = amount * multiplier
};
let times = (amount, multiplier)=>{
    amount *= multiplier
};

现在第一个测试已经完成,回顾一下, 我们做了以下工作

  • 创建一个清单,列出我们所知道的需要让其运行通过的测试
  • 通过一小段代码说明我们希望看到怎样的操作
  • 暂时忽略细节问题
  • 通过建立存根stub来让测试通过
  • 逐渐使工作代码一般化,用变量代替常量
  • 将工作逐步加入计划清单,而不是一次全部提出

测试驱动开发总体流程如下

  1. 写一个测试程序,考虑你希望实现的操作要如何在你的代码中体现出来。你是在写story。设想你希望拥有的接口interface。在story中包含任何你所能想象到的、计算出正确结果所必需的元素。
  2. 让测试程序运行。尽快让测试可运行是压倒一切的中心任务。如明显存在整洁、简单的解决方案,那就键入。如果这个方案需耗费1分钟,那把它记下来,再回到主要问题点来,即怎样才能让测试在几秒内就能运行通过。(这种偏离审美的举动是难以理解的,但只是暂时的)
  3. 编写合格的代码。回归正派的软件设计之路。消除先前的重复设计、使测试尽快运行通过。

我们最终目标是整洁可用的代码。首先解决目标的“可用”问题,,然后在解决“整洁”问题。

尽快使测试可运行的三条策略中的两条:

  • 伪实现 - 返回一个常量并逐渐用变量代替常量,直至伪实现成为真实实现代码
  • 显明实现Obvious Implementation - 将真实的实现代码键入

我经常交替使用这两种实现模式,如果一切顺利,且我知道该写些什么,我会采用显明实现。一旦测试没有通过,我会退回转而采用伪实现,重构直到得到正确的代码。当恢复自信时,再次开始显明实现

首先我们讨论系统应当是这样还是那样工作。一旦就系统的行为达成一致,就开始谈论如何用最好的办法来实现它。

为了让测试能尽快工作,我们大量使用了丑陋的方法。现在是收拾烂摊子的时候了。

待续重构笔记