() translation by (you can also view the original English article)



Ini adalah angsuran kedua dari seri pada pengujian di Angular menggunakan Jasmine. Di bagian pertama dari tutorial, kami menulis tes unit dasar untuk kelas Pastebin dan komponen Pastebin. Tes, yang awalnya gagal, menjadi hijau kemudian.
Ikhtisar
Berikut ini adalah ikhtisar dari apa kita akan kerjakan di dalam bagian kedua dari tutorial.



Dalam tutorial ini, kita akan:
- membuat komponen baru dan menulis unit test yang lain
- menulis tes untuk komponen UI
- menulis unit tes untuk layanan Pastebin
- pengujian komponen dengan input dan output
- pengujian komponen dengan route
Mari kita mulai!
Menambahkan Paste (lanjutan)
Kami sudah setengah jalan melalui proses penulisan tes unit untuk komponen AddPaste. Di sinilah kami tinggalkan di bagian satu dari seri.
1 |
it('should display the `create Paste` button', () => { |
2 |
//There should a create button in view
|
3 |
expect(element.innerText).toContain("create Paste"); |
4 |
});
|
5 |
|
6 |
it('should not display the modal unless the button is clicked', () => { |
7 |
//source-model is an id for the modal. It shouldn't show up unless create button is clicked
|
8 |
expect(element.innerHTML).not.toContain("source-modal"); |
9 |
})
|
10 |
|
11 |
it('should display the modal when `create Paste` is clicked', () => { |
12 |
|
13 |
let createPasteButton = fixture.debugElement.query(By.css("button")); |
14 |
//triggerEventHandler simulates a click event on the button object
|
15 |
createPasteButton.triggerEventHandler('click',null); |
16 |
fixture.detectChanges(); |
17 |
expect(element.innerHTML).toContain("source-modal"); |
18 |
|
19 |
})
|
20 |
|
21 |
})
|
Seperti yang disebutkan sebelumnya, kami tidak akan menulis tes UI yang ketat. Sebagai gantinya, kami akan menulis beberapa tes dasar untuk UI dan mencari cara untuk menguji logika komponen.
Tindakan klik dipicu menggunakan metode DebugElement.triggerEventHandler()
, yang merupakan bagian dari utilitas pengujian Angular.
Komponen AddPaste pada dasarnya tentang menciptakan paste baru; karenanya, template komponen harus memiliki tombol untuk membuat paste baru. Mengeklik tombol harus memunculkan 'modal window' dengan id 'source-modal' yang seharusnya tetap tersembunyi. Modal window akan dirancang menggunakan Bootstrap; Oleh karena itu, Anda mungkin menemukan banyak kelas CSS di dalam template.
Template untuk komponen add-paste akan terlihat seperti ini:
1 |
<!--- add-paste.component.html -->
|
2 |
|
3 |
<div class="add-paste"> |
4 |
<button> create Paste </button> |
5 |
<div id="source-modal" class="modal fade in"> |
6 |
<div class="modal-dialog" > |
7 |
<div class="modal-content"> |
8 |
<div class="modal-header"></div> |
9 |
<div class="modal-body"></div> |
10 |
<div class="modal-footer"></div> |
11 |
</div>
|
12 |
</div>
|
13 |
</div>
|
14 |
</div>
|
Tes kedua dan ketiga tidak memberikan informasi apa pun tentang rincian penerapan komponen. Berikut versi revisi add-paste.component.spec.ts.
1 |
it('should not display the modal unless the button is clicked', () => { |
2 |
|
3 |
//source-model is an id for the modal. It shouldn't show up unless create button is clicked
|
4 |
expect(element.innerHTML).not.toContain("source-modal"); |
5 |
|
6 |
//Component's showModal property should be false at the moment
|
7 |
expect(component.showModal).toBeFalsy("Show modal should be initially false"); |
8 |
})
|
9 |
|
10 |
it('should display the modal when `create Paste` is clicked',() => { |
11 |
|
12 |
let createPasteButton = fixture.debugElement.query(By.css("button")); |
13 |
//create a spy on the createPaste method
|
14 |
spyOn(component,"createPaste").and.callThrough(); |
15 |
|
16 |
//triggerEventHandler simulates a click event on the button object
|
17 |
createPasteButton.triggerEventHandler('click',null); |
18 |
|
19 |
//spy checks whether the method was called
|
20 |
expect(component.createPaste).toHaveBeenCalled(); |
21 |
fixture.detectChanges(); |
22 |
expect(component.showModal).toBeTruthy("showModal should now be true"); |
23 |
expect(element.innerHTML).toContain("source-modal"); |
24 |
})
|
Tes yang direvisi lebih eksplisit karena mereka mendeskripsikan logika komponen secara sempurna. Inilah komponen AddPaste dan templatenya.
1 |
<!--- add-paste.component.html --> |
2 |
|
3 |
<div class="add-paste"> |
4 |
<button (click)="createPaste()"> create Paste </button> |
5 |
<div *ngIf="showModal" id="source-modal" class="modal fade in"> |
6 |
<div class="modal-dialog" > |
7 |
<div class="modal-content"> |
8 |
<div class="modal-header"></div> |
9 |
<div class="modal-body"></div> |
10 |
<div class="modal-footer"></div> |
11 |
</div> |
12 |
</div> |
13 |
</div> |
14 |
</div> |
1 |
/* add-paste.component.ts */
|
2 |
|
3 |
export class AddPasteComponent implements OnInit { |
4 |
|
5 |
showModal: boolean = false; |
6 |
// Languages imported from Pastebin class
|
7 |
languages: string[] = Languages; |
8 |
|
9 |
constructor() { } |
10 |
ngOnInit() { } |
11 |
|
12 |
//createPaste() gets invoked from the template.
|
13 |
public createPaste():void { |
14 |
this.showModal = true; |
15 |
}
|
16 |
}
|
Tes harus tetap gagal karena mata-mata pada addPaste
gagal menemukan metode seperti itu di PastebinService. Mari kita kembali ke PastebinService dan menaruh beberapa daging di atasnya.
Menulis tes untuk Layanan
Sebelum kita melanjutkan dengan menulis tes lagi, mari kita tambahkan beberapa kode ke layanan Pastebin.
1 |
public addPaste(pastebin: Pastebin): Promise<any> { |
2 |
return this.http.post(this.pastebinUrl, JSON.stringify(pastebin), {headers: this.headers}) |
3 |
.toPromise() |
4 |
.then(response =>response.json().data) |
5 |
.catch(this.handleError); |
6 |
} |
addPaste()
adalah metode layanan untuk membuat paste baru. http.post
mengembalikan yang dapat diamati, yang diubah menjadi promise menggunakan metode toPromise()
. Respons diubah menjadi format JSON, dan segala pengecualian waktu proses tertangkap dan dilaporkan oleh handleError()
.
Bukankah kita harus menulis tes untuk layanan, Anda mungkin bertanya? Dan jawabanku pasti ya. Layanan, yang disuntikkan ke komponen Angular melalui Dependensi Injeksi (DI), juga rentan terhadap eror. Selain itu, tes untuk layanan Sudut relatif mudah. Metode di PastebinService seharusnya menyerupai empat operasi CRUD, dengan metode tambahan untuk menangani eror. Metodenya adalah sebagai berikut:
- handleError()
- getPastebin()
- addPaste()
- updatePaste()
- deletePaste()
Kami telah menerapkan tiga metode pertama dalam daftar. Mari coba tes tertulis untuk mereka. Inilah blok uraian.
1 |
import { TestBed, inject } from '@angular/core/testing'; |
2 |
import { Pastebin, Languages } from './pastebin'; |
3 |
import { PastebinService } from './pastebin.service'; |
4 |
import { AppModule } from './app.module'; |
5 |
import { HttpModule } from '@angular/http'; |
6 |
|
7 |
let testService: PastebinService; |
8 |
let mockPaste: Pastebin; |
9 |
let responsePropertyNames, expectedPropertyNames; |
10 |
|
11 |
describe('PastebinService', () => { |
12 |
beforeEach(() => { |
13 |
TestBed.configureTestingModule({ |
14 |
providers: [PastebinService], |
15 |
imports: [HttpModule] |
16 |
});
|
17 |
|
18 |
//Get the injected service into our tests
|
19 |
testService= TestBed.get(PastebinService); |
20 |
mockPaste = { id:999, title: "Hello world", language: Languages[2], paste: "console.log('Hello world');"}; |
21 |
|
22 |
});
|
23 |
});
|
Kami telah menggunakan TestBed.get(PastebinService)
untuk menyuntikkan layanan nyata ke dalam pengujian kami.
1 |
it('#getPastebin should return an array with Pastebin objects',async() => { |
2 |
|
3 |
testService.getPastebin().then(value => { |
4 |
//Checking the property names of the returned object and the mockPaste object
|
5 |
responsePropertyNames = Object.getOwnPropertyNames(value[0]); |
6 |
expectedPropertyNames = Object.getOwnPropertyNames(mockPaste); |
7 |
|
8 |
expect(responsePropertyNames).toEqual(expectedPropertyNames); |
9 |
|
10 |
});
|
11 |
});
|
getPastebin
mengembalikan array objek Pastebin. Pengecekan jenis kompilasi-waktu TypeScript tidak dapat digunakan untuk memverifikasi bahwa nilai yang dikembalikan memang merupakan array dari objek Pastebin. Oleh karena itu, kami telah menggunakan Object.getOwnPropertNames()
untuk memastikan bahwa kedua objek memiliki nama properti yang sama.
Tes kedua sebagai berikut:
1 |
it('#addPaste should return async paste', async() => { |
2 |
testService.addPaste(mockPaste).then(value => { |
3 |
expect(value).toEqual(mockPaste); |
4 |
})
|
5 |
})
|
Kedua tes harus lulus. Berikut adalah tes yang tersisa.
1 |
it('#updatePaste should update', async() => { |
2 |
//Updating the title of Paste with id 1 |
3 |
mockPaste.id = 1; |
4 |
mockPaste.title = "New title" |
5 |
testService.updatePaste(mockPaste).then(value => { |
6 |
expect(value).toEqual(mockPaste); |
7 |
}) |
8 |
}) |
9 |
|
10 |
it('#deletePaste should return null', async() => { |
11 |
testService.deletePaste(mockPaste).then(value => { |
12 |
expect(value).toEqual(null); |
13 |
}) |
14 |
}) |
Merevisi pastebin.service.ts dengan kode untuk metode updatePaste()
dan deletePaste()
.
1 |
//update a paste
|
2 |
public updatePaste(pastebin: Pastebin):Promise<any> { |
3 |
const url = `${this.pastebinUrl}/${pastebin.id}`; |
4 |
return this.http.put(url, JSON.stringify(pastebin), {headers: this.headers}) |
5 |
.toPromise() |
6 |
.then(() => pastebin) |
7 |
.catch(this.handleError); |
8 |
}
|
9 |
//delete a paste
|
10 |
public deletePaste(pastebin: Pastebin): Promise<void> { |
11 |
const url = `${this.pastebinUrl}/${pastebin.id}`; |
12 |
return this.http.delete(url, {headers: this.headers}) |
13 |
.toPromise() |
14 |
.then(() => null ) |
15 |
.catch(this.handleError); |
16 |
}
|
Kembali ke komponen
Persyaratan yang tersisa untuk komponen AddPaste adalah sebagai berikut:
- Menekan tombol Save harus menggunakan metode
addPaste()
dari Pastebin service. - Jika operasi
addPaste
berhasil, komponen harus memancarkan acara untuk memberi tahu komponen induk. - Mengeklik tombol Close harus menghapus id 'source-modal' dari DOM dan memperbarui properti
showModal
menjadi false.
Karena kasus-kasus pengujian di atas berkaitan dengan modal window, mungkin ada baiknya menggunakan blok deskripsi bertingkat.
1 |
describe('AddPasteComponent', () => { |
2 |
.
|
3 |
.
|
4 |
.
|
5 |
describe("AddPaste Modal", () => { |
6 |
|
7 |
let inputTitle: HTMLInputElement; |
8 |
let selectLanguage: HTMLSelectElement; |
9 |
let textAreaPaste: HTMLTextAreaElement; |
10 |
let mockPaste: Pastebin; |
11 |
let spyOnAdd: jasmine.Spy; |
12 |
let pastebinService: PastebinService; |
13 |
|
14 |
beforeEach(() => { |
15 |
|
16 |
component.showModal = true; |
17 |
fixture.detectChanges(); |
18 |
|
19 |
mockPaste = { id:1, title: "Hello world", language: Languages[2], paste: "console.log('Hello world');"}; |
20 |
//Create a jasmine spy to spy on the addPaste method
|
21 |
spyOnAdd = spyOn(pastebinService,"addPaste").and.returnValue(Promise.resolve(mockPaste)); |
22 |
|
23 |
});
|
24 |
|
25 |
});
|
26 |
});
|
Mendeklarasikan semua variabel di root blok menggambarkan adalah praktik yang baik karena dua alasan. Variabel akan dapat diakses di dalam blok deskripsi di mana mereka dinyatakan, dan itu membuat tes lebih mudah dibaca.
1 |
it("should accept input values", () => { |
2 |
//Query the input selectors
|
3 |
inputTitle = element.querySelector("input"); |
4 |
selectLanguage = element.querySelector("select"); |
5 |
textAreaPaste = element.querySelector("textarea"); |
6 |
|
7 |
//Set their value
|
8 |
inputTitle.value = mockPaste.title; |
9 |
selectLanguage.value = mockPaste.language; |
10 |
textAreaPaste.value = mockPaste.paste; |
11 |
|
12 |
//Dispatch an event
|
13 |
inputTitle.dispatchEvent(new Event("input")); |
14 |
selectLanguage.dispatchEvent(new Event("change")); |
15 |
textAreaPaste.dispatchEvent(new Event("input")); |
16 |
|
17 |
expect(mockPaste.title).toEqual(component.newPaste.title); |
18 |
expect(mockPaste.language).toEqual(component.newPaste.language); |
19 |
expect(mockPaste.paste).toEqual(component.newPaste.paste); |
20 |
});
|
Tes di atas menggunakan metode querySelector()
untuk menetapkan inputTitle
, SelectLanguage
, dan textAreaPaste
elemen HTML mereka masing-masing (<input>
,<select>
dan <textArea>
). Selanjutnya, nilai-nilai elemen-elemen ini digantikan oleh nilai properti mockPaste
. Ini sama dengan pengguna mengisi formulir melalui browser.
element.dispatchEvent(new Event("input"))
memicu event input baru untuk membiarkan template mengetahui bahwa nilai dari field input telah berubah. Tes mengharapkan bahwa nilai input harus disebarkan ke properti newPaste
komponen.
Menyatakan properti newPaste
sebagai berikut:
1 |
newPaste: Pastebin = new Pastebin(); |
Dan memperbarui template dengan kode berikut:
1 |
<!--- add-paste.component.html -->
|
2 |
<div class="add-paste"> |
3 |
<button type="button" (click)="createPaste()"> create Paste </button> |
4 |
<div *ngIf="showModal" id="source-modal" class="modal fade in"> |
5 |
<div class="modal-dialog" > |
6 |
<div class="modal-content"> |
7 |
<div class="modal-header"> |
8 |
<h4 class="modal-title"> |
9 |
<input placeholder="Enter the Title" name="title" [(ngModel)] = "newPaste.title" /> |
10 |
</h4>
|
11 |
</div>
|
12 |
<div class="modal-body"> |
13 |
<h5>
|
14 |
<select name="category" [(ngModel)]="newPaste.language" > |
15 |
<option *ngFor ="let language of languages" value={{language}}> {{language}} </option> |
16 |
</select>
|
17 |
</h5>
|
18 |
<textarea name="paste" placeholder="Enter the code here" [(ngModel)] = "newPaste.paste"> </textarea> |
19 |
</div>
|
20 |
<div class="modal-footer"> |
21 |
<button type="button" (click)="onClose()">Close</button> |
22 |
<button type="button" (click) = "onSave()">Save</button> |
23 |
</div>
|
24 |
</div>
|
25 |
</div>
|
26 |
</div>
|
27 |
</div>
|
Ekstra divs dan kelas untuk modal window Bootstrap. [(ngModel)]
adalah perintah Angular yang mengimplementasikan pengikatan data dua arah. (click) = "onClose ()"
dan (click) = "onSave ()"
adalah contoh teknik event binding yang digunakan untuk mengikat event klik ke metode dalam komponen. Anda dapat membaca lebih lanjut tentang teknik pengikatan data yang berbeda di Panduan Syntax Template resmi Angular.
Jika Anda mengalami Kesalahan Parse Template, itu karena Anda belum mengimpor FormsModule
ke dalam AppComponent.
Mari tambahkan lebih banyak spesifikasi untuk pengujian kami.
1 |
it("should submit the values", async() => { |
2 |
component.newPaste = mockPaste; |
3 |
component.onSave(); |
4 |
fixture.detectChanges(); |
5 |
fixture.whenStable().then( () => { |
6 |
fixture.detectChanges(); |
7 |
expect(spyOnAdd.calls.any()).toBeTruthy(); |
8 |
});
|
9 |
|
10 |
});
|
11 |
|
12 |
it("should have a onClose method", () => { |
13 |
component.onClose(); |
14 |
fixture.detectChanges(); |
15 |
expect(component.showModal).toBeFalsy(); |
16 |
})
|
component.onSave()
analog dengan pemanggilan triggerEventHandler()
pada elemen tombol Save. Karena kami telah menambahkan UI untuk tombol tersebut, memanggil component.save()
terdengar lebih berarti. Pernyataan yang diharapkan memeriksa apakah ada panggilan yang dilakukan ke mata-mata itu. Berikut ini versi terakhir dari komponen AddPaste.
1 |
import { Component, OnInit, Input, Output, EventEmitter } from '@angular/core'; |
2 |
import { Pastebin, Languages } from '../pastebin'; |
3 |
import { PastebinService } from '../pastebin.service'; |
4 |
|
5 |
@Component({ |
6 |
selector: 'app-add-paste', |
7 |
templateUrl: './add-paste.component.html', |
8 |
styleUrls: ['./add-paste.component.css'] |
9 |
})
|
10 |
export class AddPasteComponent implements OnInit { |
11 |
|
12 |
@Output() addPasteSuccess: EventEmitter<Pastebin> = new EventEmitter<Pastebin>(); |
13 |
showModal: boolean = false; |
14 |
newPaste: Pastebin = new Pastebin(); |
15 |
languages: string[] = Languages; |
16 |
|
17 |
constructor(private pasteServ: PastebinService) { } |
18 |
|
19 |
ngOnInit() { } |
20 |
//createPaste() gets invoked from the template. This shows the Modal
|
21 |
public createPaste():void { |
22 |
this.showModal = true; |
23 |
|
24 |
}
|
25 |
//onSave() pushes the newPaste property into the server
|
26 |
public onSave():void { |
27 |
this.pasteServ.addPaste(this.newPaste).then( () => { |
28 |
console.log(this.newPaste); |
29 |
this.addPasteSuccess.emit(this.newPaste); |
30 |
this.onClose(); |
31 |
});
|
32 |
}
|
33 |
//Used to close the Modal
|
34 |
public onClose():void { |
35 |
this.showModal=false; |
36 |
}
|
37 |
}
|
Jika operasi onSave
berhasil, komponen harus memancarkan peristiwa yang menandakan komponen induk (komponen Pastebin) untuk memperbarui view. addPasteSuccess
, yang merupakan properti acara yang dihiasi dengan penghias @Output
, melayani tujuan ini.
Menguji komponen yang memancarkan event keluaran itu mudah.
1 |
describe("AddPaste Modal", () => { |
2 |
|
3 |
beforeEach(() => { |
4 |
.
|
5 |
.
|
6 |
//Subscribe to the event emitter first
|
7 |
//If the emitter emits something, responsePaste will be set
|
8 |
component.addPasteSuccess.subscribe((response: Pastebin) => {responsePaste = response},) |
9 |
|
10 |
});
|
11 |
|
12 |
it("should accept input values", async(() => { |
13 |
.
|
14 |
.
|
15 |
component.onSave(); |
16 |
fixture.detectChanges(); |
17 |
fixture.whenStable().then( () => { |
18 |
fixture.detectChanges(); |
19 |
expect(spyOnAdd.calls.any()).toBeTruthy(); |
20 |
expect(responsePaste.title).toEqual(mockPaste.title); |
21 |
});
|
22 |
}));
|
23 |
|
24 |
});
|
25 |
Tes ini berlangganan properti addPasteSuccess
seperti yang dilakukan oleh komponen induk. Harapan pada akhirnya akan membenarkan hal ini. Pekerjaan kami pada komponen AddPaste dilakukan.
Hapus tanda komentar baris ini di pastebin.component.html:
1 |
<app-add-paste (addPasteSuccess)= 'onAddPaste($event)'> </app-add-paste> |
Dan memperbarui pastebin.component.ts dengan kode di bawah ini.
1 |
//This will be invoked when the child emits addPasteSuccess event
|
2 |
public onAddPaste(newPaste: Pastebin) { |
3 |
this.pastebin.push(newPaste); |
4 |
}
|
Jika Anda mengalami kesalahan, itu karena Anda belum menyatakan komponen AddPaste
dalam file spesifikasi komponen Pastebin. Bukankah lebih bagus lagi jika kita dapat menyatakan semua yang diperlukan pengujian kami di satu tempat dan mengimpornya ke dalam pengujian kami? Untuk mewujudkan hal ini, kita dapat mengimpor AppModule
ke dalam pengujian kami atau membuat Modul baru untuk pengujian kami. Buat file baru dan beri nama app-testing-module.ts:
1 |
import { BrowserModule } from '@angular/platform-browser'; |
2 |
import { NgModule } from '@angular/core'; |
3 |
|
4 |
//Components
|
5 |
import { AppComponent } from './app.component'; |
6 |
import { PastebinComponent } from './pastebin/pastebin.component'; |
7 |
import { AddPasteComponent } from './add-paste/add-paste.component'; |
8 |
//Service for Pastebin
|
9 |
|
10 |
import { PastebinService } from "./pastebin.service"; |
11 |
|
12 |
//Modules used in this tutorial
|
13 |
import { HttpModule } from '@angular/http'; |
14 |
import { FormsModule } from '@angular/forms'; |
15 |
|
16 |
//In memory Web api to simulate an http server
|
17 |
import { InMemoryWebApiModule } from 'angular-in-memory-web-api'; |
18 |
import { InMemoryDataService } from './in-memory-data.service'; |
19 |
|
20 |
@NgModule({ |
21 |
declarations: [ |
22 |
AppComponent, |
23 |
PastebinComponent, |
24 |
AddPasteComponent, |
25 |
],
|
26 |
|
27 |
imports: [ |
28 |
BrowserModule, |
29 |
HttpModule, |
30 |
FormsModule, |
31 |
InMemoryWebApiModule.forRoot(InMemoryDataService), |
32 |
],
|
33 |
providers: [PastebinService], |
34 |
bootstrap: [AppComponent] |
35 |
})
|
36 |
export class AppTestingModule { } |
Sekarang Anda dapat mengganti:
1 |
beforeEach(async(() => { |
2 |
TestBed.configureTestingModule({ |
3 |
declarations: [ AddPasteComponent ], |
4 |
imports: [ HttpModule, FormsModule ], |
5 |
providers: [ PastebinService ], |
6 |
})
|
7 |
.compileComponents(); |
8 |
}));
|
dengan:
1 |
beforeEach(async(() => { |
2 |
TestBed.configureTestingModule({ |
3 |
imports: [AppTestingModule] |
4 |
})
|
5 |
.compileComponents(); |
6 |
}));
|
Metadata yang menentukan providers
dan declarations
telah menghilang dan sebaliknya, AppTestingModule
mendapat diimpor. Itu rapi! TestBed.configureTestingModule()
terlihat lebih ramping dari sebelumnya.
View, Edit, and Delete Paste
Komponen ViewPaste menangani logika untuk melihat, mengedit, dan menghapus paste. Desain komponen ini mirip dengan apa yang kami lakukan dengan komponen AddPaste.






