阅读更多

0顶
0踩

研发管理
最近在做 Coding 企业版 前端开发时花了很多时间写测试,于是和大家分享一些前端开发中的测试概念与方法。

什么是写测试代码
我理解的写测试其实是你写一些代码来验证你所谓的可以交付的代码是你所预期的设计,有一些朋友叫他 TDD 也就是测试驱动型的设计,其实到底是先写代码还是先写测试,并不是最重要的,倒是能给你信心这个代码是符合设计的更重要。

为什么要测试,前端需要测试么
这个问题不是这篇分享要和大家聊的,但是作为曾经也有这样疑问的我还是简单提一下。我们经常过于自信自己的代码,因为编写的时候已经做过 debug 调试,完事后觉得足够了,或者期待下次重构再调整之。结果遇到 bug 无法最快时间确定问题,别人接手代码也不知道这个模块的设计意图和使用方法,必须跳进去读代码,也不清楚改了一些内容后会不会影响这个模块功能,又得耗时再次 debug 。在弱类型的语言尤其前端开发中尤为明显。那种决定暂时弃之而不顾的的思想很可怕,因为我们没有听过过勒布朗法则:稍后等于永不。

聊聊测试的几种类型
单元测试
从字面意思理解,写一段代码来测试一个单元。何为单元?其实和编程语言相关,他有可能是一个 function,一个 module 一个package 一个类,当然在 JavaScript 中也很有可能只是一个 object 。既然如此,那么测试这样的一个小块基本上就是比较孤立,单独验证这个小块的逻辑,一个 function 的输入输出,一个算法的功能和复杂度等等。接下来举几个企业版前端开发中的实际案例。

我们使用 jest 作为测试框架(断言库)。jest会自动搜索所有文件目录下的.spec.js结尾的文件,然后执行测试。断言库其实还有很多,他们都具备类似 describe , it , expect 些 api。对于一个没有其他依赖的纯函数,例如 redux 中同步 action 或 reducer。 我们要测的当然就是输入用例然后对应输出是否符合预期
it('should return showMore action', () => {
    expect(showMore()).toEqual({
        type: ACTION.DEMO_LIST_REMOVE_ITEM,
    });
});

我们注意到这样的一个 function 并没有 I/O 和 UI 上的依赖,他更有利于做单元测试。其中的 it 接受一个 string 参数,描述一个小测试。另一个就是测试方法体函数,it 这种测试不能单独使用,一般都包在一个 describe 方法下成为的方法组。那方法体里写什么呢,其实我也可以写成
if (showMore().type !== ACTION.DEMO_LIST_REMOVE_ITEM)
  throw 'failed'

只要抛出异常那么框架就会认为这条测试跑不过。当然 expect 则 api 更加的漂亮,拥有 toEqual toBe、toMapSnap
shot 等判断 api 确定两个条件之间的关系.
对于纯函数的测试并不难,难的还是如何把代码写的更可单元测试化,而不要有太多的依赖。

集成测试
事实上很多情况小块代码还是会有函数和 I/O 依赖,比如一些 code 依赖 Ajax 或者 localStorage 或者 IndexedDB ,这样的代码是不能被 united-test 的,于是我们需要 mock 相应依赖的接口拿到上下文测试我们的代码,这样的测试叫集成测试。我们项目中主要依赖了 js-dom 和异步的 action 。下面分别讨论

涉及依赖的函数情况--(异步action)
事实上很多情况函数还是会有函数和I/O依赖,最典型的就是异步action等,他的I/O可能会依赖store.getState(),自身又会依赖异步中间键。这类使用原生js测试起来是比较困难的。我们思考我们测试目的,即当我们触发了一个action后它经历了一个圈异步最终store.getAction中这个action拿到的数据是否和我们预期一致。既然大家依赖redux中store的生命周期与store,于是我们需要两个工具库 redux-mock-store和nock ,于是测试就变成了这样。
import configureMockStore from 'redux-mock-store';
import nock from 'nock';
import thunk from 'redux-thunk';
const mockStore = configureMockStore([thunk]);
// 配置mock的store,也让他有我们相同的middleware
describe('get billings actions', () => {
    afterEach(() => nock.cleanAll());// 每执行完一个测试后清空nock
    it('create get all Billings action', () => {
        const store = mockStore({ 
        // 以我们约定的初始state创建store,控制I/O依赖
            APP: { enterprise: { key : 'codingcorp' } }
        });
        const data = [
            // 接口返回信息
            { ...
            },
        ];
        nock(API_HOST)// 拦截请求返回假定的response
            .get(`/api/enterprise/codingcorp/billings`)
            .reply(200, { code: 0, data })
        return store.dispatch(actions.getAllBillings())
            .then(() => {
                expect(store.getActions()).toMatchSnapshot();
        });
    });
});

  • 用 nock 来 mock 拦截 http 请求结果,并返回我们给定的 response。
  • 用 redux-mock-store 来 mock store 的生命周期,需要预先把 middleware 配成和项目一致。
  • desribe会包含一些生命周期的 api,比如全部测试开始做啥,单个测试结束做啥这类 api。这里每执行完一个测试就清空 nock。
  • 用了 jest 中的 toMatchSnapshot api 判断两个条件一致与否。原先可能要写成expect(store.getActions()).toEqual({data ...});这样,你需要把 equal 里的东西都想具体描写清楚,而 toMatchSnapshot 可在当前目录下生成一个 snapshot 存放这个当前结果,写测试时看一眼结果是预期的就可以 commit。如果改坏了函数就不匹配 snapshot 了。
