Vue_TS_测试

此文项目代码:https://github.com/bei-yang/I-want-to-be-an-architect
码字不易,辛苦点个star,感谢!

引言


此篇文章主要涉及以下内容:

  1. TypeScript
  2. 使用TypeScript编写vue应用
  3. vue测试
  4. 写易于测试的vue组件和代码

学习资源


TypeScript


TypeScript是JavaScript的超集,它可编译为纯JavaScript,是一种给JavaScript添加特性的语言扩展。ts有如下特点:

  • 类型注解和编译时类型检查
  • 基于类的面向对象编程
  • 泛型
  • 接口
  • 声明文件

ts和es6

typescript是angular2的开发语言,Vue3正在使用TS重写

准备工作

新建一个基于ts的vue项目

vue create vue-ts

选项选择:

  • 自定义选项
  • 添加ts支持
  • 基于类的组件
  • tslint

浏览基本项目结构
新建一个组件,components/Hello.vue

/* 展示模板 */
<template>
  <div id='app'>
  </div>
</template>
<script lang='ts'>
//导入组件
import Vue from 'vue'
export default Vue.extend({
  name: 'App'
})
</script>
<style>
/* 样式代码 */
#app {
}
</style>

在App.vue中引入

import Hello from './components/Hello.vue'
@component({
  components:{
    HelloWorld,
    Hello
  }
})
export default class App extendds Vue{}

类型注解和编译时类型检查

定义变量后,可以通过冒号来指定类型注解

//  Hello.vue
let name='xxx';  //  类型推论
let title:string= '123455';  // 类型注解
name=2;  //  错误
title=4;  // 错误

数组类型

let names:string[];
names=['Tom'];  //  或Array<string>

任意类型

let foo:any='xxx'
foo=3

//  any类型也可用于数组
let list:any[]=[1,true,'free'];
list[1]=100;

函数中使用类型

function greeting(person:string):string{
  return 'Hello'+person;
}
// void类型,常用于没有返回值的函数
function warnUser():void{alert('this is my warning message');}

内置的类型
1. string
2. number
3. boolean
4. void函数不返回值
5. any任意类型

范例,Hello.vue

<template>
  <div>
    {{msg}}
    {{foo}}
    <p>
      <input type="text" placeholder="输入特性名称" @keyup.enter="addFeature">
    </p>
    <ul>
      <li v-for="f in features" :key="f.id">{{f.name}}</li>
      <li>特性数量:{{featureCount}}</li>
    </ul>
  </div>
</template>

<script lang='ts'>
import { Component, Prop, Vue, Emit,Watch } from "vue-property-decorator";

export class Feature {
  constructor(public id: number, public name: string, public version: string) {}
}

interface Result<T> {
  ok: 0 | 1;
  data: T[];
}
// 泛型函数
function getData<T>(): Promise<Result<T>> {
  const data: any[] = [
    { id: 1, name: "类型注解", version: "2.0" },
    { id: 2, name: "编译型语言", version: "1.0" }
  ];
  return Promise.resolve({ ok: 1, data } as Result<T>);
}

@Component({
  props: {
    // 属性也可以在这里配置
    sname: {
      type: String,
      default: "匿名"
    }
  }
})
export default class Hello extends Vue {
  // private 仅当前类可用
  // protected 子类也可以用
  // public  都可以用
  @Prop() private msg!: string; // 属性msg必填项,字符串类型
  @Prop({ default: "匿名" }) private foo?: string; // 属性foo必填项,字符串类型

  // 普通的属性相当于组件data
  private features: Feature[] = [];

  // 生命周期
  async created() {
    //...
    const result = await getData<Feature>();
    this.features = result.data;
  }

  // 计算属性
  get featureCount() {
    return this.features.length;
  }

  @Emit()
  private addFeature(event: any) {
    // 若没有返回值形参将作为事件参数
    const feature = {
      name: event.target.value,
      id: this.features.length + 1,
      version: "1.0"
    };
    this.features.push(feature);
    event.target.value = "";
    return feature; // 返回值作为事件参数
  }


@Watch('msg')
onRouteChange(val:string, oldVal:any){
    console.log(val, oldVal);
}
  //   addFeature(event: any) {
  //     console.log(event);

  //     this.features.push({
  //       name: event.target.value,
  //       id: this.features.length + 1,
  //       version: "1.0"
  //     });
  //     event.target.value = "";
  //   }
}

// 类型注解
let title: string;
let name = "xx"; // 类型推论

