何为BDD(Behavior Driven Development)

  • 行为驱动的开发,是一种敏捷软件开发的技术,他鼓励软件项目中的开发者,QA和非技术人员或商业参与者之间的协作。
    在这里插入图片描述

  • 以上节TDD的代码为例子,将测试文件全部删除。从BDD的角度出发,开发人员不需要先写测试用例,直接编写代码即可。

  • 编写完代码之后,第二步就是需要站在用户的角度,怎么去测试,测试的重点是什么?从用户的行为角度出发,编写我们的测试用例。因为用户行为是连续的,比如toDOList案例,用户希望在输入框输入内容按下回车后,list组件可以显示出来。他们需要不同组建的一个协同。

  • 所以BDD一般是结合集成测试来开发。

  • 还是以todoList案例,等我们编写玩开发代码,站在用户的角度出发,我希望,输入内容之后,按下回车,能在List里面看到我想要的内容。根据这个需求编写测试用例。


it(`
 1. 用户输入输入框内容
 2. 点击回车
 3. 列表中显示输入的内容
`, () => {
  // 多个组件,使用mount
  const wrapper = mount(<TodoList />);
  const inputElm = wrapper.find("[data-test='input']");

  const inputValue = "用户输入了内容";
  // 模拟用户第一个行为
  inputElm.simulate("change", {
    target: { value: inputValue },
  });
  // 点击回车
  inputElm.simulate("keyUp", {
    keyCode: 13,
  });
  // 列表中应该显示内容
  const listItems = wrapper.find("[data-test='list-item']");
  console.log('listItems', listItems.debug());
  expect(listItems.length).toBe(1);
  expect(listItems.text()).toBe(inputValue);
});

如上,因为涉及到多个组件,所以必须使用mount渲染,然后模拟用户输入内容,触发回车事件,然后判断列表中是不是真的多出一个list。
在这里插入图片描述
测试通过。

何为BDD?

通过简单的案例我们可以知道BDD,是以用户行为驱动的开发,

  • 流程就是 先根据功能编写代码,等功能开发完毕之后,
  • 站在用户的角度出发,去编写测试用例。
  • Bdd一般结合集成测试。
  • 而TDD则是以测试驱动的开发,先编写测试用例,再开发代码。
  • TDD一般结合单元测试。主要是为每个组件的功能进行测试。
TDD & BDD

TDD:

  • 先写测试再写代码
  • 一般结合单元测试,是白盒测试
  • 测试重点在代码
  • 安全感第
  • 速度快(shallow)
    BDD:
  • 先写代码,再写测试
  • 一般结合集成测试使用,是黑盒测试
  • 测试重点在UI(DOM)
  • 安全感高
  • 速度慢(mount)

如何进行Redux的测试

如果使用TDD进行redux的测试,那么action ,reducer等等都需要编写测试用例,代码量大。也不能保证数据流正确。
所以得使用BDD来进行redux的测试比较好。

  • 还是以toDOlist案例,把状态存储到redux去。
    改造一下demo:
    Header组件
