Vue_TS_测试

2019-10-22  本文已影响0人  LM林慕

此文项目代码: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'));

面向对象:

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,算是测试的雏形

编写测试代码的好处

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


准备工作

在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介绍

测试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了解即可。

你的赞是我前进的动力

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

上一篇下一篇

猜你喜欢

热点阅读