// 数组类型
// let names: Array<string>;
let names: string[];
names = ["tom", "jerry"];
// names[0] = 1; // 错误

// 任意类型
let list: any[] = [1, true, "free"];
list[0] = "lala";

// 函数中使用
function greeting(person: string): string {
  return "hello, " + person;
}
greeting("tom");

// void类型
function warn(): void {
  alert("warning!!!");
}

// 内置类型:string,number,boolean,void,any

// ts函数中如果声明,就是必选参数
function sayHello(name: string, age: number = 20): string {
  return name + " " + age;
}
sayHello("tom", 20);
sayHello("tom");

// 函数重载:多个同名函数,通过参数数量或者类型不同或者返回值不同
function info(a: { name: string }): string;
function info(a: string): object;
function info(a: any): any {
  if (typeof a === "object") {
    return a.name;
  } else {
    return { name: a };
  }
}
info({ name: "jerry" });
info("jerry");

class Shape {
  readonly foo: string = "foo";
  protected area: number;

  constructor(public color: string, width: number, height: number) {
    this.area = width * height;
  }

  shoutout() {
    return (
      "I'm " + this.color + " with an area of " + this.area + " cm squared."
    );
  }
}

class Square extends Shape {
  constructor(color: string, side: number) {
    super(color, side, side);
    console.log(this.color);
  }
  shoutout() {
    return "我是" + this.color + " 面积是" + this.area + "平方厘米";
  }
}
const square: Square = new Square("blue", 2);
console.log(square.shoutout());

class Employee {
  private firstName: string = "Mike";
  private lastName: string = "James";

  get fullName(): string {
    return this.firstName + " " + this.lastName;
  }
  set fullName(newName: string) {
    console.log("您修改了用户名");
    this.firstName = newName.split(" ")[0];
    this.lastName = newName.split(" ")[1];
  }
}
const employee = new Employee();

employee.fullName = "Bob Smith";

// 接口约束结构
interface Person {
  firstName: string;
  lastName: string;
  sayHello(): string; // 要求实现方法
}
// 类实现接口
class Greeter implements Person {
  constructor(public firstName = "", public lastName = "") {}
  sayHello() {
    return "Hello, " + this.firstName + " " + this.lastName;
  }
}
function greeting2(person: Person) {
  return "Hello, " + person.firstName + " " + person.lastName;
}
const user = { firstName: "Jane", lastName: "User", sayHello: () => "lalala" };
const user2 = new Greeter("a", "b");
console.log(user);
console.log(greeting2(user2));
</script>

<style scoped>
</style>

函数

必填参:参数一旦声明,就要求传递,且类型需符合

function sayHello(name:string,age:number):string{
  console.log(name,age)
}
sayHello(11,12)  // 报错,与指定类型不一致
sayHello('xxx','xxx')  //  报错,与指定类型不一致

可选参数:参数名后面加上句号,变成可选参数

function sayHello(name:string,age?:number):string{
  console.log(name,age)
}

参数默认值

function sayHello(name:string,age:number=20):string{
  console.log(name,age)
}

函数重载:以参数数量或类型区分多个同名函数

function add(a:number,b:number):string;
function add(a:string,b:string):string;
function add(a:any,b:any):string{
  if(typeof a==='number'){
    return a+b;
  }else{
    return a+b
  }
}
console.log(add(1,1));
console.log(add('foo','bar'));

面向对象:

  • 通过extends实现继承
  • 使用public等访问修饰符实现封装
  • 通过方法覆盖实现多态
class Shape {
  readonly foo: string = "foo";
  protected area: number;

  constructor(public color: string, width: number, height: number) {
    this.area = width * height;
  }

  shoutout() {
    return (
      "I'm " + this.color + " with an area of " + this.area + " cm squared."
    );
  }
}

class Square extends Shape {
  constructor(color: string, side: number) {
    super(color, side, side);
    console.log(this.color);
  }
  shoutout() {
    return "我是" + this.color + " 面积是" + this.area + "平方厘米";
  }
}
const square: Square = new Square("blue", 2);
console.log(square.shoutout());