function Headers({ onAddItem }: Props) {
  const dispatch = useDispatch();
  const [value, setValue] = React.useState("");

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

list组件


function TodoList() {
  const { items } = useSelector(
    (state: any) => ({ items: state.todo.items }),
    shallowEqual
  );
  return (
    <div>
      <Headers />
      <div>------------</div>
      {items.map((ctem) => {
        return (
          <div data-test="list-item" key={ctem}>
            {ctem}
          </div>
        );
      })}
    </div>
  );
}

将状态存储到redux去。
然后重新编写刚才的测试用例


it(`
 1. 用户输入输入框内容
 2. 点击回车
 3. 列表中显示输入的内容
`, () => {
  // 多个组件,使用mount
  const wrapper = mount(
    <Provider store={store}>
      <TodoList />
    </Provider>
  );
  const inputElm = wrapper.find("[data-test='input']");

  const inputValue = "用户输入了内容";
  // 模拟用户第一个行为
  inputElm.simulate("change", {
    target: { value: inputValue },
  });
  // 点击回车
  inputElm.simulate("keyUp", {
    keyCode: 13,
  });
  // 列表中应该显示内容
  const listItems = wrapper.find("[data-test='list-item']");
  console.log("listItems", listItems.debug());
  expect(listItems.length).toBe(1);
  expect(listItems.text()).toBe(inputValue);
});

这里需要跟外部一样,将store传进去给他。这里只是需要更改一下moutn的内容,其他东西根本不需要更改,这也是BDD的好处。

  • 开发业务代码不用考虑测试用例
  • 业务代码如果更改比如从state到redux,那么BDD测试用例是很少改动的,因为他不是基于代码的,而是基于用户角度,只要流程跑的通,就正确。(耦合度低)
  • BDD驱动的集成测试,安全感较高,因为这个测试用例一通过,就表示这个功能是可以用的。而不会出现单元测试无法保证多个组件结合在一起的可用性。
  • BDD的代码覆盖率会降低,但是影响不大。
    在这里插入图片描述

异步代码的测试

  • 还是以todoList为例
  • 实现一个功能,在页面初始化的时候i请求数据,展示List。
 useEffect(() => {
 // 测试的时候我们不需要发送请求
    axios.get("/xxx").then((res: any) => {
      setItem((pre) => [...pre, ...res.data]);
    });
  }, []);

功能写完编写测试用例

因为测试用例需要执行组件,所以模拟axios
__mocks__/axios.ts
export default {
  get(url) {
    if (url === "/xxx") {
      return new Promise((r) => {
          r({ data: ["test1", "test2"] });
      });
    }
  },
};

当我们使用mount渲染组件的时候,里面遇到axios的地方就会去上面获取,如上,返回了test1 test2。

test(`1.用户打开页面
    2. 展示接口返回的数据
`, (done) => {
  const wrapper = mount(
    <Provider store={store}>
      <TodoList />
    </Provider>
  );
  // 异步需要等待
  setTimeout(() => {
    wrapper.update()
    console.log(wrapper.debug())
    const listItems = (wrapper as any).find("[data-test='list-item']");
    expect(listItems.length).toBe(2);
    done()
  }, 1000);
});

因为请求时异步的,所以我们需要等待一下才能判断,然后wrapper.update去更新dom,此时dom已经有了请求回来的数据。因为这里是异步,所以需要用到done。
看下更新后的dom:
第一次渲染

 console.log
      <Provider store={{...}}>
        <TodoList>
          <div>
            <Headers>
              <div className="Header">
                <input className="header-input" type="text" data-test="input" value="" onChange={[Function: onChange]} onKeyUp={[Function: onKeyUp]} />
              </div>
            </Headers>
            <div>
              ------------
            </div>
          </div>
        </TodoList>
      </Provider>

没有List。
更新后:

<Provider store={{...}}>
        <TodoList>
          <div>
            <Headers>
              <div className="Header">
                <input className="header-input" type="text" data-test="input" value="" onChange={[Function: onChange]} onKeyUp={[Function: onKeyUp]} />
              </div>
            </Headers>
            <div>
              ------------
            </div>
            <div data-test="list-item">
              test1
            </div>
            <div data-test="list-item">
              test2
            </div>
          </div>
        </TodoList>
      </Provider>

已经渲染出list了。

  • setTimeout的异步怎么优化?不可能等待所有setTImeout完成的。
  useEffect(() => {
    setTimeout(() => {
      axios
        .get("/xxx")
        .then((res: any) => {
          console.log("res----------", res);
          setItem((pre) => [...pre, ...res.data]);
        })
        .catch((err) => {
        });
    }, 5000);
  }, []);

需要使用jests提供的setTImoeut

eforeEach(() => {
    console.log(123123);
    
  jest.useFakeTimers();
});
afterEach(() => {
  jest.useRealTimers();
});
test(`1.用户打开页面
    2. 展示接口返回的数据
`, (done) => {
    // 将所有setTimoeut里面执行
  const wrapper = mount(
    <Provider store={store}>
      <TodoList />
    </Provider>
  );
  jest.runAllTimers();
  // 异步需要等待
  setTimeout(() => {
    wrapper.update();
    const listItems = (wrapper as any).find("[data-test='list-item']");
    expect(listItems.length).toBe(2);
    done();
  }, 1000);
});
Test Suites: 2 passed, 2 total
Tests:       2 passed, 2 total
Snapshots:   1 obsolete, 0 total
Time:        6.854 s

这样就可以了。

总结:
  • BDD就是先开发组件的功能,然后从用户角度出发,编写一套流程的测试用例,而不是单个单个的,当测试用例通过的时候,就表示这些组件结合起来的功能是完整的。
  • 而TDD是先编写测试用例,(单元测试),然后再开发代码,可以保证每个组件的功能完整性,但是不能保证这些组件结合起来后的功能是否完整。
  • BDD+集成测试对于业务代码是一个相对更好的方案,TDD+单元测试对于单独的函数库来说会更好。

前端自动化测试的优势

  • 更好的代码组织,项目的可维护性增强
  • 更小的bug出现概率,尤其回归测试的bug
  • 修改工程质量差的项目,更加安全
Logo

开放原子开发者工作坊旨在鼓励更多人参与开源活动,与志同道合的开发者们相互交流开发经验、分享开发心得、获取前沿技术趋势。工作坊有多种形式的开发者活动,如meetup、训练营等,主打技术交流,干货满满,真诚地邀请各位开发者共同参与!

更多推荐