6

在前后台开发的项目中,我们当前已经做到了后台依靠单元测试,完全的脱离前台进行开发。那么,在进行单台开发时,是否也可以做到只依赖于UML图,不依赖于后台进行独立的开发呢?答案是肯定的。

本文旨在带领你:在前台完全脱离后台开发路上更近一步。

前期代码准备

在此,我们以近期开发的Alice学生管理系统为例,将学院管理的index组件拿出来,以展示脱离后台的前台。
文件列表:

CollegeIndexComponent: 学院管理INDEX列表组件
College: 学院实体
CollegeService: 学院服务
其它辅助文件略

CollegeIndexComponent 学院管理INDEX列表组件

import {Component, OnInit} from '@angular/core';
import {CollegeService} from '../../../../../service/college.service';
import {College} from '../../../../../entity/college';
import {Page} from '../../../../../entity/page';

@Component({
    selector: 'app-college-index',
    templateUrl: './college-index.component.html',
    styleUrls: ['./college-index.component.css']
})
export class CollegeIndexComponent implements OnInit {

    page: number;

    size: number;

    total: number;
    collegeList: Array<College>;

    constructor(private collegeService: CollegeService) {
    }

    ngOnInit() {
        console.log(this.collegeService);
        this.collegeList = new Array<College>();
        this.page = 1;
        this.size = 5;
        this.queryPage();
    }

    queryPage() {
        this.collegeService.getCollegeByPage(this.page - 1, this.size)
            .subscribe((data: Page<College>) => {
                this.collegeList = data.content;
                this.total = data.totalElements;
            }, () => {
                console.log('network error');
            });
    }
}

College: 学院实体

import {Teacher} from './teacher';
import {Major} from './major';

/**
 * 学院实体
 */
export class College {
    id: number;                 // id
    name: string;               // 名称
    teacherList: Teacher[];     // 负责教师
    majorList: Major[];

    [propName: string]: any;

    /**
     * 获取学院实体
     */
    static instance(): College {
        const college = new College();
        college.id = 1;
        college.name = '测试学院';
        college.teacherList = new Array<Teacher>(
            Teacher.instance()
        );
        college.majorList = new Array<Major>();
        return college;
    }
}

CollegeService: 学院服务

import { Injectable } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { College } from '../entity/college';
import { Observable } from 'rxjs';
import { Page } from '../entity/page';

@Injectable({
    providedIn: 'root',
})
export class CollegeService {
    baseUrl = 'College';

    constructor(protected http: HttpClient) {}
    /**
     *获取分页数据
     */
    public getCollegeByPage(page: number, size: number): Observable<Page<College>> {
        const params = {
            page: page.toString(),
            size: size.toString(),
        };
        return this.http.get<Page<College>>(this.baseUrl + '/page', {
            params: params,
        });
    }
}

单元测试

代码如下:

import {async, ComponentFixture, TestBed} from '@angular/core/testing';

import {CollegeIndexComponent} from './college-index.component';
import {NzGridModule, NzDividerModule, NzTableModule, NzFormModule} from 'ng-zorro-antd';
import {HttpClientModule} from '@angular/common/http';
import {RouterTestingModule} from '@angular/router/testing';
import {FormsModule, ReactiveFormsModule} from '@angular/forms';

/**
 * 对测试的描述:CollegeIndexComponent
 */
describe('CollegeIndexComponent', () => {
    // 定义两个变量
    let component: CollegeIndexComponent;
    let fixture: ComponentFixture<CollegeIndexComponent>;

    beforeEach(async(() => {
        // TestBed工作台
        // TestBed.configureTestingModule 配置要测试的模块
        // 这贴近于现实生活。现实生活中,我们测试一块电表是否正确.
        // 1. 需要一个专门用于测试的工作台
        // 2. 需要对这个测试的工作进行配置,以让其符合测试电表的基础工作
        TestBed.configureTestingModule({
            // 声明要上工作台的组件
            declarations: [CollegeIndexComponent],
            // 配置测试的依赖,没有这些模块,测试就进行不了。
            // 比如我们测试电表是否准确,需要有交流电,需要有电流表,需要有电压表等
            imports: [NzGridModule,
                HttpClientModule,
                NzDividerModule,
                NzTableModule,
                RouterTestingModule,
                ReactiveFormsModule,
                FormsModule,
                NzFormModule]
        }).compileComponents(); // 配置完后,开始编译要测试组件
    }));

    // 每次测试前,均执行一遍
    beforeEach(() => {
        // fix = 固定。用工作台创建一个供测试的组件。
        // 由于其想要被测试,就必然还需要其它的一些脚手架。
        // 我们把脚手架与被测试的组件组成的联合体称为:ComponentFixture 被固定的组件
        fixture = TestBed.createComponent(CollegeIndexComponent);

        // 实例化要测试的组件
        component = fixture.componentInstance;

        // 检测变化
        fixture.detectChanges();
    });

    /**
     * 测试方法的描述信息:should create
     */
    it('should create', () => {
        // 期待 组件 被成功创建
        expect(component).toBeTruthy();
    });
});