注意事项:

  • 私有private:当成员被标记成private时,它就不能在声明它的类的外部访问。
  • 保护protected:protected成员在派生类中仍然可以访问。
  • 只读readonly:只读属性必须在声明时或构造函数里被初始化。
  • 参数属性:给构造函数的参数加上修饰符,能够定义并初始化一个成员属性。
 class Animal{
   constructor(private name:string){//  定义name属性并将构造参赋值给他}
 }
  • 存取器:当获取和设置属性时有额外逻辑时可以使用存取器(又称getter、setter)
class Employee{
 private _fullName:string='Mike James';
 get fullName():string{
   return this._fullName;
 }
 set fullName(newName:string){
   console.log('您修改了用户名');
   this._fullName=newName;
 }
}
const employee=new Employee();
employee.fullName='Bob Smith';

范例代码:通过类可以声明自定义类型约束数据结构,Hello.vue

// 定义一个特性类,拥有更多属性
class Feature {
  constructor(public id: number, public name: string, public version: string) {}
}

// 可以对获取的数据类型做约束
@Component
export default class HelloWorld extends Vue {
  private features: Feature[]

  constructor() {
    super()
    this.features = []
  }

  created() {
    setTimeout(()=>{
      // 数据结构相同即可,不必是Feature实例
      this.features=[
        {id:1,name:'类型注解',version:'2.0'},
        {id:2,name:'编译型语言',version:'1.0'}
      ];
    },1000)
  }
}

// template中的变化
<li v-for="feature in features" :key="feature">{{feature.name}} ,{{feature.version}}</li>

范例:利用getter设置计算属性

<li>特性数量:{{count}}</li>

get count(){
  return this.features.length;
}

接口

接口仅约束结构,不要求实现,使用更简单

interface Person {
  firstName: string
  lastName: string
}
function greeting(person:Person){
  return 'Hello,'+person.firstName+" "+person.lastName;
}
const user={firstName:'Jane',lastName:'User'};
console.log(user)
console.log(greeting(user))

面向接口编程

interface Person {
  firstName: string
  lastName: string
  sayHello(): string // 要求实现方法
}
// 实现接口
class Greeter implements Person {
  constructor(public firstName = '', public lastName = '') {}
  sayHello() {
    return 'Hello,' + this.firstName + ' ' + this.lastName
  }
}
// 面向接口编程
function greeting(person: Person) {
  return person.sayHello()
}
// const user = { firstName: 'Jane', lastName: 'User' }
const user = new Greeter('Jane', 'User') // 创建对象实例
console.log(user)
console.log(greeting(user))

范例:修改Feature为接口形式

<script lang='ts'>
// 接口中只需定义结构,不需要初始化
interface Feature{
  id:number;
  name:string;
  version:string;
}
</script>

泛型

泛型(Generics)是指在定义函数、接口或类的时候,不预先指定具体的类型,而在使用的时候再指定类型的一种特性。

// 不用泛型
// interface Result {
//   ok: 0 | 1
//   data: Feature
// }

//  使用泛型
interface Result<T> {
  ok: 0 | 1
  data: T[]
}

范例:使用泛型约束接口返回类型

// Hello.vue
function getData<T>(): Result<T> {
  const data: any[] = [
    { id: 1, name: '类型注解', version: '2.0' },
    { id: 2, name: '编译性语言', version: '1.0' }
  ]
  return { ok: 1, data }
}

使用接口

created(){
  this.features=getData<Feature>().data;
}

甚至返回Promise

function getData<T>(): Promise<Result<T>> {
  const data: any[] = [
    { id: 1, name: '类型注解', version: '2.0' },
    { id: 2, name: '编译型语言', version: '1.0' }
  ]
  return Promise.resolve({ ok: 1, data } as Result<T>)
}

使用

async created(){
  const result=await getData<Feature>();
  this.features=result.data;
}

装饰器

装饰器实际上是工厂函数,传入一个对象,输出处理后的新对象。
典型应用是组件装饰器@Component

@Component
export default class Hello extends Vue {}

若不加小括号,则装饰器下面紧挨着的对象就是目标对象

如果装饰器需要配置,则要以函数形式使用并传入配置

@Component({
  props:{ //  属性也可以在这里配置
    name:{
      type:String,
      default:'匿名'
    }
  }
})
export default class Hello extends Vue {}

// 类似的还有App.vue中配置的依赖组件选项components

范例:事件处理@Emit
新增特性时派发事件通知父组件,Hello.vue

// 通知父类新增事件,若未指定事件名则函数名作为事件名(羊肉串形式)
@Emit()
private addFeature(event:any){  // 若没有返回值形参将作为事件参数
  const feature={name:event.target.value,id:this.features.length+1};
  this.features.push(feature);
  event.target.value='';
  return feature; // 返回值作为事件参数
}

父组件接收并处理,App.vue

@Watch('msg')
onRouteChange(val:string,oldVal:any){
  console.log(val,oldVal);
}

测试


测试分类

常见的开发流程里,都有测试人员,这种我们成为黑盒测试,测试人员不管内部实现机制,只看最外层的输入输出,比如你写一个加法的页面,会设计N个case,测试加法的正确性,这种代码里,我们称之为E2E测试
更负责一些的我们称之为集成测试,就是集合多个测试过的单元一起测试。
还有一种测试叫做白盒测试,我们针对一些内部机制的核心逻辑,使用代码进行编写,我们称之为单元测试

测试是前端开发人员进阶必备的技能
我们日常使用console,算是测试的雏形

编写测试代码的好处

  • 提供描述组件行为的文档
  • 节省手动测试的事件
  • 减少研发新特性时产生的bug
  • 改进设计
  • 促进重构

自动化测试使得大团队中的开发者可以维护复杂的基础代码。让你改代码不再小心翼翼。


准备工作

在vue中,推荐用Mocha+Chai或者Jest,演示代码使用Jest,它们语法基本一致
新建vue项目,手动选择特性,添加Unit Testing和E2E Testing



单元测试解决方案选择:Jest



端到端测试解决方案选择:Cypress

单元测试

单元测试(unit testing),是指对软件中的最小可测试单元进行检查和验证。
新建test/unit/test.spec.js,*.spec.js是命名规范,写一下代码:

function add(num1,num2){
  return num1+num2
}

// 测试套件 test suite
describe('test',()=>{
  // 测试用例 test case
  interface('测试add函数',()=>{
    // 断言assert
    expect(add(1,3)).toBe(3)
    expect(add(1,3)).toBe(4)
    expect(add(-2,3)).toBe(1)
  })
})

执行单元测试

npm run test:unit

api介绍

  • describe:定义一个测试套件
  • it:定义一个测试用例
  • expect:断言的判断条件
  • toBe:断言的比较结果

测试Vue组件


一个简单的组件

<template>
    <div>
    <span>{{ message }}</span>
    <button @click="changeMsg">点击</button>
    </div>
</template>

<script>
  export default {
    data () {
      return {
        message: 'vue-text'
      }
    },
    created () {
      this.message = 'test vue组件'
    },
    methods:{
        changeMsg(){
            this.message = '按钮点击'
        }
    }
  }
</script>
// 导入 Vue.js 和组件,进行测试
import Vue from 'vue'
import KaikebaComp from '@/components/Kaikeba.vue'

describe('KaikebaComp', () => {
  // 检查原始组件选项
  it('由created生命周期', () => {
    expect(typeof KaikebaComp.created).toBe('function')
  })

  // 评估原始组件选项中的函数的结果
  it('初始data是vue-text', () => {
    // 检查data函数存在性
    expect(typeof KaikebaComp.data).toBe('function')
    // 检查data返回的默认值
    const defaultData = KaikebaComp.data()
    expect(defaultData.message).toBe('vue-text')
  })
})

检查mounted之后

it('mount之后测试',()=>{
  const vm=new Vue(KaikebaComp).$mount()
  expect(vm.message).toBe('test vue组件')
}

用户点击

和写vue没什么本质区别,只不过我们用测试的角度去写代码,vue提供了专门针对测试的@vue/test-utils

it('按钮点击后',()=>{
  const wrapper=mount(KaikebaComp)
  wrapper.find('button').trigger('click')
  expect(wrapper.vm.message).toBe('按钮点击')
  //  测试HTML渲染结果
  expect(wrapper.find('span').html()).toBe('<span>按钮点击</span>')
})

测试覆盖率

jest自带覆盖率,如果用的mocha,需要使用istanbul来统计覆盖率
package.json里修改jest配置

'jest':{
  'collectCoverage':true,
  'collectCoverageFrom':['src/**/*.{js,vue}'],
}

在此执行npm run test:unit
可以看到我们kaikeba.vue的覆盖率是100%。

端到端测试E2E

借用浏览器的能力,站在用户测试人员的角度,输入框,点击按钮等,安全模拟用户,这个和具体的框架关系不大,完全模拟浏览器行为。

运行E2E测试

npm run test:e2e

E2E了解即可。

你的赞是我前进的动力

求赞,求评论,求分享...

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 216,163评论 6 498
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,301评论 3 392
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 162,089评论 0 352
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,093评论 1 292
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,110评论 6 388
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,079评论 1 295
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,005评论 3 417
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,840评论 0 273
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,278评论 1 310
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,497评论 2 332
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,667评论 1 348
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,394评论 5 343
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,980评论 3 325
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,628评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,796评论 1 268
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,649评论 2 368
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,548评论 2 352

推荐阅读更多精彩内容