Tujuan komponen ViewPaste tercantum di bawah ini:
- Komponen yang template harus memiliki sebuah tombol yang bernama View Paste.
- Mengklik tombol View Paste harus menampilkan jendela modal dengan id 'source-modal'.
- Data paste harus menyebar dari komponen parent ke komponen child dan harus ditampilkan di dalam modal window.
- Menekan tombol edit harus mengatur
component.editEnabled
ke true (editEnabled
digunakan untuk beralih antara mode edit dan mode view) - Mengklik tombol Save harus memanggil metode
updatePaste()
Layanan Pastebin. - Klik pada tombol Delete harus memanggil metode
deletePaste()
Layanan Pastebin. - Pembaruan dan penghapusan operasi yang berhasil harus memancarkan event untuk memberi tahu komponen parent dari setiap perubahan pada komponen child.
Mari kita mulai! Dua spesifikasi pertama identik dengan pengujian yang kami tulis untuk komponen AddPaste sebelumnya.
1 |
it('should show a button with text View Paste', ()=> { |
2 |
expect(element.textContent).toContain("View Paste"); |
3 |
});
|
4 |
|
5 |
it('should not display the modal until the button is clicked', () => { |
6 |
expect(element.textContent).not.toContain("source-modal"); |
7 |
});
|
Mirip dengan apa yang kami lakukan sebelumnya, kami akan membuat blok deskripsi baru dan menempatkan sisa spesifikasi di dalamnya. Nesting mendeskripsikan blok dengan cara ini membuat file spek lebih mudah dibaca dan keberadaan fungsi mendeskripsi lebih bermakna.
Blok deskripsi bersarang akan memiliki fungsi beforeEach()
di mana kami akan menginisialisasi dua mata-mata, satu untuk metode updatePaste()
dan yang lainnya untuk metode deletePaste()
. Jangan lupa untuk membuat objek mockPaste
karena tes kami bergantung padanya.
1 |
beforeEach(()=> { |
2 |
//Set showPasteModal to true to ensure that the modal is visible in further tests |
3 |
component.showPasteModal = true; |
4 |
mockPaste = {id:1, title:"New paste", language:Languages[2], paste: "console.log()"}; |
5 |
|
6 |
//Inject PastebinService |
7 |
pastebinService = fixture.debugElement.injector.get(PastebinService); |
8 |
|
9 |
//Create spies for deletePaste and updatePaste methods |
10 |
spyOnDelete = spyOn(pastebinService,'deletePaste').and.returnValue(Promise.resolve(true)); |
11 |
spyOnUpdate = spyOn(pastebinService, 'updatePaste').and.returnValue(Promise.resolve(mockPaste)); |
12 |
|
13 |
//component.paste is an input property |
14 |
component.paste = mockPaste; |
15 |
fixture.detectChanges(); |
16 |
|
17 |
}) |
Berikut adalah tes.
1 |
it('should display the modal when the view Paste button is clicked',() => { |
2 |
|
3 |
fixture.detectChanges(); |
4 |
expect(component.showPasteModal).toBeTruthy("Show should be true"); |
5 |
expect(element.innerHTML).toContain("source-modal"); |
6 |
})
|
7 |
|
8 |
it('should display title, language and paste', () => { |
9 |
expect(element.textContent).toContain(mockPaste.title, "it should contain title"); |
10 |
expect(element.textContent).toContain(mockPaste.language, "it should contain the language"); |
11 |
expect(element.textContent).toContain(mockPaste.paste, "it should contain the paste"); |
12 |
});
|
Pengujian mengasumsikan bahwa komponen memiliki properti paste
yang menerima input dari komponen parent. Sebelumnya, kami melihat contoh bagaimana event yang dipancarkan dari komponen child dapat diuji tanpa harus memasukkan logika komponen host ke dalam pengujian kami. Demikian pula, untuk menguji properti input, lebih mudah melakukannya dengan menyetel properti ke objek tiruan dan mengharapkan nilai objek tiruan muncul di kode HTML.
Modal window akan memiliki banyak tombol, dan itu bukan ide yang buruk untuk menulis spec untuk menjamin bahwa tombol tersedia di template.
1 |
it('should have all the buttons',() => { |
2 |
expect(element.innerHTML).toContain('Edit Paste'); |
3 |
expect(element.innerHTML).toContain('Delete'); |
4 |
expect(element.innerHTML).toContain('Close'); |
5 |
});
|
Mari perbaiki tes yang gagal sebelum melakukan tes yang lebih rumit.
1 |
<!--- view-paste.component.html -->
|
2 |
<div class="view-paste"> |
3 |
<button class="text-primary button-text" (click)="showPaste()"> View Paste </button> |
4 |
<div *ngIf="showPasteModal" id="source-modal" class="modal fade in"> |
5 |
<div class="modal-dialog"> |
6 |
<div class="modal-content"> |
7 |
<div class="modal-header"> |
8 |
<button type="button" class="close" (click)='onClose()' aria-hidden="true">×</button> |
9 |
<h4 class="modal-title">{{paste.title}} </h4> |
10 |
</div>
|
11 |
<div class="modal-body"> |
12 |
<h5> {{paste.language}} </h5> |
13 |
<pre><code>{{paste.paste}}</code></pre> |
14 |
</div>
|
15 |
<div class="modal-footer"> |
16 |
<button type="button" class="btn btn-default" (click)="onClose()" data-dismiss="modal">Close</button> |
17 |
<button type="button" *ngIf="!editEnabled" (click) = "onEdit()" class="btn btn-primary">Edit Paste</button> |
18 |
<button type = "button" (click) = "onDelete()" class="btn btn-danger"> Delete Paste </button> |
19 |
</div>
|
20 |
</div>
|
21 |
</div>
|
22 |
</div>
|
23 |
</div>
|
24 |
1 |
/* view-paste.component.ts */
|
2 |
|
3 |
export class ViewPasteComponent implements OnInit { |
4 |
|
5 |
@Input() paste: Pastebin; |
6 |
@Output() updatePasteSuccess: EventEmitter<Pastebin> = new EventEmitter<Pastebin>(); |
7 |
@Output() deletePasteSuccess: EventEmitter<Pastebin> = new EventEmitter<Pastebin>(); |
8 |
|
9 |
showPasteModal:boolean ; |
10 |
readonly languages = Languages; |
11 |
|
12 |
constructor(private pasteServ: PastebinService) { } |
13 |
|
14 |
ngOnInit() { |
15 |
this.showPasteModal = false; |
16 |
}
|
17 |
//To make the modal window visible
|
18 |
public showPaste() { |
19 |
this.showPasteModal = true; |
20 |
}
|
21 |
//Invoked when edit button is clicked
|
22 |
public onEdit() { } |
23 |
|
24 |
//invoked when save button is clicked
|
25 |
public onSave() { } |
26 |
|
27 |
//invoked when close button is clicked
|
28 |
public onClose() { |
29 |
this.showPasteModal = false; |
30 |
}
|
31 |
|
32 |
//invoked when Delete button is clicked
|
33 |
public onDelete() { } |
34 |
|
35 |
}
|
Mampu melihat paste saja tidak cukup. Komponen ini juga bertanggung jawab untuk mengedit, memperbarui, dan menghapus paste. Komponen harus memiliki properti editEnabled
, yang akan disetel ke true ketika pengguna mengklik tombol Edit paste.
1 |
it('and clicking it should make the paste editable', () => { |
2 |
|
3 |
component.onEdit(); |
4 |
fixture.detectChanges(); |
5 |
expect(component.editEnabled).toBeTruthy(); |
6 |
//Now it should have a save button
|
7 |
expect(element.innerHTML).toContain('Save'); |
8 |
|
9 |
});
|
Tambahkan editEnabled=true;
metode onEdit()
untuk menghapus pernyataan pertama ekspektasi.
Template di bawah ini menggunakan direktif ngIf
untuk beralih antara mode tampilan dan mode edit. <ng-container>
adalah wadah logika yang digunakan untuk mengelompokkan beberapa elemen atau node.
1 |
<div *ngIf="showPasteModal" id="source-modal" class="modal fade in" > |
2 |
|
3 |
<div class="modal-dialog"> |
4 |
<div class="modal-content"> |
5 |
<!---View mode -->
|
6 |
<ng-container *ngIf="!editEnabled"> |
7 |
|
8 |
<div class="modal-header"> |
9 |
<button type="button" class="close" (click)='onClose()' data-dismiss="modal" aria-hidden="true">×</button> |
10 |
<h4 class="modal-title"> {{paste.title}} </h4> |
11 |
</div>
|
12 |
<div class="modal-body"> |
13 |
<h5> {{paste.language}} </h5> |
14 |
<pre><code>{{paste.paste}}</code> |
15 |
</pre>
|
16 |
|
17 |
</div>
|
18 |
<div class="modal-footer"> |
19 |
<button type="button" class="btn btn-default" (click)="onClose()" data-dismiss="modal">Close</button> |
20 |
<button type="button" (click) = "onEdit()" class="btn btn-primary">Edit Paste</button> |
21 |
<button type = "button" (click) = "onDelete()" class="btn btn-danger"> Delete Paste </button> |
22 |
|
23 |
</div>
|
24 |
</ng-container>
|
25 |
<!---Edit enabled mode -->
|
26 |
<ng-container *ngIf="editEnabled"> |
27 |
<div class="modal-header"> |
28 |
<button type="button" class="close" (click)='onClose()' data-dismiss="modal" aria-hidden="true">×</button> |
29 |
<h4 class="modal-title"> <input *ngIf="editEnabled" name="title" [(ngModel)] = "paste.title"> </h4> |
30 |
</div>
|
31 |
<div class="modal-body"> |
32 |
<h5>
|
33 |
<select name="category" [(ngModel)]="paste.language"> |
34 |
<option *ngFor ="let language of languages" value={{language}}> {{language}} </option> |
35 |
</select>
|
36 |
</h5>
|
37 |
|
38 |
<textarea name="paste" [(ngModel)] = "paste.paste">{{paste.paste}} </textarea> |
39 |
</div>
|
40 |
<div class="modal-footer"> |
41 |
<button type="button" class="btn btn-default" (click)="onClose()" data-dismiss="modal">Close</button> |
42 |
<button type = "button" *ngIf="editEnabled" (click) = "onSave()" class="btn btn-primary"> Save Paste </button> |
43 |
<button type = "button" (click) = "onDelete()" class="btn btn-danger"> Delete Paste </button> |
44 |
</div>
|
45 |
</ng-container>
|
46 |
</div>
|
47 |
</div>
|
48 |
</div>
|
Komponen harus memiliki dua Output()
event emitters, satu untuk properti updatePasteSuccess
dan yang lainnya untuk deletePasteSuccess
. Tes di bawah ini memverifikasi hal-hal berikut:
- Template komponen yang menerima input.
- Masukan template terikat untuk properti
paste
komponen. - Jika operasi update berhasil,
updatePasteSuccess
memancarkan sebuah event dengan paste diperbarui.
1 |
it('should take input values', fakeAsync(() => { |
2 |
component.editEnabled= true; |
3 |
component.updatePasteSuccess.subscribe((res:any) => {response = res},) |
4 |
fixture.detectChanges(); |
5 |
|
6 |
inputTitle= element.querySelector("input"); |
7 |
inputTitle.value = mockPaste.title; |
8 |
inputTitle.dispatchEvent(new Event("input")); |
9 |
|
10 |
expect(mockPaste.title).toEqual(component.paste.title); |
11 |
|
12 |
component.onSave(); |
13 |
//first round of detectChanges()
|
14 |
fixture.detectChanges(); |
15 |
|
16 |
//the tick() operation. Don't forget to import tick
|
17 |
tick(); |
18 |
|
19 |
//Second round of detectChanges()
|
20 |
fixture.detectChanges(); |
21 |
expect(response.title).toEqual(mockPaste.title); |
22 |
expect(spyOnUpdate.calls.any()).toBe(true, 'updatePaste() method should be called'); |
23 |
|
24 |
}))
|
Perbedaan jelas antara tes ini dan yang sebelumnya adalah penggunaan fungsi fakeAsync
. fakeAsync
dapat dibandingkan dengan async karena kedua fungsi tersebut digunakan untuk menjalankan pengujian dalam zona pengujian asynchronous. Namun, fakeAsync
membuat tampilan tes Anda terlihat lebih sinkron.
Metode tick()
menggantikan fixture.whenStable().then()
, dan kode lebih mudah dibaca dari perspektif pengembang. Jangan lupa untuk mengimpor fakeAsync
dan tanda dari @angular/core/testing
.
Akhirnya, berikut adalah spesifikasi untuk menghapus paste.
1 |
it('should delete the paste', fakeAsync(()=> { |
2 |
|
3 |
component.deletePasteSuccess.subscribe((res:any) => {response = res},) |
4 |
component.onDelete(); |
5 |
fixture.detectChanges(); |
6 |
tick(); |
7 |
fixture.detectChanges(); |
8 |
expect(spyOnDelete.calls.any()).toBe(true, "Pastebin deletePaste() method should be called"); |
9 |
expect(response).toBeTruthy(); |
10 |
}))
|
11 |
Kami hampir selesai dengan komponen. Berikut ini adalah rancangan akhir dari komponen ViewPaste
.
1 |
/*view-paste.component.ts*/
|
2 |
export class ViewPasteComponent implements OnInit { |
3 |
|
4 |
@Input() paste: Pastebin; |
5 |
@Output() updatePasteSuccess: EventEmitter<Pastebin> = new EventEmitter<Pastebin>(); |
6 |
@Output() deletePasteSuccess: EventEmitter<Pastebin> = new EventEmitter<Pastebin>(); |
7 |
|
8 |
showPasteModal:boolean ; |
9 |
editEnabled: boolean; |
10 |
readonly languages = Languages; |
11 |
|
12 |
constructor(private pasteServ: PastebinService) { } |
13 |
|
14 |
ngOnInit() { |
15 |
this.showPasteModal = false; |
16 |
this.editEnabled = false; |
17 |
}
|
18 |
//To make the modal window visible
|
19 |
public showPaste() { |
20 |
this.showPasteModal = true; |
21 |
}
|
22 |
//Invoked when the edit button is clicked
|
23 |
public onEdit() { |
24 |
this.editEnabled=true; |
25 |
}
|
26 |
//Invoked when the save button is clicked
|
27 |
public onSave() { |
28 |
this.pasteServ.updatePaste(this.paste).then( () => { |
29 |
this.editEnabled= false; |
30 |
this.updatePasteSuccess.emit(this.paste); |
31 |
})
|
32 |
}
|
33 |
//Invoked when the close button is clicked
|
34 |
public onClose() { |
35 |
this.showPasteModal = false; |
36 |
}
|
37 |
|
38 |
//Invoked when the delete button is clicked
|
39 |
public onDelete() { |
40 |
this.pasteServ.deletePaste(this.paste).then( () => { |
41 |
this.deletePasteSuccess.emit(this.paste); |
42 |
this.onClose(); |
43 |
})
|
44 |
}
|
45 |
|
46 |
}
|
Komponen induk (pastebin.component.ts) perlu diperbarui dengan metode untuk menangani peristiwa yang dipancarkan oleh komponen child.
1 |
/*pastebin.component.ts */
|
2 |
public onUpdatePaste(newPaste: Pastebin) { |
3 |
this.pastebin.map((paste)=> { |
4 |
if(paste.id==newPaste.id) { |
5 |
paste = newPaste; |
6 |
}
|
7 |
})
|
8 |
}
|
9 |
|
10 |
public onDeletePaste(p: Pastebin) { |
11 |
this.pastebin= this.pastebin.filter(paste => paste !== p); |
12 |
|
13 |
}
|
Berikut adalah pastebin.component.html Diperbarui:
1 |
<tbody>
|
2 |
<tr *ngFor="let paste of pastebin"> |
3 |
<td> {{paste.id}} </td> |
4 |
<td> {{paste.title}} </td> |
5 |
<td> {{paste.language}} </td> |
6 |
|
7 |
<td> <app-view-paste [paste] = paste (updatePasteSuccess)= 'onUpdatePaste($event)' (deletePasteSuccess)= 'onDeletePaste($event)'> </app-view-paste></td> |
8 |
</tr>
|
9 |
</tbody>
|
10 |
<app-add-paste (addPasteSuccess)= 'onAddPaste($event)'> </app-add-paste> |
Mengatur Route
Untuk membuat aplikasi yang dirutekan, kami memerlukan beberapa komponen stok lagi sehingga kami dapat membuat rute sederhana yang mengarah ke komponen ini. Saya telah membuat komponen About dan komponen Contact sehingga kami dapat menempatkannya di dalam bilah navigasi. AppComponent
akan menahan logika untuk rute. Kami akan menulis tes untuk rute setelah kami selesai dengan mereka.
Pertama, impor RouterModule
dan Routes
ke AppModule
(dan AppTestingModule
).
1 |
import { RouterModule, Routes } from '@angular/router'; |
Selanjutnya, tentukan rute Anda dan berikan definisi rute ke metode RouterModule.forRoot
.
1 |
const appRoutes :Routes = [ |
2 |
{ path: '', component: PastebinComponent }, |
3 |
{ path: 'about', component: AboutComponent }, |
4 |
{ path: 'contact', component: ContactComponent}, |
5 |
];
|
6 |
|
7 |
imports: [ |
8 |
BrowserModule, |
9 |
FormsModule, |
10 |
HttpModule, |
11 |
InMemoryWebApiModule.forRoot(InMemoryDataService), |
12 |
RouterModule.forRoot(appRoutes), |
13 |
|
14 |
],
|
Setiap perubahan yang dilakukan untuk AppModule
juga harus dilakukan ke AppTestingModule
. Tetapi jika Anda mengalami eror No base href set saat menjalankan pengujian, tambahkan baris berikut ke array providers
AppTestingModule Anda.
1 |
{provide: APP_BASE_HREF, useValue: '/'} |
Sekarang tambahkan kode berikut untuk app.component.html.
1 |
<nav class="navbar navbar-inverse"> |
2 |
<div class="container-fluid"> |
3 |
<div class="navbar-header"> |
4 |
<div class="navbar-brand" >{{title}}</div> |
5 |
</div>
|
6 |
<ul class="nav navbar-nav bigger-text"> |
7 |
<li>
|
8 |
<a routerLink="" routerLinkActive="active">Pastebin Home</a> |
9 |
</li>
|
10 |
<li>
|
11 |
<a routerLink="/about" routerLinkActive="active">About Pastebin</a> |
12 |
</li>
|
13 |
<li>
|
14 |
<a routerLink="/contact" routerLinkActive="active"> Contact </a> |
15 |
</li>
|
16 |
</ul>
|
17 |
</div>
|
18 |
</nav>
|
19 |
<router-outlet></router-outlet>
|
20 |
|
21 |
routerLink
adalah petunjuk yang digunakan untuk mengikat elemen HTML dengan route. Kami telah menggunakannya dengan tag anchor HTML di sini. RouterOutlet
adalah petunjuk lain yang menandai tempat di template di mana tampilan router harus ditampilkan.
Menguji route agak sulit karena melibatkan lebih banyak interaksi UI. Inilah tes yang memeriksa apakah tautan berfungsi baik.
1 |
describe('AppComponent', () => { |
2 |
beforeEach(async(() => { |
3 |
TestBed.configureTestingModule({ |
4 |
imports: [AppTestingModule], |
5 |
|
6 |
}).compileComponents(); |
7 |
}));
|
8 |
|
9 |
|
10 |
it(`should have as title 'Pastebin Application'`, async(() => { |
11 |
const fixture = TestBed.createComponent(AppComponent); |
12 |
const app = fixture.debugElement.componentInstance; |
13 |
expect(app.title).toEqual('Pastebin Application'); |
14 |
}));
|
15 |
|
16 |
|
17 |
it('should go to url', |
18 |
fakeAsync((inject([Router, Location], (router: Router, location: Location) => { |
19 |
let anchorLinks,a1,a2,a3; |
20 |
let fixture = TestBed.createComponent(AppComponent); |
21 |
fixture.detectChanges(); |
22 |
//Create an array of anchor links
|
23 |
anchorLinks= fixture.debugElement.queryAll(By.css('a')); |
24 |
a1 = anchorLinks[0]; |
25 |
a2 = anchorLinks[1]; |
26 |
a3 = anchorLinks[2]; |
27 |
|
28 |
//Simulate click events on the anchor links
|
29 |
a1.nativeElement.click(); |
30 |
tick(); |
31 |
|
32 |
expect(location.path()).toEqual(""); |
33 |
|
34 |
a2.nativeElement.click(); |
35 |
tick() |
36 |
expect(location.path()).toEqual("/about"); |
37 |
|
38 |
a3.nativeElement.click(); |
39 |
tick() |
40 |
expect(location.path()).toEqual("/contact"); |
41 |
|
42 |
}))));
|
43 |
});
|
Jika semuanya berjalan dengan baik, Anda akan melihat sesuatu seperti ini.



Sentuhan Akhir
Tambahkan desain Bootstrap yang cantik ke proyek Anda, dan sajikan proyek Anda jika Anda belum melakukannya.
1 |
ng serve |
Ringkasan
Kami menulis aplikasi lengkap dari awal dalam lingkungan yang digerakkan oleh pengujian. Bukankah itu sesuatu? Dalam tutorial ini, kita belajar:
- cara mendesain komponen menggunakan pendekatan tes pertama
- bagaimana menulis tes unit dan tes UI dasar untuk komponen
- tentang utilitas pengujian Angular dan bagaimana menggabungkannya ke dalam pengujian kami
- tentang menggunakan
async()
danfakeAsync()
untuk menjalankan pengujian asynchronous - dasar-dasar routing dalam tes Angular dan menulis untuk route
Saya harap Anda menikmati alur kerja TDD. Silakan hubungi melalui komentar dan beri tahu kami pendapat Anda!