执行此单元测试,虽然能够通过,但将在控制台得到以下错误信息。

clipboard.png

clipboard.png

同时,展示的界面如下:

clipboard.png

我们看到,在测试时依赖于httpClient发起的请求,由于我们没有启动后台服务(即使启动了,使用绝对地址访问的方法也有问题),所以无法由后台获取数据。其实,即使是我们进行跨域的地址设置,由于后台我们往往会进行权限验证(是否登录,当前登录用户是否有当前操作的权限等),所以:想发起一个正常的请求,并不容易。

下面,让我们在不破坏原有服务层的基础上,自定义返回数据,从而规避http请求,达到不需要后台,便可以看到真实数据的目的。

自定义返回数据

在单元测试时,调用beforeEachit方法时均可以将@angular/core/testing -> injetct()传入。在此,我们以beforeEach为例,注入CollegeService,并在测试组件实例化以前,对其getCollegeByPage方法的返回值,进行重写。

 /**
     *  每次测试前,均执行一遍
     *  inject([], () => {}) 单元测试中的方法
     *  CollegeService 要注入的服务
     *  StudentService 要注入的服务 (仅用于展示注入多个服务)
     *  s为服务起的别名,类型为CollegeService,其对应注入的第一个参数
     *  t为服务起的别名,类型为StudentService,其对应注入的第二个参数(仅用于展示注入多个服务)
     */
    beforeEach(inject([CollegeService, StudentService], (s: CollegeService, t: StudentService) => {
        console.log(s);
        console.log(t);
        // fix = 固定。用工作台创建一个供测试的组件。
        // 由于其想要被测试,就必然还需要其它的一些脚手架。
        // 我们把脚手架与被测试的组件组成的联合体称为:ComponentFixture 被固定的组件
        fixture = TestBed.createComponent(CollegeIndexComponent);

        // 实例化要测试的组件
        component = fixture.componentInstance;

        // 检测变化
        fixture.detectChanges();
    }));

clipboard.png

使用如下方法改写返回值:

     /**
     *  每次测试前,均执行一遍
     *  inject([], () => {}) 单元测试中的方法
     *  CollegeService 要注入的服务
     *  StudentService 要注入的服务(仅用于展示注入多个服务)
     *  collegeService为服务起的别名,类型为CollegeService,其对应注入的第一个参数
     *  studentService为服务起的别名,类型为StudentService,其对应注入的第二个参数(仅用于展示注入多个服务)
     */
    beforeEach(inject([CollegeService, StudentService], (collegeService: CollegeService, studentService: StudentService) => {
        // 变量初始化
        const collegePage = new Page<College>();
        collegePage.content = new Array<College>(
            College.instance()
        );

        // 改写collegeService中的getCollegeByPage方法的返回值。
        spyOn(collegeService, 'getCollegeByPage').and.returnValue(of(collegePage));

        // fix = 固定。用工作台创建一个供测试的组件。
        // 由于其想要被测试,就必然还需要其它的一些脚手架。
        // 我们把脚手架与被测试的组件组成的联合体称为:ComponentFixture 被固定的组件
        fixture = TestBed.createComponent(CollegeIndexComponent);

        // 实例化要测试的组件
        component = fixture.componentInstance;

        // 检测变化
        fixture.detectChanges();
    }));

此时,我们再次测试,正常的通过了测试,控制台没有报错,而且:我们在浏览器上看到了我们的组件。最关键的是:我们即没有使用后台,也没有改变CollegeService中的代码。

clipboard.png

参考文献:
深度好文

言简意赅

官方文档


潘杰
3.1k 声望238 粉丝