Vue下使用jest进行单元测试

在前端开发逐渐复杂的今天,当然也需要对前端进行测试。单元测试主要用于白盒测试,检测程序的最小单位的正确性,通常来说就是验证函数的输入和输出。

# vue 集成 jest

vue-cli 已经提供现成的插件 (opens new window),我们可以很方便的进行集成。官方文档也有讲述如何 debug。在使用 jest 之前,我们需要知道 jest 的测试代码不是像前端代码一样运行在浏览器环境,而是使用 node 运行,并且浏览器的 DOM 环境使用jsdom (opens new window)来模拟,所以在集成测试的时候需要解决不少问题。

# jest 与 webpack

当今的前端开发都离不开 webpack 进行打包,我们使用各种各样的 loader 来处理代码,所以可以使用例如import语法来引用 JS 文件,但是 node 环境目前还不支持原生的 es6 import文件。所以在使用 jest 的时候,我们需要用 babel 进行代码转换,好在 @vue/cli-plugin-unit-jest 都已经给配置好了,使用预置的 preset 就行。我是基于 TS 开发,所以用的 preset 是 @vue/cli-plugin-unit-jest/presets/typescript-and-babel。但是我们在代码里使用的一些 webpack 的特殊语法就不太好对应了,例如我有使用inline loader (opens new window)来打包特殊的 SVG 文件到代码中,但是在测试的时候就会遇到无法 import 的尴尬问题。这时候的解决方案就是使用moduleNameMapper:

moduleNameMapper: {
    '^!svg-inline-loader!@(.*)$': require.resolve('jest-transform-stub')
}
1
2
3

通过把 import 的资源文件映射为空,就能解决 jest 运行出错的问题,毕竟我们在做单元测试的时候不会检测资源文件。

# jest mock vuex store

做单元测试的时候,mock 是非常常见的需求,例如我们在一个文件里引用了 store 的数据,但是要构造这个数据非常复杂,所以我们需要直接 mock 数据。这里需要注意的是,如果是 import 引入的话,我们需要先 mock 再使用动态引用的方式加载测试模块,保证是在 import 之前 mock 数据。

jest.doMock('@/store', () => ({
  state: {
    account: {
      hello: 'world'
    }
  }
}));
const { default: formatter } = await import('@/utils/formatter');
1
2
3
4
5
6
7
8

不过,mock 之后记得 reset:

beforeEach(() => {
  jest.resetModules();
});
1
2
3

# jest mock navigator

有些测试用例中,我们需要 mock 用户的浏览器是不同语言,这个时候就需要使用spyOn来修改属性的返回值。示例代码如下:

describe('test i18n', () => {
  let languageGetter: jest.SpyInstance;
  beforeEach(() => {
    jest.resetModules();
    languageGetter = jest.spyOn(window.navigator, 'language', 'get');
  });

  it('set lang is zh-CN', () => {
    languageGetter.mockReturnValue('zh-CN');
    console.log(navigator.language); // zh-CN
  });
});
1
2
3
4
5
6
7
8
9
10
11
12

# jest mock vue router

测试 Vue 组件主要依靠Vue Test Utils (opens new window)。官方文档也有介绍如何使用,这个仅给出一个例子来描述用法:

import { mount, createLocalVue } from '@vue/test-utils';
import VueRouter from 'vue-router';

import App from '@/App.vue';
import routes from '@/router/routes';
import HelloWorld from '@/views/HelloWorld.vue';

const localVue = createLocalVue();
localVue.use(VueRouter);

describe('Test index routes', () => {
  it('should find HelloWorld when router is HelloWorld', async () => {
    const router = new VueRouter({ routes, mode: 'history' });
    await router.push('/helloworld');

    const wrapper = mount(App, {
      localVue,
      router
    });

    expect(wrapper.find(HelloWorld).exists()).toBe(true);
  });
});
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23

这里使用 localVue 的原因是项目中只有一个 Vue 实例,直接使用的话会污染原本的 Vue,所以要在测试代码里手动创建router。这里要注意的是,如果 find 是某个组件,需要在 Vue 组件里面显示声明name,否则在 Lazy Loading 的情况下是无法找到组件的。还有一个指得注意的是router.push 是一个 Promise 异步操作,所以如果写成同步的话,会一直找不到组件的。

# 总结

jest 集成了很多测试的工具,只装一个工具便可进行单元测试。但是由于不是运行在浏览器环境,导致jsdom不支持的特性都需要自己来处理。所以希望选择一个在浏览器运行单元测试的框架应该会少遇到很多坑吧。特别是用了很多浏览器特性的前端项目,测试起来更加折腾。

# 引用

Using with webpack (opens new window)