涉及依赖的函数情况--(react component)
我们写的很多component是extends component 的jsx,测试这类需要一个 mock component 的工具库 Enzyme 。
    it('should add key with never expire', () => {
        ... 
        挂载我们的dom
        const wrapper = shallow(
            <TwoFactorModal
                verifyKey={verifyKeySpy}
                onVerifySuccess={onVerifySuccessSpy}
            />
        );
        // wrapper的setstate方法
        wrapper.setState({
            name: 'test',
            password: '123',
        });
        const name = 'new name';
        const content = 'new content';
        const expiration = '2016-01-01';
        
        wrapper.find('.name').simulate('change', {}, name);
        wrapper.find('.content').simulate('change', {}, content);
       
        expect(wrapper.find('.permanentCheck').prop('checked')).toBe(true);
        // 此处也可以使用toMatchSnapshot
        // submit to add
        wrapper.find('.submitBtn').simulate('click', e);

        return promise.then(() => {
            expect(onCheckSuccess).toBeCalledWith({
                name,
                password,
            });
        });
    });

Enzyme 给我们提供了很多 react-dom 的事件操作与数据获取。
这类 component 的测试一般分为
- Structural Testing 结构测试
主要关心一个界面是否有这些元素
例如我们有一个界面是

Screen Shot 2017-03-26 at 1.25.15 PM.png
结构化测试将包含:
- 一个title包含“登入到codingcorp.coding.net”
- 一个副标题包含“..”
- 两个输入框
- 一个提交按钮
...
比较方便的实现就是利用 jest的snapshot 测试方法,先做一个预期生成snapshot,之后的版本与预期对比。
Interaction Testing 交互测试
比如上述案例触发提交按钮,他应该返回给我用户名和密码,并得到验证结果
这类一般使用 Enzyme 比较方便

样式测试
UI的样式测试为了测试我们的样式是否复合设计稿预期。同时通过样式测试我们可以感受当我们 code 变化带来的ui变化,以及他是否符合预期。

inline style
如果样式是 inline style,这类测试其实直接使用 jest 的 Snapshot Testing 最方便,一般在组件库中使用。

CSS
这部分其实属于 E2E 测试中的部分,这里提前讲,主要解决的问题是我们写出来的ui是否符合设计稿的预期。我们使用 BackstopJS 他的原理是通过对页面的viewports和 scenarios 等做配置,利用 web-driver 获取图片,与设计稿或者预期图做 diff,产生报告。
{
// 需要测试的模块元素定义
  "viewports": [
    {
      "name": "password", //密码框
      "width": 320,
      "height": 480
    },
  ],
  "scenarios": [
    {
      "label": "members",
      "url": "/member/admin",
      "selectors": [ // css选择器
        ".member-selector"
      ],
      "readyEvent": "gmapResponded",
      "delay": 100,
      "misMatchThreshold" : 1,
      "onBeforeScript": "onBefore.js",
      "onReadyScript": "onReady.js"
    }
  ],
  "paths": {
    "bitmaps_reference": "backstop_data/bitmaps_reference",
    "bitmaps_test": "backstop_data/bitmaps_test",
    "html_report": "backstop_data/html_report",
    "ci_report": "backstop_data/ci_report"
  },
  "casperFlags": [],
  "engine": "slimerjs",
  "report": ["browser"],
  "debug": false
}

最后会得出类似这样的报告

E2E 测试
E2E 测试是在实际生产环境测试整个app,通常来说这部分工作会让测试人工做,并在实体环境跑,就像用户实际在操作一样。靠人工做遇到项目逻辑比较复杂,则需要每一个版本都要测很多逻辑,担心提交一个影响了其他部分。其实也有比较好的自动化跑脚本方案能帮助测试,我们使用 selenium-webdriver 工具配合async await进行自动化E2E测试。
const {prepareDriver, cleanupDriver} = require('../utils/browser-automation')

