Bagi anda yang berpeluang melihat rangka kerja Sudut (V2 dan banyak lagi) anda telah melihat bagaimana dokumentasi di tapak rasmi boleh menjadi banyak, terutamanya pada bahagian Ujian Unit. Tujuan artikel ini adalah untuk memberi anda helaian ringkasan amalan terbaik ujian unit dalam Angular.
Artikel itu menganggap bahawa anda telah menggunakan Angular dan ujian unitnya, dan anda mengetahui medan leksikal Ujian Unit.
Nasihat dan penyelesaian yang diberikan di sini datang daripada pengalaman enam bulan pembangunan Angular dengan pasukan lima orang. Mereka sudah tentu tertakluk kepada penambahbaikan/perbincangan.
Mukadimah: Bagaimana untuk "mengejek" dengan betul dengan Angular?
Mukadimah kecil diperlukan untuk menjelaskan perkara yang bagi saya terlalu kerap diabaikan dalam literatur mengenai subjek ini, dan yang akan menjadi penting untuk pemahaman yang lain:
Olok-olok, Perisik dan Stub lain.
Selalunya, pendekatan yang agak naif ditawarkan dalam contoh ujian unit sudut. Mari ambil kes itu daripada ujian komponen dalam dokumen rasmi:
class MockUserService {
isLoggedIn = true;
user = { name: "Test User" };
}
/* ... */
beforeEach(() => {
TestBed.configureTestingModule({
// provide the component-under-test and dependent service
providers: [
WelcomeComponent,
{ provide: UserService, useClass: MockUserService }
]
});
// inject both the component and the dependent service.
comp = TestBed.get(WelcomeComponent);
userService = TestBed.get(UserService);
});
Kod ini menerangkan bahawa untuk mengejek pelaksanaan perkhidmatan (di sini UserService) cuma buat kelas palsu menggantikan perkhidmatan ini (di sini MockUserService).
Pendekatan ini pada dasarnya menjengkelkan saya kerana kita kehilangan semua jenis pemeriksaan. Malah, tiada jaminan itu MockUserService menghormati antara muka kelas UserService. Ditambah lagi dengan kos menulis kelas palsu...
[bctt tweet=”Dalam Angular, mencipta olok-olok dengan tangan bukanlah satu pendekatan yang selamat dan tidak cekap.”] Satu lagi cara untuk melakukannya, yang boleh dilihat di sana sini, adalah dengan memanjangkan kelas yang ingin kita ejek ketika itu, membebankan tingkah laku yang diingini. Jika kita kembali kepada contoh sebelumnya, ia akan kelihatan seperti ini:
class MockUserService extends UserService {
user = { name: "Another user" };
}
/* ... */
beforeEach(() => {
TestBed.configureTestingModule({
// provide the component-under-test and dependent service
providers: [
WelcomeComponent,
FirstLevelNeededService,
SecondLevelNeededService,
{ provide: UserService, useClass: MockUserService }
]
});
// inject both the component and the dependent service.
comp = TestBed.get(WelcomeComponent);
userService = TestBed.get(UserService);
});
Kaedah ini lebih teruk daripada yang sebelumnya kerana walaupun anda membetulkan masalah menaip dan verbositi, anda telah memecahkan pengasingan yang diperlukan untuk ujian unit.
Seperti pelaksanaan sebenar UserService diimport, TestBed akan memerlukan kami juga menyediakannya dengan semua kelas yang digunakan UserService. Bayangkan skema pergantungan di bawah:
+-----------------+ +-------------------------+ +---- - ----------------------+ | | | | | | | UserService +<---+ FirstLevelNeededService +<----+ SecondLevelNeededService | | | | | | | +-----------------+ +-------------------------+ +---- - -----------------------+
Berikutan skim ini, kami harus menyediakan TestBed, FirstLevelNeededService et SecondLevelNeededService. Ini tidak masuk akal apabila anda memikirkannya kerana tujuan olok-olok adalah untuk mensimulasikan tingkah laku kelas. Jadi mengapa menyediakan kebergantungan olok-olok?
[bctt tweet="Jangan sekali-kali membuat olok-olok kelas TypeScript dengan melanjutkan pelaksanaan asalnya dalam Angular."] Jika tidak, anda perlu menyediakan sejumlah besar kelas untuk setiap suite ujian.
Gunakan perpustakaan olok-olok
Untuk pembangun berorientasikan objek, penggunaan perpustakaan olok-olok adalah jelas dalam konteks ujian unit kerana bahasa mereka selalunya tidak dinamik seperti JavaScript dan tidak membenarkan anda menulis Mocks on the fly ("on the fly"). snatch?” – Siapa yang berkata demikian?).
Pustaka Mock akan menggunakan definisi jenis yang dibuat melalui TypeScript untuk mencipta olok-olok kelas.
Refleks lama "Javaiste" mungkin, saya secara peribadi suka ts mockito sebagai penjual buku Mock, tetapi ada yang lain: TS-olok-olok atau Typemoq
Jika kami mengambil contoh kami dengan ts-mockito, kod kami akan kelihatan seperti ini:
import { instance, mock, when } from "ts-mockito";
/***/
let userServiceMock: UserService;
beforeEach(() => {
userServiceMock = mock(UserService);
when(userServiceMock.user).thenReturn({ name: "A user" });
TestBed.configureTestingModule({
providers: [
WelcomeComponent,
{ provide: UserService, useValue: instance(userServiceMock) }
]
});
userService = TestBed.get(UserService);
});
Mudah, bukan? ts-mockito akan mula-mula membuat kelas olok-olok menggunakan fungsi tersebut mock kemudian buat contoh olok-olok berdasarkan kelas ini dengan fungsi instance.
Untuk mensimulasikan hasil, kami selalunya akan menggunakan fungsi tersebut when. Sentiasa gunakan when pada kelas yang diejek dan panggilan instance hanya selepas itu, jika tidak, contoh olok-olok anda tidak akan mempunyai gelagat yang ditentukan.
Dalam artikel yang lain, kami akan menggunakan ts-mockito secara sistematik
Panduan
Kes n°1: Menguji perkhidmatan tanpa pergantungan dengan Angular
Mari kita mulakan dengan yang paling mudah, perkhidmatan. Dalam Angular, perkhidmatan tidak lebih atau kurang daripada kelas TypeScript yang akan digunakan oleh Bekas IOC of Angular dan disuntik ke dalam semua elemen lain yang akan mentakrifkannya sebagai kebergantungan.
Jika perkhidmatan tidak menggunakan kebergantungan yang datang dari Angular (seperti HttpClient sebagai contoh) anda boleh mengujinya seperti mana-mana kelas TypeScript dalam mana-mana projek seperti ini:
import { MyService } from "./my.service";
import { instance, mock, when } from "ts-mockito";
import { MathLib } from "./my.service";
describe("MyService", () => {
it("should add correctly two positive integers and multiply them by two", () => {
// given
const mathMock = mock(MathLib);
when(mathMock.add(1, 2)).thenReturn(3);
when(mathMock.multiply(3)).thenReturn(6);
const myService = new MyService(instance(mathMock));
// when
const result = myService.addAndMultiplyByTwo(1, 2);
// then
expect(result).toBe(6);
});
});
Perkara pertama, anda akan melihat bahawa bertentangan dengan sudut-cli kita tidak mencipta ujian should be created. Ujian ini yang akan digunakan untuk mengesahkan kewujudan kelas secara de facto telah disahkan oleh ujian unit lain, oleh itu tidak berguna.
Perkara kedua dan paling penting dalam artikel ini:
[bctt tweet=”Gunakan `TestBed` hanya jarang dalam ujian unit sudut.”] The TestBed, lebih khusus panggilan kepada kaedahnya configureTestingModule, panjang dan membawa lebih kerumitan dalam ujian. Dan untuk alasan yang baik, panggilan kaedah TestBed.configureTestingModule akan, seperti namanya, mencipta a modul sudut pada setiap panggilan dan nyatakan semua elemen 'modul palsu' ini terima kasih kepada bekas IOC Angular.
Kami telah memerhatikan dalam ujian kami bahawa penggunaan TestBed berpotensi mengambil masa lima kali lebih lama daripada instantiasi objek mudah dan olok-oloknya seperti dalam contoh kami (maklumat lanjut di sini): Angular TestBed terlalu panjang)
Kami tidak mencipta apa-apa di sana, ia dipanggil a Ujian Terpencil dalam dokumen Angular (yang saya tidak dapat mencari pautan lagi kerana ia besar: D)
Ok, tetapi apa yang berlaku jika saya menggunakan Module datang dari Angular seperti HttpModule ?
Kes n°2: Menguji perkhidmatan yang bergantung pada modul Angular
Mari kita ambil ujian yang agak standard bagi perkhidmatan pengesahan yang menggunakan HttpClient :
import { AuthService } from "./auth.service";
import { inject, TestBed } from "@angular/core/testing";
import {
HttpClientTestingModule,
HttpTestingController
} from "@angular/common/http/testing";
describe("AuthService", () => {
beforeEach(() => {
TestBed.configureTestingModule({
providers: [AuthService],
imports: [HttpClientTestingModule]
});
});
it(
"should return null user when server responds HTTP error",
inject(
[HttpTestingController, AuthService],
(httpMock: HttpTestingController, service: AuthService) => {
service.user.subscribe(user => {
expect(user).toBe(null);
});
httpMock.expectOne("auth/user").error(new ErrorEvent("401"));
httpMock.verify();
}
)
);
});
Kami tidak mempunyai pilihan selain menggunakan TestBed kerana dialah yang membenarkan akses kepada httpMock bertanggungjawab untuk mensimulasikan respons pelayan.
[bctt tweet=”Kami hanya akan menggunakan `TestBed` untuk menguji komponen atau kelas yang bergantung pada modul Angular.”] Anda juga akan melihat penggunaan pembantu kecil inject, berguna untuk mendapatkan semula kejadian yang dibuat di dalam bekas IOC Angular. Anda juga mungkin perasan bahawa saya beforeEach tidak mengandungi panggilan ke async. Async cenderung digunakan secara sembarangan, tetapi fungsi ini ada untuk menunggu asynchrony.
Jika anda melihat pada dokumentasi rasmi configureTestingModule jangan balik dari Promise, jadi ia bukan tak segerak, jadi ia tidak berbaloi untuk dipanggil async dalam kes ini.
Kes n°3: Menguji komponen "asas".
Ini adalah jenis komponen yang anda banyak tulis dalam Angular.
Kami akan melihat dalam kes berikut bahawa komponen yang terletak tinggi dalam hierarki juga akan menjadi subjek pemprosesan tambahan.
Untuk menggambarkan ujian ini, kami akan membayangkan sedang dalam proses menyemak komponen ListComponent yang memaparkan senarai yang dimuatkan daripada perkhidmatan ItemsService : kes buku teks…
Cara yang betul untuk melakukannya akan kelihatan seperti ini:
import { ComponentFixture, TestBed } from "@angular/core/testing";
import { anyString, instance, mock, when } from "ts-mockito";
import { ListComponent } from "./list.component";
import { ItemsService } from "./item/items.service";
import { Item } from "./item/item.model";
import { of } from "rxjs/observable/of";
describe("ListComponent", () => {
let component: ListComponent;
let fixture: ComponentFixture<ListComponent>;
let mockedItemsService: ItemsService;
beforeEach(() => {
mockedItemsService = mock(ItemsService);
});
async function configureTestingModule() {
await TestBed.configureTestingModule({
providers: [
{ provide: ItemsService, useValue: instance(mockedItemsService) }
],
declarations: [ListComponent]
}).compileComponents();
fixture = TestBed.createComponent(ListComponent);
component = fixture.componentInstance;
fixture.detectChanges();
}
it("should display an empty list", async () => {
// given
when(mockedItemsService.loadItems()).thenReturn(of([new Item()]));
// when
await configureTestingModule();
// then
const nbEl = fixture.debugElement.queryAll(By.css("li")).length;
expect(nbEl).toEqual(0);
});
});
Perkara pertama yang mungkin mengejutkan anda ialah fungsinya configureTestingModule di luar beforeEach. Memang, adalah perlu untuk memanggilnya dalam ujian unit kami dan bukan sebelum ini untuk mengekalkan susunan panggilan fungsi mockito yang disebutkan dalam mukadimah:
mengejek -> bila -> contoh
mock akan dipanggil dalam beforeEach kemudian when pada permulaan ujian unit dan akhirnya instance melalui panggilan daripada configureTestingModule di tengah-tengah ujian unit.
Jika kita fokus pada configureTestingModule, kami melihat bahawa ia tidak segerak kerana kami menyeru compileComponents yang membalas janji. ini async "mencemarkan" ujian unit kami kerana kami perlu menandakannya sebagai async juga.
Kami juga akan mendapati bahawa kami membebankan pembekal dengan ItemsService untuk memberikannya contoh olok-olok ts-mockito kami:
{ sediakan: ItemsService, useValue: instance(mockedItemsService) }
Ini akan memastikan bahawa TestBed menyuntik simulakrum ini ItemsService kepada komponen apabila ia dicipta.
Akhirnya, kami mensimulasikan pemulangan kaedah loadItems perkhidmatan supaya ia kembali a Observable<Item[]> kosong dan kami menyemak bahawa senarai html elemen memang kosong.
Kes n°4: Menguji komponen peringkat tinggi
Apa yang saya panggil komponen "peringkat tinggi", ialah komponen yang tinggi dalam hierarki komponen aplikasi kami. Halaman komponen atau mengandungi banyak kanak-kanak sepadan dengan sempurna dengan denominasi ini. yang AppComponent akan menjadi contoh yang digunakan.
Perkara yang menjengkelkan untuk memeriksa kelakuan komponen jenis ini: mereka bergantung mengikut definisi pada banyak sub-komponen dan sub-modul dan ia akan menjadi perlu Tous tambah mereka dalam TestBed. Untuk AppComponent ia seperti memberinya hampir keseluruhan aplikasi dalam pergantungan...
Jika, lebih-lebih lagi, saya memberitahu anda bahawa TestBed.configureTestingModule lambat dalam kes #1, ia menjadi lebih perlahan apabila bilangan kebergantungan meningkat.
Untuk mengatasi masalah ini, terdapat parameter schemas untuk diberikan kepada TestBed.configureTestingModule dengan nilai NO_ERRORS_SCHEMA :
import { TestBed } from "@angular/core/testing";
import { AppComponent } from "./app.component";
import { NO_ERRORS_SCHEMA } from "@angular/core";
describe("AppComponent", () => {
let component: ListComponent;
let fixture: ComponentFixture<ListComponent>;
beforeEach(async () => {
TestBed.configureTestingModule({
providers: [],
imports: [],
declarations: [AppComponent],
schemas: [NO_ERRORS_SCHEMA]
}).compileComponents();
fixture = TestBed.createComponent(ListComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it("should render title in a h1 tag", async () => {
// given
const selectorText = compiled.querySelector("h1").textContent;
// then
expect(selectorText).toBe("My awesome app");
});
});
Parameter ajaib ini, mengkonfigurasi TestBed supaya ia tidak menimbulkan ralat jika ia tidak dapat mencari pembekal komponen atau modul yang digunakan dalam templat komponen yang diuji. Kaedah ini dipanggil shallow testing. Kami hanya menguji komponen AppComponent dan tidak mentafsir subkomponennya.
Le NO_ERRORS_SCHEMA adalah untuk digunakan dengan berhati-hati, kerana digunakan di mana-mana, ia menjadikan semakan sistem pergantungan tidak aktif dan boleh menyebabkan anda terlupa untuk memuatkan komponen apabila ia perlu dalam kes ujian anda.
[bctt tweet=”Dalam Sudut, `NO_ERRORS_SCHEMA` harus digunakan dengan berhati-hati dan hanya untuk menguji komponen yang tinggi dalam hierarki.”]
Kes n°5: Paip, kelas, selebihnya
Paip, seperti elemen lain yang boleh mengarang aplikasi anda, akan diuji seperti perkhidmatan tanpa pergantungan Sudut, iaitu kelas TypeScript asas.
Mari kita bayangkan paip yang matlamatnya adalah untuk bergabung dalam bentuk rentetan aksara semua kekunci objek yang nilainya bukan nol. Kami akan memanggilnya AliciaKeys… (ya, kami cukup bangga dengan nama itu)
import { Pipe, PipeTransform } from "@angular/core";
@Pipe({
name: "aliciaKeys"
})
export class AliciaKeys implements PipeTransform {
transform(obj: any, arg: string[]): string {
return obj
? Object.entries(obj)
.filter(([key, value]) => !!value)
.map(([key]) => key)
.join(", ")
: "";
}
}
Fail ujian unit yang dilampirkan adalah remeh kerana ia adalah instantiasi mudah kelas AliciaKeys dengan dua panggilan daripada transform dalam dua kes berbeza:
import { AliciaKeys } from "./aliciakeys.pipe";
describe("AliciaKeys", () => {
it("should return valued keys", () => {
const result = new AliciaKeys().transform({
validKey: {},
inValidKey: null,
anotherValidKey: 1
});
expect(result).toEqual("validKey, anotherValidKey");
});
it("should return blank when input is falsy", () => {
const result = new AliciaKeys().transform(null;
expect(result).toEqual("");
});
});
Kesimpulan & pergi lebih jauh
Kami berharap setiap perihalan kes akan membantu anda juga, untuk lebih memahami cara menguji unit aplikasi Angular anda dengan betul.
Perkara penting yang perlu diingat ialah kelas dalam Angular, kekal sebagai kelas TypeScript klasik dan ia boleh diuji dengan mudah. Perkara kedua ialah TestBed tidak sistematik, bahawa ia harus digunakan hanya apabila anda tidak mempunyai pilihan. Akhir sekali, olok-olok dalam Angular mesti diuruskan dengan perpustakaan olok-olok: Dalam kes kami ts-mockito.
Anda harus tahu bahawa dalam projek kami, kami mempunyai kira-kira 700 ujian unit yang mengambil masa lebih daripada 30 saat untuk dijalankan. Memperkemas ujian dan mempercepatkannya menjadi penting bagi kami.
Kami juga telah meneruskan usaha untuk pecutan ini dengan melepasi yang terakhir ini isyarat. Ini sepenuhnya mungkin dengan mengikuti arahan dalam repo jest-preset-sudut dan ia menjimatkan banyak masa!
Kami akan kembali kepada anda dengan butiran lanjut tentang subjek ini semasa maklum balas "JS-Talks" kami (Hari tontonan teknologi yang kami lakukan setiap bulan). Beberapa foto akhir untuk melihat rupanya:

Publié par Matthew Breton CTO di JS-Republic