Test Vue Router (opens new window)

前端开发如何防止逆向以及对策

前端开发分析代码的途径最简单不过了,第一个途径是查看网页源代码,第二个途径是打开开发者工具审查元素。

# 防止查看源代码

现在网站的基本对策是禁用用户右键选项菜单来避免查看源代码。首先,源代码肯定是能查看到的。采取的措施也只不过是掩耳盗铃而已。解决对策也有很多,我随便说几个。

第一,针对 Chrome 浏览器,我们可以直接在地址栏里输入 view-source:https://www.google.com/ 这种方式来查看源代码。第二,我们可以使用其他工具,例如curl, wget来直接下载源代码。这种是很初级的防范对策。

# 防止审查元素

现代化的前端开发,都是基于 JS 来渲染页面元素的,有时候我们查看源代码也只是看到了 JS 文件的引用。没有太大意义,所以需要浏览器渲染之后查看真正的 DOM 元素才有意义。这时候如何禁止审查元素就是另外一种对策了。 现在用来检测打开开发者工具主要是通过判断console.log的执行,因为不打开开发者工具的话console.log是不会执行的。所以有些网站就可以利用这个来阻止用户打开开发者工具审查元素和 Debug 代码。 下面给出一个例子来:

<script>
let el = new Image();
Object.defineProperty(el,'id',{get: function(){window.location.href="http://www.bimibimi.tv"}});
console.log(el);
</script>
1
2
3
4
5

这是一个非常巧妙的例子,当执行console.log输出 Image 元素的时候,比如会访问 id 属性,从而触发后面的重定向 URL 代码。所以表现就是用户试图打开开发者面板就会被自动定位到首页。

要破解这个的话,只能在页面执行之前注入代码修改 Image 函数的使用:

window.Image = function() {};
Object.defineProperty(window, 'Image', { writable: false });
1
2

通过注入上面的代码,来规避自动跳转到首页。如何注入呢? 就需要利用 Chrome 的扩展功能,详细代码请参照bimibimi-hack (opens new window)

# 总结

前端代码逆向和反逆向,永远没有银弹。只能说是提高逆向的门槛,但想完全防止是不可能的。如果自己编译一个浏览器,修改浏览器的默认行为的话,就没有不能调试的网站。

# 引用

Find out whether Chrome console is open (opens new window)

Source code of bimibimi.tv

前端部署之CDN的那些事情

现在的前端开发主流是 PWA,简而言之就是像 App 一样使用。基于 PWA 技术可以离线访问,并且可以添加程序到菜单里。离线技术使用的是比较成熟的 Service Workers。但是配合 CDN 就有很多不得不注意的事情。

# CDN 部署静态资源

现代化前端开发主要是把 CSS,JS,图片等静态资源放在 CDN 上有效提高载入速度。把静态 HTML 文件放在服务器上,有利于更新网站最新状态,例如进入维护状态,直接把静态 HTML 文件替换为维护页面即可。

# 同源策略问题

正常情况下,资源文件和主网站不在同一个域名下,例如主网站是icehoney.me,资源网站是static.icehoney.me。当然了,资源文件使用 GET 方法浏览器自动载入,没有任何问题。但是注册 Service Worker 的 JS 文件是不能放在 CDN 上的,因为这是官方的规定。个人理解是出于安全因素的考虑。所以我们在主服务器上不仅要放静态的 HTML 文件,还要放一个 serive-worker.js 文件。

# SVG symbol 的使用问题

现在前端的开发针对很多小图标,都是采用 SVG 的方式来引用。其中 SVG symbol 特别好用,可以在页面里嵌入一个 inline 的 SVG,然后在其他对方使用<use>标签来引用。但是对于放在 CDN 上的 SVG 文件,不能直接使用下面的形式:

<use xlink:href="https://static.icehoney.me/icon.svg#china" />
1

如果引用 CDN 的 SVG 浏览器会报错,解决方案是使用svg-inline-loader,手动在 HTML 里面注入 SVG 文件。document.body.insertAdjacentHTML 方法可以注入 inline 的 SVG。

# Workbox 配置

首先,PWA 项目的路由都是由前端来处理,所以我们需要使用navigateFallback保证不管导航到哪个路由都能映射到缓存的 HTML 文件。然后是使用runtimeCaching配置缓存服务器请求。

# 总结

由于 Service Workers 的导入使得 CDN 部署变得有些麻烦,不过这些问题好在都能找到解决方案。还有一点是 Chrome 的 Network 面板里的 offline 模式不能测试 Service Workers 的离线。需要自己手动拔网线才能测试。

# 引用

Is it possible to serve service workers from CDN/remote origin? (opens new window)

Workbox webpack Plugins (opens new window)