Advertisement
  1. Code
  2. Angular

Komponen Pengujian dalam Angular Menggunakan Jasmine: Bagian 2, Layanan

Scroll to top
Read Time: 22 min
This post is part of a series called Testing Components in Angular Using Jasmine.
Testing Components in Angular Using Jasmine: Part 1

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

Final product imageFinal product imageFinal product image
What You'll Be Creating

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.

High level overview of things weve discussed in the previous tutorial and things we will be discussing in this tutorialHigh level overview of things weve discussed in the previous tutorial and things we will be discussing in this tutorialHigh level overview of things weve discussed in the previous tutorial and things we will be discussing in this 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.

Mock design of the ViewPasteComponent in edit modeMock design of the ViewPasteComponent in edit modeMock design of the ViewPasteComponent in edit mode
Mode edit
Mock design of the ViewPasteComponent in view modeMock design of the ViewPasteComponent in view modeMock design of the ViewPasteComponent in view mode
Mode view

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">&times;</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">&times;</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">&times;</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:

  1. Template komponen yang menerima input.
  2. Masukan template terikat untuk properti paste komponen.
  3. 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.

Screenshot of Karma test runner on Chrome displaying the final test resultsScreenshot of Karma test runner on Chrome displaying the final test resultsScreenshot of Karma test runner on Chrome displaying the final test results

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() dan fakeAsync() 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!

Advertisement
Did you find this post useful?
Want a weekly email summary?
Subscribe below and we’ll send you a weekly email summary of all new Code tutorials. Never miss out on learning about the next big thing.
Advertisement
Looking for something to help kick start your next project?
Envato Market has a range of items for sale to help get you started.