//...
describe('member', function () {
  let driver
  ...
  before(async () => {
    driver = await prepareDriver()
  })
  after(() => cleanupDriver(driver))

  it('should work', async function () {
  const submitBtn = await driver.findElement(By.css('.submitBtn'))
    await driver.get('http://localhost:4000')
    await retry(async () => {
      const displayElement = await driver.findElement(By.css('.display'))
      const displayText = await displayElement.getText()
      expect(displayText).to.equal('0')
  })
    await submitBtn.click()
})

selenium-webdriver 提供了很多浏览器的操作以及对元素对查找方法,以及元素内容的获取方法,比如这里的 By.css 选择器。
有时候用户端的设备很不一致,需要在不同设备上的匹配,于是我们可以用 selenium-webdriver 搭配 sourcelab 的设备墙进行

测试覆盖率与代码变异测试
测试覆盖率表达本次测试有有多少比例的语句,函数分支没有被测到。当然绝对数字作为代码质量依据并没有什么意义,因为它是根据我们写的测试来的。倒是学习为什么有些代码没有被覆盖到,以及为什么有些代码变了测试却没有失败。很有意义。我们在jestconfig中配置完目标数据后,每次他会检测我们的测试覆盖率并给我们报告

Function Coverage 函数覆盖
顾名思义,就是指这个函数是否被测试代码调用了。以下面的代码为例
,对函数exchange要做到覆盖,只要一个测试——如expect(exchange(2, 2)) 就可以了。如果连函数覆盖都达不到,那这个函数是否真的需要。
 let z = 0
  if (x>0 && y>0) {
    z=x
  }
  return z
}

Line Coverage 语句覆盖
还是前面那个 exchange 例子,他检测的是某一行代码是否被测试覆盖了,同样 选择用例2,2也能覆盖它,但是如果变成 2, -1 就不行了。通常这种情况是由于一些分支语句导致的,因为相应的问题就是“那行代码(以及它所对应的分支)需要吗?

Decision Coverage 决策覆盖
它是指每一个逻辑分支是否被测试覆盖了,有一个if的真和假一般就要两组用例,至少测一组 true 一组 false

Condifiton Coverage 条件覆盖
它是指分支中的每个条件是否被测试覆盖了,像前面那个exchange例子,要达到全部条件覆盖,测试用例就需要四个,即 x 和 y 四种情况,如果测不到就要思考是否不需要某个分支呢

代码变异测试
说到这里重新提一下 jest 的 toMatchSnapshot 实践,他对期望的表达并不是写一个期望值和实际做匹配,而是生成一个快照让我们之后的每次变异代码和它匹配, jest--watch 的实时测试变动的代码更方便做这个事。
这里所谓的变异是指修改一处代码来改变代码的行为,检查测试是否因为这个代码的变异而失败,如果有失败则说明这个变异被消灭,此时的测试本身行为是符合预期。不然变异存活则测试不到位。
平时用到比较多的变异方法是:
条件边界变异、反向条件变异、数学运算变异、增量运算变异、负值翻转变异等

小结
养成写测试的好习惯能避免很多问题,极大的提升效率,避免重复 debug。在前端开发中由于语言本身对写法限制比较弱,测试保障非常重要,既让自己对代码有信心也让别人更容易理解你设计的每一个模块用意。在写代码的时候就要从可测试如何测试的角度思考,尽量每一行代码都是有用且符合预期的。
  • 大小: 33.4 KB
  • 大小: 58 KB
  • 大小: 62.5 KB
  • 大小: 36.1 KB
来自: coding
0
0
评论 共 0 条 请登录后发表评论

发表评论

您还没有登录,请您登录后再发表评论

相关推荐

  • 浅谈如何进行有效的前端测试

    逻辑性强,测试方便等 聊到这导致了好多前端从来不写测试(测试全靠手点~~~) 其实没必要达到测试驱动开发的程度,只要写完代码可以补测试,并且补出高效的测试,前端或许真的不需要手点 大前端时代不谈环境不成...

  • 聊聊前端如何玩测试

    再简单聊下接口测试: 功能测试:接口是否按照设计文档实现 逻辑业务测试:接口之间调用先后顺序,是否会对逻辑业务产生影响 异常:传参异常,数据异常 接口性能:接口单次调用的响应时间、返回数据量等 安全性测试...

  • 聊聊我对测试开发岗的理解

    说那么多,就是想给一些还在迷茫,或者未来可能迷茫的同学说,大厂测开VS小厂开发很正常,但是不要被一时的...有实习测试回来找开发的,也有实习开发回来嫌累找测试的。鄙视链什么的听听就好,自己喜欢,能接受就行了。

  • 测试开发必学Vue,开发百度网站

    如果您是Vue开发的新手,您可能已经听过很多关于它的专业术语了,例如:单页面应用程序、异步组件、服务器端呈现等。 另外您可能还经常听到和Vue一起提到的工具和库,如Vuex、Webpack、Vue CLI和Nuxt。 也许您在...

  • 开发比软件测试好吗,前端开发比软件测试发展好吗?

    为了解决这个问题,小编就和大家聊一聊前端开发和软件测试到底哪个更好,他们的发展前景怎么样。前端方面非常稀缺人才,如果懂H5那是非常好的了,而且待遇也不低,不过浏览器兼容性也是前端er的噩梦测试虽然入门门槛低,...

  • 简单聊聊微前端

    通常,项目有不同大小的体积和不同的要求,如果你的项目很简单,只有简单的一些页面,一个团队可以全部搞定,那完全没有必要使用微前端,市面上有许多框架可以快速完成这个项目开发。但是,如果你的应用,是另一个大...

  • 聊一聊前端业务开发

    在我从事 2 年团队基础工具建设后,最近 3 个月我的主要精力投入在了业务开发上。...对于前端开发而言,大部分需求的实现方式是类似的,可重合的。即使需求时间本身并不着急,但对于前端开发者而言,还是希望...

  • 聊聊我遇到的前端和产品经理

    接下来,产品经理会根据需求画出产品的原型图,然后会拉来技术进行需求评审,大概会包含前端、后端、测试、UI、算法等相关部门的人员。来一起讨论需求能否实现,以及开发工期、排期、上线时间等。然

  • 聊一聊我对测试开发的看法

    前言 在一线大厂,没有测试这个岗位,只有测开这个岗位 即使是做业务测试,那么你的title也是测开 所以想聊一聊测开的看法 但不代表这是正确的看法,仅供参考 ...况且测试平台要会前端还得会后端,你都这么全栈为...

  • 聊一聊前端程序员的现状与挑战

    聊一聊前端程序员的现状与挑战

  • 前端开发模式之vue练手小demo

    今天和同事在一起聊天,突然聊到了前端的开发模式,一推人在一起探讨我们经过讨论后,一致认为前端的开发大致的开发过程跟下面这张图差不多,欢迎大家一起讨论,提出不同的建议. 以下附上一个小demo欢迎大佬前来指导 ...

  • 前端开发创新实践

    近年来,前端技术领域迅猛发展,各种新技术、框架、工具,层出不穷。业内众多优秀企业保持积极探索精神,对现有工具、技术进行了深入探索,甚至缘于研发工作需求进行了升级研发与深度定制。本期我们集合多位前端技术...

  • 如何进行前端自动化测试?

    转自专栏:[从入门到不放弃]多浏览器的自动化测试(1)-本地测试 - 知乎专栏[从入门到不放弃]多浏览器的自动化测试(2)-云服务测试前端之殇 要是你碰到前端工程师朋友,那聊聊浏览器的兼容性准是没错,这和碰到英国朋友...

  • 最实用的web前端开发知识框架图

    web前端的知识点非常多,也非常散,需要好几层结构来组织这个体系,否则就会显得很乱。那么如何组织、把谁和谁放在一块儿?这是真正值得我们去思考的,你也可以自己来思考一下这个问题。 在我总结的这个知识框架中...

  • 简单聊聊嵌入式软件测试

    简单聊聊嵌入式软件测试,帮助小伙伴们找到方向,认清大门,然后进行较好的职业发展!

  • 聊聊前端项目重构

    添加单元测试 添加前端监控 二、参考monorepo的思想,合并项目、拆分组件 1.合并项目 其实对于我们团队的这三个项目,有很多公共的组件,就要考虑是否可以将这些公用组件提取,提高项目可维护性。具体操作方式,...

  • 深聊测试开发之:从订单支付流程来聊一聊,如何预防重复支付,建议收藏。

    如果作为产品的产品/开发/测试人员,遇到这种事情,先要给顾客退款,再进行总结,到底是哪个环节没做到位,最后解决问题,毕竟谁都不喜欢被重复“消费”。 我是小鱼: - CSDN博客专家; - 51Testing认证讲师; 关注...

  • 前端测试一共有哪几种?

    最近有不少朋友找到我聊了聊测试相关的内容,发现他们对测试的分类有些迷茫。实际上测试一共就 3 种:E2E,集成,单测,其它的功能测试、UI 测试、界面测试只是它们中里面的一种。 Kent C. Dodds 在这篇文章 ...

  • 前端(一)——前端开发遇到的普遍问题以及解决策略

    前端框架如何简化视图层开发? Vue.js是基于哪种架构模式实现的? Vue 2和Vue 3的区别是什么? 为什么现在很少有人在UniApp中使用Vue 3? 为什么不直接使用Vue.js开发跨平台应用,而要使用UniApp呢? Vue2,Vu

Global site tag (gtag.js) - Google Analytics