實踐是檢驗真理的唯一標(biāo)準(zhǔn)。無論什么樣的選手,單元測試做的再好也難免會有想不到的地方。此時便需要集成測試來補(bǔ)刀了。什么是集成測試呢?簡單來說就是把幾個小的模塊組裝到一起,或是把一些單元測試的小的粒度組合到一起進(jìn)行測試。最簡單最不可靠的集成測試的方法便是本教程中采用的:人為驗證法。angular其實為我們提供了強(qiáng)大的集成測試工具,在angular中又被稱為`端對端的測試`,即` End to end `,由于英文的`2(two)`與`to`同音,所以又被稱為`e2e`。在angular的根目錄中為我們內(nèi)置了e2e的測試樣例,我們可以通過`ng e2e`或`ng e`來啟動它們。
> 你可能由于網(wǎng)絡(luò)原因?qū)г搯硬怀晒Γ唧w可參考:[https://segmentfault.com/a/1190000021216402](https://segmentfault.com/a/1190000021216402)解決。
e2e測試和單元測試不同,單元測試需要關(guān)注代碼的執(zhí)行情況,需要對變量的值,是否按我們預(yù)期的參數(shù)進(jìn)行調(diào)用,函數(shù)的返回值等分別進(jìn)行斷言。而e2e測試僅停留在界面上,通過模擬打開某個地址,模擬人為的操作最后斷言界面會產(chǎn)生什么樣的效果。受篇幅、教程難度設(shè)計及筆者水平的限制,在本教程中只求帶領(lǐng)大家接觸了解下更自動化的e2e測試,力求起到拋磚引玉的效果。
# 了解e2e測試
首次啟動e2e時,其將為我們下載最新的驅(qū)動,此過程的耗時取決于我們的網(wǎng)絡(luò)速度。e2e測試正式啟動后,將查找項目根目錄下的e2e文件夾下的src文件夾下的以`.e2e-sppec.ts`結(jié)尾的文件,比如angular在初始化時為我們自動生成的`app.e2e-spec.ts`

在angular為我們生成的示例中`app.po.ts`及`app.e2e-spec.ts`兩個文件相互配合使用:`app.po.ts`用于獲?。ㄔO(shè)置)應(yīng)用的值,而`app.e2e-spec.ts`則負(fù)責(zé)斷言。
e2e/src/app.po.ts
```
import { browser, by, element } from 'protractor';
export class AppPage { ①
navigateTo() { ②
return browser.get(browser.baseUrl) as Promise<any>; ③
}
getTitleText() {
return element(by.css('app-root .content span')).getText() as Promise<string>; ④
}
}
```
* ① 定義類名并export(否則其它文件無法調(diào)用它,也就失去了配合`app.e2e-spec.ts`的作用
* ② 定義方法
* ③ 打開瀏覽器首頁
* ④ 通過css選擇器,找到`app-root .content span`的`text 文本值`
e2e/src/app.e2e-spec.ts
```
import { AppPage } from './app.po';
import { browser, logging } from 'protractor';
describe('workspace-project App①', () => {
let page: AppPage;
beforeEach(() => { ②
page = new AppPage();
});
it('should display welcome message', () => { ③
page.navigateTo(); ④
expect(page.getTitleText()).toEqual('web-app app is running!'); ⑤
});
afterEach(async? () => { ?
// Assert that there are no errors emitted from the browser
const logs = await browser.manage().logs().get(logging.Type.BROWSER); ?
expect(logs).not.toContain(jasmine.objectContaining({ ?
level: logging.Level.SEVERE,
} as logging.Entry));
});
});
```
* ① 與單元測試一樣,為此測試啟個好記的名字
* ② 每次執(zhí)行測試用例前,執(zhí)行1次本方法
* ③ 測試用例
* ④ 調(diào)用輔助類中的方法,打開應(yīng)用首頁
* ⑤ 斷言輔助類獲取的頁面元素內(nèi)容為`web-app app is running!`
* ? 在每個測試用例執(zhí)行完畢后執(zhí)行
* ? 聲明該測試用異步測試
* ? 獲取瀏覽器報的錯誤
* ? 斷言錯誤列表中的錯誤類型沒有`logging.Level.SEVERE`
由于我們的項目早已不是angular生成的原始項目,所以在運(yùn)行此測試時會發(fā)生如下錯誤:

它在說通過相應(yīng)的CSS選擇器未找到任何的元素,我們對應(yīng)修正為:
e2e/src/app.po.ts
```
getTitleText() {
return element(by.css('app-root .content span')).getText() as Promise<string>; ?
return element(by.css('app-welcome h1')).getText() as Promise<string>; ?
}
```
e2e/src/app.e2e-spec.ts
```
import {browser, logging} from 'protractor';
...
it('should display welcome message', () => {
browser.sleep(1000); ?
page.navigateTo();
browser.sleep(2000); ?
expect(page.getTitleText()).toEqual('web-app app is running!'); ?
expect(page.getTitleText()).toEqual('歡迎使用河北工業(yè)大學(xué)教務(wù)管理系統(tǒng)'); ?
browser.sleep(2000); ?
});
```
* ? 為了更清晰的觀察測試執(zhí)行的過程,每執(zhí)行1步我們讓瀏覽器小睡一會
最后打開終端進(jìn)入項目文件夾,執(zhí)行`ng e2e`來啟動集成測試,網(wǎng)絡(luò)暢通的情況下我們將看到此測試自動打開chrome執(zhí)行相應(yīng)的測試程序,測試完成后主動的半閉瀏覽器以結(jié)束測試。

了解過程到此結(jié)束,如果你感覺到意猶未盡,可以來到protractortest的[官網(wǎng)](https://www.protractortest.org/#/)進(jìn)一步地學(xué)習(xí)。
# 添加路由
與添加其它的路由相同,打開student模塊的路由文件student-routing.module.ts,并添加如下路由:
```
const routes: Routes = [
{
path: 'add',
component: AddComponent
},
{ ?
path: '',
component: IndexComponent
}
];
```
# 添加菜單項
繼續(xù)打開nav組件,在菜單中添加'學(xué)生管理'菜單項:
nav/nav.component.ts
```
ngOnInit() {
this.title = '教務(wù)管理系統(tǒng)';
this.menus.push({url: 'teacher', name: '教師管理'});
this.menus.push({url: 'klass', name: '班級管理'});
this.menus.push({url: 'student', name: '學(xué)生管理'}); ?
}
```
使用`ng serve --open`啟動應(yīng)用,啟動瀏覽器控制臺,點(diǎn)擊`學(xué)生管理`菜單并查看報錯信息:

# 添加依賴
按錯誤提示增加student模擬的依賴:
student/student.module.ts
```
@NgModule({
declarations: [AddComponent, KlassSelectComponent, IndexComponent],
imports: [
CommonModule,
StudentRoutingModule,
ReactiveFormsModule,
FormsModule, ?
CoreModule
]
})
export class StudentModule {
}
```
點(diǎn)擊測試:

錯誤類型為網(wǎng)絡(luò)錯誤,此時便可以啟動后臺來進(jìn)一步進(jìn)行其它功能的驗證了。
# 啟動后臺
使用你最喜歡的方式來啟動后臺。
# 測試并修正其它內(nèi)容
在分模塊開發(fā)的情況下,若想保障各個模塊間的有效聯(lián)通是比較困難的事情。由于在開發(fā)過程中每個模塊將分配給不同的團(tuán)隊成員開發(fā),所以學(xué)生列表組件與新增學(xué)生組件可能是同步開發(fā)的。而各個模塊間的正常跳轉(zhuǎn)的前提則是:預(yù)跳轉(zhuǎn)的模塊是存在的。
## index組件 -> 新增組件
student/index/index.component.html
```
...
</form>
<div class="row">
<div class="col text-right">
<a class="btn btn-primary" routerLink="./add">新增學(xué)生</a>
</div>
</div>
<table>
...
```
接下來按 新增教師 -> 新增班級 -> 新增學(xué)生的順序測試添加學(xué)生功能:

測試過程中我們發(fā)現(xiàn)以下問題:
* 標(biāo)題應(yīng)該由 編輯教師 修正為 新增學(xué)生
* 新增學(xué)生完成后,點(diǎn)擊保存按鈕,界面未跳轉(zhuǎn)
* 學(xué)生管理列表組件的 table 沒有添加bootstrap樣式
## 修正標(biāo)題
請自行將新裝學(xué)生組件中 編輯老師 修正為 新增學(xué)生
## 新增組件 -> index組件
新增組件中點(diǎn)擊保存按鈕后,應(yīng)該成功跳轉(zhuǎn)到index界面,請自動完成
## 增加bootstrap樣式
為index組件的table增加bootstrap樣式,請自行完成
# 功能測試
完成了基礎(chǔ)樣式修正后,開發(fā)進(jìn)行功能測試。
## 綜合查詢
綜合查詢主要對姓名、學(xué)號、班級進(jìn)行查詢,要使測試正常進(jìn)行,則需要準(zhǔn)備不同姓名、不同學(xué)號、不同班級的學(xué)生。

測試如下:

## 全選、單選

## 分頁
成功的測試分頁,則需要不少于5頁的學(xué)生數(shù)據(jù),在開始測試前先新增多條測試學(xué)生:

當(dāng)前共7頁數(shù)據(jù),開始進(jìn)行測試

發(fā)現(xiàn)兩個問題:
* 頁碼應(yīng)該為1基,實際卻為0基
* 點(diǎn)擊其它頁碼時卻跳轉(zhuǎn)到了首頁
第一個問題修正相對簡單,請自行完成。
第二個問題是由于我們在頁碼中使用了`a`標(biāo)簽,然后`a`標(biāo)簽中定義了`href="#"`引起的。在一般的WEB應(yīng)用中,我們習(xí)慣性的使用`href="#"`來表示當(dāng)點(diǎn)擊該a標(biāo)簽時不進(jìn)行任何跳轉(zhuǎn)。但在`single page web application(SPA) 單頁面WEB應(yīng)用`中就不一樣了。在單頁面WEB應(yīng)用中,雖然瀏覽器的導(dǎo)航欄也會按照用戶的點(diǎn)擊進(jìn)行變更,但卻并沒有重新發(fā)起頁面加載請求。這種變更是通過調(diào)用瀏覽器相關(guān)的API來實現(xiàn)的,而非用戶點(diǎn)擊了需要跳轉(zhuǎn)的a標(biāo)簽。在學(xué)生管理中,雖然瀏覽器顯示的地址為`localhost/student`但angular很清晰的明了:當(dāng)前系統(tǒng)的實際請求地址為:`localhost`,遇到`href="#"`實際對應(yīng)的地址應(yīng)該為:`localhost/#`。而我們所期待的卻是`localhost/student/#`。猜出了原因,那么解決方案也就隨著而來了。
即然不能使用`href="#"`,那么我們將其刪除好了:
student/index/index.component.html
```
<ul class="pagination">
<li class="page-item" [ngClass]="{'disabled': params.page === 0}" (click)="onPage(0)">
<span class="page-link">首頁</span>
</li>
<li class="page-item" [ngClass]="{'disabled': params.page === 0}" (click)="onPage(params.page - 1)">
<span class="page-link">上一頁</span>
</li>
<li class="page-item" [ngClass]="{'active': params.page === page}" *ngFor="let page of pages" (click)="onPage(page)">
<a class="page-link" *ngIf="page !== params.page">{{page + 1}}</a>
<span class="page-link" *ngIf="page === params.page">{{page + 1}}<span class="sr-only">(current)</span></span>
</li>
<li class="page-item" [ngClass]="{'disabled': params.page === pageStudent.totalPages - 1}" (click)="onPage(params.page + 1)">
<a class="page-link">下一頁</a>
</li>
<li class="page-item" [ngClass]="{'disabled': params.page === pageStudent.totalPages - 1}" (click)="onPage(pageStudent.totalPages - 1)">
<a class="page-link">尾頁</a>
</li>
</ul>
```

刪除`href="#"`后的確修正了前面的跳轉(zhuǎn)首頁問題,但分頁表現(xiàn)卻并不完美。三個新問題又被暴露了出來:
* 首頁 上一頁 的樣式是我們期望的,但其它頁碼的樣式卻不行。
* 以前將鼠標(biāo)移到分頁按鈕上的時候,會有個 小手 出現(xiàn),現(xiàn)在沒有了。
* 當(dāng)前頁為第1頁時,點(diǎn)擊上一頁仍生效
* 當(dāng)前頁為最后1頁時,點(diǎn)擊下一頁仍生效
下面分別對上述問題進(jìn)行修正:
### 樣式問題
其它頁碼的樣式不同于首頁、上一頁是由于我們在首頁、上一頁中使用為`span`標(biāo)簽,也在頁碼中使用的`a`標(biāo)簽。使用`a`標(biāo)簽有個默認(rèn)的好處:當(dāng)`a`標(biāo)簽存在`href`屬性時,鼠標(biāo)移上去將自動變成 小手 的樣子。為了使格式統(tǒng)一,首先將`a`標(biāo)簽全部換成`span`標(biāo)簽。
student/index/index.component.html
```
<ul class="pagination">
<li class="page-item" [ngClass]="{'disabled': params.page === 0}" (click)="onPage(0)">
<span class="page-link">首頁</span>
</li>
<li class="page-item" [ngClass]="{'disabled': params.page === 0}" (click)="onPage(params.page - 1)">
<span class="page-link">上一頁</span>
</li>
<li class="page-item" [ngClass]="{'active': params.page === page}" *ngFor="let page of pages" (click)="onPage(page)">
<span class="page-link" *ngIf="page !== params.page">{{page + 1}}</span>
<span class="page-link" *ngIf="page === params.page">{{page + 1}}<span class="sr-only">(current)</span></span>
</li>
<li class="page-item" [ngClass]="{'disabled': params.page === pageStudent.totalPages - 1}" (click)="onPage(params.page + 1)">
<span class="page-link">下一頁</span>
</li>
<li class="page-item" [ngClass]="{'disabled': params.page === pageStudent.totalPages - 1}" (click)="onPage(pageStudent.totalPages - 1)">
<span class="page-link">尾頁</span>
</li>
</ul>
```

瀏覽器默認(rèn)為有`href`屬性的`a`標(biāo)簽的`hover`違類上添加了`cursor: pointer`屬性,以使得鼠標(biāo)移動到元素上變成 小手 的樣子。如果想用`span`標(biāo)簽添加此屬性,則為其`hover`違類添加對應(yīng)的樣式即可:
student/index/index.component.sass
```
ul.pagination > li > span:hover
cursor: pointer
```

pointer常用于跳轉(zhuǎn)的鏈接,分頁功能中我們更習(xí)慣于使用default( 默認(rèn)光標(biāo)(通常是一個箭頭))。
student/index/index.component.sass
```
ul.pagination > li > span:hover
cursor: default
```
### 上一頁、下一頁
此時我有了一個疑問,明明下一頁、上一頁顯示為灰色,卻為什么還能點(diǎn)擊呢?這時候就需要看看angular為我們生成的頁面源碼了:

在代碼中使用`[ngClass]="{'disabled': params.page === 0}"`來控制`disabled`時,其實angular是為該元素在`params.page === 0`時添加了一個`disabled`樣式,使其看起來是不能夠點(diǎn)擊的。要使一個元素真正的不能夠被點(diǎn)擊,前提是該元素具有天然的`disabled`屬性,比如`button`元素就具有這個屬性。實驗如下:
student/index/index.component.html
```
<li class="page-item" [ngClass]="{'disabled': params.page === 0}" (click)="onPage(params.page - 1)">
<span class="page-link">上一頁</span>
</li>
<button [disabled]="params.page === 0" (click)="onPage(params.page - 1)">上一頁</button>
```

實驗得出:天然有`disabled`屬性的button,可以設(shè)置其`disabled`屬性,能起到禁止點(diǎn)擊的作用。而天然并沒有`disabled`屬性的li,則只能是看起來`disabled`不能被點(diǎn)擊了,而實際上點(diǎn)用戶點(diǎn)擊該元素時,仍然能夠觸發(fā)其綁定的onPage方法。對于C層的onPage方法而言,如果按正常的邏輯,是不應(yīng)該接收到小于0或是大于等于總頁數(shù)的值的,但程序的魅力就是如此:它總能找點(diǎn)小樂子,讓本來應(yīng)該的事情變得那么的不應(yīng)該。既然V層解決不了(在V層中也可以考慮將li變?yōu)閎utton標(biāo)簽,但這將破壞了原bootstrap結(jié)構(gòu)而使得該分頁樣式變得難以維護(hù)),那就在C層的onPage方法上下功夫吧。
student/index/index.component.ts
```
/**
* 點(diǎn)擊分頁按鈕
* @param page 要請求的頁碼
*/
onPage(page: number) {
if (page < 0 || page >= this.pageStudent.totalPages) { ?
return;
}
this.params.page = page;
this.loadData();
}
```
對應(yīng)修正單元測試:
student/index/index.component.spec.ts
```
fit('onPage 功能測試', () => {
spyOn(component, 'loadData');
component.params.page = 4;
component.onPage(3);
expect(component.params.page).toEqual(3);
expect(component.loadData).toHaveBeenCalled();
/* 越界測試:期望不改變當(dāng)前頁碼值,loadData僅被前面的代碼調(diào)用了1次(本次未調(diào)用)*/
component.onPage(-1);
expect(component.params.page).toEqual(3);
expect(component.loadData).toHaveBeenCalledTimes(1);
/* 越界測試:期望不改變當(dāng)前頁碼值,loadData僅被前面的代碼調(diào)用了1次(本次未調(diào)用)*/
component.pageStudent.totalPages = 5;
component.onPage(5);
expect(component.params.page).toEqual(3);
expect(component.loadData).toHaveBeenCalledTimes(1);
});
```
此時當(dāng)頁碼為首、尾頁時,再次點(diǎn)擊上一頁下一頁時便不會發(fā)生越界的情況了,至此集成測試完畢。
# 總結(jié)
基于**人是必然會犯錯誤的**的理論,在開發(fā)時引入單元測試來對自己的代碼進(jìn)行功能性驗證,從而降低犯錯誤的概率。同樣基于該理論,當(dāng)各個模塊分離完成后進(jìn)行組合的測試來進(jìn)一步降低犯錯的概率。所以的準(zhǔn)備工作,都是為了最終減少生產(chǎn)環(huán)境中可能面臨的用戶各種無厘頭以及**非常正常**的操作。我們夢想并努力著:在應(yīng)用上線的那一天,我們可以關(guān)上手機(jī)心無旁騖地躺在床上休息。這應(yīng)該就是軟件工程的初心吧。
# 參考文檔
| 名稱 | 鏈接 | 預(yù)計學(xué)習(xí)時長(分) |
| --- | --- | --- |
| 源碼地址 | [https://github.com/mengyunzhi/spring-boot-and-angular-guild/releases/tag/step4.6.10](https://github.com/mengyunzhi/spring-boot-and-angular-guild/releases/tag/step4.6.10) | - |
- 序言
- 第一章:Hello World
- 第一節(jié):Angular準(zhǔn)備工作
- 1 Node.js
- 2 npm
- 3 WebStorm
- 第二節(jié):Hello Angular
- 第三節(jié):Spring Boot準(zhǔn)備工作
- 1 JDK
- 2 MAVEN
- 3 IDEA
- 第四節(jié):Hello Spring Boot
- 1 Spring Initializr
- 2 Hello Spring Boot!
- 3 maven國內(nèi)源配置
- 4 package與import
- 第五節(jié):Hello Spring Boot + Angular
- 1 依賴注入【前】
- 2 HttpClient獲取數(shù)據(jù)【前】
- 3 數(shù)據(jù)綁定【前】
- 4 回調(diào)函數(shù)【選學(xué)】
- 第二章 教師管理
- 第一節(jié) 數(shù)據(jù)庫初始化
- 第二節(jié) CRUD之R查數(shù)據(jù)
- 1 原型初始化【前】
- 2 連接數(shù)據(jù)庫【后】
- 3 使用JDBC讀取數(shù)據(jù)【后】
- 4 前后臺對接
- 5 ng-if【前】
- 6 日期管道【前】
- 第三節(jié) CRUD之C增數(shù)據(jù)
- 1 新建組件并映射路由【前】
- 2 模板驅(qū)動表單【前】
- 3 httpClient post請求【前】
- 4 保存數(shù)據(jù)【后】
- 5 組件間調(diào)用【前】
- 第四節(jié) CRUD之U改數(shù)據(jù)
- 1 路由參數(shù)【前】
- 2 請求映射【后】
- 3 前后臺對接【前】
- 4 更新數(shù)據(jù)【前】
- 5 更新某個教師【后】
- 6 路由器鏈接【前】
- 7 觀察者模式【前】
- 第五節(jié) CRUD之D刪數(shù)據(jù)
- 1 綁定到用戶輸入事件【前】
- 2 刪除某個教師【后】
- 第六節(jié) 代碼重構(gòu)
- 1 文件夾化【前】
- 2 優(yōu)化交互體驗【前】
- 3 相對與絕對地址【前】
- 第三章 班級管理
- 第一節(jié) JPA初始化數(shù)據(jù)表
- 第二節(jié) 班級列表
- 1 新建模塊【前】
- 2 初識單元測試【前】
- 3 初始化原型【前】
- 4 面向?qū)ο蟆厩啊?/a>
- 5 測試HTTP請求【前】
- 6 測試INPUT【前】
- 7 測試BUTTON【前】
- 8 @RequestParam【后】
- 9 Repository【后】
- 10 前后臺對接【前】
- 第三節(jié) 新增班級
- 1 初始化【前】
- 2 響應(yīng)式表單【前】
- 3 測試POST請求【前】
- 4 JPA插入數(shù)據(jù)【后】
- 5 單元測試【后】
- 6 惰性加載【前】
- 7 對接【前】
- 第四節(jié) 編輯班級
- 1 FormGroup【前】
- 2 x、[x]、{{x}}與(x)【前】
- 3 模擬路由服務(wù)【前】
- 4 測試間諜spy【前】
- 5 使用JPA更新數(shù)據(jù)【后】
- 6 分層開發(fā)【后】
- 7 前后臺對接
- 8 深入imports【前】
- 9 深入exports【前】
- 第五節(jié) 選擇教師組件
- 1 初始化【前】
- 2 動態(tài)數(shù)據(jù)綁定【前】
- 3 初識泛型
- 4 @Output()【前】
- 5 @Input()【前】
- 6 再識單元測試【前】
- 7 其它問題
- 第六節(jié) 刪除班級
- 1 TDD【前】
- 2 TDD【后】
- 3 前后臺對接
- 第四章 學(xué)生管理
- 第一節(jié) 引入Bootstrap【前】
- 第二節(jié) NAV導(dǎo)航組件【前】
- 1 初始化
- 2 Bootstrap格式化
- 3 RouterLinkActive
- 第三節(jié) footer組件【前】
- 第四節(jié) 歡迎界面【前】
- 第五節(jié) 新增學(xué)生
- 1 初始化【前】
- 2 選擇班級組件【前】
- 3 復(fù)用選擇組件【前】
- 4 完善功能【前】
- 5 MVC【前】
- 6 非NULL校驗【后】
- 7 唯一性校驗【后】
- 8 @PrePersist【后】
- 9 CM層開發(fā)【后】
- 10 集成測試
- 第六節(jié) 學(xué)生列表
- 1 分頁【后】
- 2 HashMap與LinkedHashMap
- 3 初識綜合查詢【后】
- 4 綜合查詢進(jìn)階【后】
- 5 小試綜合查詢【后】
- 6 初始化【前】
- 7 M層【前】
- 8 單元測試與分頁【前】
- 9 單選與多選【前】
- 10 集成測試
- 第七節(jié) 編輯學(xué)生
- 1 初始化【前】
- 2 嵌套組件測試【前】
- 3 功能開發(fā)【前】
- 4 JsonPath【后】
- 5 spyOn【后】
- 6 集成測試
- 7 @Input 異步傳值【前】
- 8 值傳遞與引入傳遞
- 9 @PreUpdate【后】
- 10 表單驗證【前】
- 第八節(jié) 刪除學(xué)生
- 1 CSS選擇器【前】
- 2 confirm【前】
- 3 功能開發(fā)與測試【后】
- 4 集成測試
- 5 定制提示框【前】
- 6 引入圖標(biāo)庫【前】
- 第九節(jié) 集成測試
- 第五章 登錄與注銷
- 第一節(jié):普通登錄
- 1 原型【前】
- 2 功能設(shè)計【前】
- 3 功能設(shè)計【后】
- 4 應(yīng)用登錄組件【前】
- 5 注銷【前】
- 6 保留登錄狀態(tài)【前】
- 第二節(jié):你是誰
- 1 過濾器【后】
- 2 令牌機(jī)制【后】
- 3 裝飾器模式【后】
- 4 攔截器【前】
- 5 RxJS操作符【前】
- 6 用戶登錄與注銷【后】
- 7 個人中心【前】
- 8 攔截器【后】
- 9 集成測試
- 10 單例模式
- 第六章 課程管理
- 第一節(jié) 新增課程
- 1 初始化【前】
- 2 嵌套組件測試【前】
- 3 async管道【前】
- 4 優(yōu)雅的測試【前】
- 5 功能開發(fā)【前】
- 6 實體監(jiān)聽器【后】
- 7 @ManyToMany【后】
- 8 集成測試【前】
- 9 異步驗證器【前】
- 10 詳解CORS【前】
- 第二節(jié) 課程列表
- 第三節(jié) 果斷
- 1 初始化【前】
- 2 分頁組件【前】
- 2 分頁組件【前】
- 3 綜合查詢【前】
- 4 綜合查詢【后】
- 4 綜合查詢【后】
- 第節(jié) 班級列表
- 第節(jié) 教師列表
- 第節(jié) 編輯課程
- TODO返回機(jī)制【前】
- 4 彈出框組件【前】
- 5 多路由出口【前】
- 第節(jié) 刪除課程
- 第七章 權(quán)限管理
- 第一節(jié) AOP
- 總結(jié)
- 開發(fā)規(guī)范
- 備用