<ruby id="bdb3f"></ruby>

    <p id="bdb3f"><cite id="bdb3f"></cite></p>

      <p id="bdb3f"><cite id="bdb3f"><th id="bdb3f"></th></cite></p><p id="bdb3f"></p>
        <p id="bdb3f"><cite id="bdb3f"></cite></p>

          <pre id="bdb3f"></pre>
          <pre id="bdb3f"><del id="bdb3f"><thead id="bdb3f"></thead></del></pre>

          <ruby id="bdb3f"><mark id="bdb3f"></mark></ruby><ruby id="bdb3f"></ruby>
          <pre id="bdb3f"><pre id="bdb3f"><mark id="bdb3f"></mark></pre></pre><output id="bdb3f"></output><p id="bdb3f"></p><p id="bdb3f"></p>

          <pre id="bdb3f"><del id="bdb3f"><progress id="bdb3f"></progress></del></pre>

                <ruby id="bdb3f"></ruby>

                至此,一個具有完整輸入、輸出的組件便已經被我們驕傲的開發完畢了。但請考慮以下問題: * ① 新增組件的輸入功能后,是否對本組件的歷史功能產生了影響 * ② 將新增組件應用于第三方組件中,是否對第三方組件的功能產生了影響 如果我們對其產生了影響那么你認為當前都產生了什么影響 ,產生的原因又是什么,同時又是計劃如何修正的。 # 會說話的代碼 如果你僅憑想像便給出了自己的答案,那么無論你的答案是什么,都將是蒼白而無力的。在軟件開發的領域里沒有實踐就沒有發言權,在沒有真實的實踐以前,任何主觀的錯誤預計都是耍流氓。而單元測試就不會說謊。在此,我們不防借助單元測試來回答一下前面的問題。 #### 測試本組件 找到klass/teacher-select/teacher-select.component.spec.ts并把`describe`修改為`fdescribe`,然后運行`ng test`來觀察會發生什么。 ![](https://img.kancloud.cn/64/e8/64e8dce5526bc82fe69aa24f1e95bf27_826x192.png) ``` LOG: 'data emit', Teacher{id: 1, name: '潘杰', username: 'panjie', email: undefined, sex: undefined} Chrome 78.0.3904 (Mac OS X 10.13.6): Executed 0 of 13 SUCCESS (0 secs / 0 secs) LOG: '潘杰' Chrome 78.0.3904 (Mac OS X 10.13.6): Executed 1 of 13 SUCCESS (0 secs / 0.095 secs) LOG: '張喜碩' Chrome 78.0.3904 (Mac OS X 10.13.6): Executed 1 of 13 SUCCESS (0 secs / 0.095 secs) Chrome 78.0.3904 (Mac OS X 10.13.6): Executed 2 of 13 (skipped 11) SUCCESS (0.15 secs / 0.121 secs) TOTAL: 2 SUCCESS TOTAL: 2 SUCCESS ``` 這說明當前組件新功能的加入未對`組件彈出器`及`獲取教師列表后選擇教師`功能造成影響。隨后我們打開[http://localhost:4200/klass/add](http://localhost:4200/klass/add)測試相關功能運行正常。 **測試完成后,將`fdescribe`恢復為`describe`** #### 測試班級添加組件 我們再打開klass/add/add.component.spec.ts,并把`describe`修改為`fdescribe`,然后運行`ng test`來觀察會發生什么: ![](https://img.kancloud.cn/c0/12/c012d2ce849df89b146ba8cb3a0ceaa9_482x252.png) ``` Chrome 78.0.3904 (Mac OS X 10.13.6): Executed 3 of 13 (3 FAILED) (skipped 10) ERROR (0.228 secs / 0.201 secs) ``` 這說明:當前組件已被變更,而變更后無法滿足歷史的單元測試要求或未動該組件的變更進行測試。事實也的確如此,我們需要在班級新增、班級編輯組件中引入了選擇教師組件,但卻沒有對引入教師組件后原組件的功能是否正常進行測試。 ## 修正錯誤 當一個組件A依賴于其它組件B時,在進行測試的過程中也需要對B組件進行聲明。打開klass/add/add.component.spec.ts ``` beforeEach(async(() => { TestBed.configureTestingModule({ declarations: [AddComponent, TeacherSelectComponent?], imports: [ FormsModule, ReactiveFormsModule, HttpClientTestingModule ] }) .compileComponents(); })); ``` #### 測試 ![](https://img.kancloud.cn/39/df/39dfe2b9fbdf74e3caac39ed4e194efb_673x94.png) 提示找有找到`Router`的`provider`,則加入`RouterTestingModule` ``` beforeEach(async(() => { TestBed.configureTestingModule({ declarations: [AddComponent, TeacherSelectComponent], imports: [ FormsModule, ReactiveFormsModule, HttpClientTestingModule, RouterTestingModule ? ] }) .compileComponents(); })); ``` 再測試 ![](https://img.kancloud.cn/ae/32/ae32b162213af4ad3a1143a2bce9a7b7_911x131.png) 此時基礎的依賴錯誤提示已經完全消除,而上圖得到的便是一個真真切切的錯誤了。此錯誤表示:對該組件進行變更(引入了選擇教師組件)后,對原組件的正常功能產生了影響。 提示信息再說:我們預測應該得到2,但實際上卻得到了null。我們此時可以訪問[http://localhost:4200/klass/add](http://localhost:4200/klass/add)來驗證單元測試拋出的錯誤信息是否是真真切切有幫助的。 ![](https://img.kancloud.cn/97/f9/97f91c533b60a3d0f40805c20102c777_595x214.png) 通過點擊測試我們發現當點擊`保存`按鈕時并沒有進行數據提交,而是在控制臺中報了以上錯誤。 ### 錯誤 根據單元測試的提示,我們來到測試文件79行所在的測試方法: ``` it('測試V層向C層綁定', () => { expect(component).toBeTruthy(); fixture.whenStable().then(() => { const debugElement: DebugElement = fixture.debugElement; const nameElement = debugElement.query(By.css('#name')); const nameInput: HTMLInputElement = nameElement.nativeElement; nameInput.value = 'test2'; nameInput.dispatchEvent(new Event('input')); expect(component.name.value).toBe('test2'); const teacherIdElement = debugElement.query(By.css('#teacherId')); const teacherIdInput: HTMLInputElement = teacherIdElement.nativeElement; teacherIdInput.value = '2'; teacherIdInput.dispatchEvent(new Event('input')); expect(component.teacherId.value).toBe(2); }); }); ``` 該方法是通過設置input的值來達到改變teacherId的目的。但引入選擇教師組件后,已經沒有teacherId這個input了,所以后續的測試代碼當然也就隨著發生了問題了。 ### 修正組件功能 此時我們來到klass/add/add.component.ts中,發現原來我們在前面引入選擇教師組件后并沒有增加相應的功能。這也就是難怪該單元測試會報錯了。 下面我們對功能進行修正,并重新修正單元測試。 ``` teacherId: FormControl; ? teacher: Teacher; ? ... ngOnInit() { this.name = new FormControl(''); this.teacherId = new FormControl(); ? } ... const klass = new Klass(undefined, this.name.value, new Teacher(parseInt(this.teacherId.value, 10), undefined, undefined) ? this.teacher ? ); ... /** * 當選擇某個教師時觸發 * @param {Teacher} teacher 教師 */ onTeacherSelected(teacher: Teacher) { console.log(teacher); ? this.teacher = teacher; ? } ``` ### 修正單元測試 ① 刪除設置teacherId這個input的測試代碼 ② 測試當選擇教師組件數據變更后,點擊保存按鈕觸發了正確的HTTP請求 > 該部分代碼重構有一定的難度,第一次閱讀時可忽略。 部分代碼如下: ``` /** * 設置表單數據 * 點擊按鈕發起請求 * 斷言:請求地址、請求方法、發送的數據 */ it('保存按鈕點擊后,提交相應的http請求', () => { httpTestingController = TestBed.get(HttpTestingController); expect(component).toBeTruthy(); component.name.setValue('test3'); component.teacher = new Teacher(2, null, null, null); ? fixture.whenStable().then(() => { const debugElement: DebugElement = fixture.debugElement; const submitButtonElement = debugElement.query(By.css('button')); const submitButton: HTMLButtonElement = submitButtonElement.nativeElement; submitButton.click(); const req = httpTestingController.expectOne('http://localhost:8080/Klass'); expect(req.request.method).toEqual('POST'); const klass: Klass = req.request.body.valueOf(); expect(klass.name).toEqual('test3'); expect(klass.teacher.id).toEqual(2); ? req.flush(null, {status: 201, statusText: 'Accepted'}); }); ``` 整體代碼如下: ``` import {async, ComponentFixture, TestBed} from '@angular/core/testing'; import {AddComponent} from './add.component'; import {FormsModule, ReactiveFormsModule} from '@angular/forms'; import {DebugElement} from '@angular/core'; import {By} from '@angular/platform-browser'; import {HttpClientTestingModule, HttpTestingController} from '@angular/common/http/testing'; import {Klass} from '../../norm/entity/Klass'; import {TeacherSelectComponent} from '../teacher-select/teacher-select.component'; import {RouterTestingModule} from '@angular/router/testing'; import {Teacher} from '../../norm/entity/Teacher'; fdescribe('Klass/AddComponent', () => { let component: AddComponent; let fixture: ComponentFixture<AddComponent>; let httpTestingController: HttpTestingController; beforeEach(async(() => { TestBed.configureTestingModule({ declarations: [AddComponent, TeacherSelectComponent], imports: [ FormsModule, ReactiveFormsModule, HttpClientTestingModule, RouterTestingModule ] }) .compileComponents(); })); beforeEach(() => { fixture = TestBed.createComponent(AddComponent); component = fixture.componentInstance; fixture.detectChanges(); }); /** * 測試C層向V層數據綁定 * 在C層中使用setValue方法對表單項賦值 * 重新渲染V層后,使用CSS選擇器來獲取元素 * 獲取元素的值并斷言 */ it('測試C層向V層數據綁定', () => { expect(component).toBeTruthy(); component.name.setValue('test'); fixture.detectChanges(); fixture.whenStable().then(() => { const debugElement: DebugElement = fixture.debugElement; const nameElement = debugElement.query(By.css('#name')); const nameInput: HTMLInputElement = nameElement.nativeElement; expect(nameInput.value).toBe('test'); }); }); /** * 測試V層向C層綁定 * 獲取V層的元素,并設置元素的值 * 斷言在C層中獲取到了元素的值 */ it('測試V層向C層綁定', () => { expect(component).toBeTruthy(); const debugElement: DebugElement = fixture.debugElement; const nameElement = debugElement.query(By.css('#name')); const nameInput: HTMLInputElement = nameElement.nativeElement; nameInput.value = 'test2'; nameInput.dispatchEvent(new Event('input')); expect(component.name.value).toBe('test2'); }); /** * 設置表單數據 * 點擊按鈕發起請求 * 斷言:請求地址、請求方法、發送的數據 */ it('保存按鈕點擊后,提交相應的http請求', () => { httpTestingController = TestBed.get(HttpTestingController); expect(component).toBeTruthy(); component.name.setValue('test3'); component.teacher = new Teacher(2, null, null, null); fixture.whenStable().then(() => { const debugElement: DebugElement = fixture.debugElement; const submitButtonElement = debugElement.query(By.css('button')); const submitButton: HTMLButtonElement = submitButtonElement.nativeElement; submitButton.click(); const req = httpTestingController.expectOne('http://localhost:8080/Klass'); expect(req.request.method).toEqual('POST'); const klass: Klass = req.request.body.valueOf(); expect(klass.name).toEqual('test3'); expect(klass.teacher.id).toEqual(2); req.flush(null, {status: 201, statusText: 'Accepted'}); }); }); }); ``` ## 測試編輯組件并進行修正 參考新增班級的代碼,我們修正編輯班級代碼如下: klass/edit/edit.component.html ``` <h3>編輯班級</h3> <form (ngSubmit)="onSubmit()" [formGroup]="formGroup"> <label for="name">名稱:<input id="name" type="text" formControlName="name"/></label> <label for="teacherId">教師:<app-teacher-select *ngIf="teacher" id="teacherId" [teacher]="teacher" (selected)="onSelected($event)"①></app-teacher-select></label> <button>更新</button> </form> ``` klass/edit/edit.component.ts ``` import {Component, OnInit} from '@angular/core'; import {FormControl, FormGroup} from '@angular/forms'; import {ActivatedRoute, Router} from '@angular/router'; import {HttpClient} from '@angular/common/http'; import {Klass} from '../../norm/entity/Klass'; import {Teacher} from '../../norm/entity/Teacher'; @Component({ selector: 'app-edit', templateUrl: './edit.component.html', styleUrls: ['./edit.component.sass'] }) export class EditComponent implements OnInit { formGroup: FormGroup; teacher: Teacher; private url: string; constructor(private route: ActivatedRoute, private router: Router, private httpClient: HttpClient) { } private getUrl(): string { return this.url; } /** * 加載要編輯的班級數據 */ loadData(): void { this.httpClient.get(this.getUrl()) .subscribe((klass: Klass) => { this.formGroup.setValue({name: klass.name}); this.teacher = klass.teacher; }, () => { console.error(`${this.getUrl()}請求發生錯誤`); }); } ngOnInit() { this.formGroup = new FormGroup({ name: new FormControl(), }); this.route.params.subscribe((param: { id: number }) => { this.setUrlById(param.id); this.loadData(); }); } /** * 用戶提交時執行的操作 */ onSubmit(): void { const data = { name: this.formGroup.value.name, teacher: this.teacher }; this.httpClient.put(this.getUrl(), data) .subscribe(() => { this.router.navigateByUrl('/klass'); }, () => { console.error(`在${this.getUrl()}上的PUT請求發生錯誤`); }); } /** * 選中某個教師時 * @param teacher 教師 */ onSelected(teacher: Teacher): void { this.teacher = teacher; } private setUrlById(id: number): void { this.url = `http://localhost:8080/Klass/${id}`; } } ``` #### 單元測試 修正單元測試如下(后面好要講相關知識,第一遍可略過): klass/edit/edit.component.spec.ts ``` import {async, ComponentFixture, fakeAsync, TestBed, tick} from '@angular/core/testing'; import {EditComponent} from './edit.component'; import {ReactiveFormsModule} from '@angular/forms'; import {RouterTestingModule} from '@angular/router/testing'; import {ActivatedRoute, Router} from '@angular/router'; import {HttpClientTestingModule, HttpTestingController} from '@angular/common/http/testing'; import {ActivatedRouteStub} from './activated-route-stub'; import {Klass} from '../../norm/entity/Klass'; import {Teacher} from '../../norm/entity/Teacher'; import {FormTest} from '../../testing/FormTest'; import SpyObj = jasmine.SpyObj; import {Test} from 'tslint'; import {TeacherSelectComponent} from '../teacher-select/teacher-select.component'; describe('klass EditComponent', () => { let component: EditComponent; let fixture: ComponentFixture<EditComponent>; beforeEach(async(() => { const routerSpy = jasmine.createSpyObj<Router>('Router', ['navigateByUrl']); TestBed.configureTestingModule({ declarations: [EditComponent, TeacherSelectComponent], imports: [ ReactiveFormsModule, HttpClientTestingModule, RouterTestingModule ], providers: [ {provide: ActivatedRoute, useClass: ActivatedRouteStub}, {provide: Router, useValue: routerSpy} ] }) .compileComponents(); })); beforeEach(() => { fixture = TestBed.createComponent(EditComponent); component = fixture.componentInstance; fixture.detectChanges(); }); /** * 組件初始化 * 發送路由參數 * 斷言發起了HTTP請求 * 斷言請求的方法為PUT */ it('should create', () => { expect(component).toBeTruthy(); let route: ActivatedRouteStub; route = TestBed.get(ActivatedRoute); route.subject.next({id: 1}); testGetHttp(1); }); /** * 測試組件發起的GET請求 * 斷言請求地址及方法 * 返回數據后,斷言input項成功綁定返回數據 * @param id 請求的班級ID */ const testGetHttp = (id: number) => { const httpTestingController: HttpTestingController = TestBed.get(HttpTestingController); const req = httpTestingController.expectOne(`http://localhost:8080/Klass/${id}`); expect(req.request.method).toEqual('GET'); req.flush(new Klass(id, '測試編輯班級', new Teacher(1, null, null))); fixture.whenStable().then(() => { expect(FormTest.getInputValueByFixtureAndCss(fixture, '#name')).toEqual('測試編輯班級'); onSubmitTest(1); }); }; /** * 數據更新測試,步驟: * 1. 設置路由參數 * 2. 輸入input的值 * 3. 點擊提交扭鈕:斷言向預期的地址以對應的方法提交了表單中的數據 * 4. 斷言跳轉到''路由地址 */ const onSubmitTest = (id: number) => { FormTest.setInputValue(fixture, '#name', '測試更新班級'); component.teacher = new Teacher(100, null, null, null); fixture.whenStable().then(() => { FormTest.clickButton(fixture, 'button'); const httpTestController: HttpTestingController = TestBed.get(HttpTestingController); const req = httpTestController.expectOne(`http://localhost:8080/Klass/${id}`); expect(req.request.method).toEqual('PUT'); const klass: Klass = req.request.body.valueOf(); expect(klass.name).toEqual('測試更新班級'); expect(klass.teacher.id).toEqual(100); const routerSpy: SpyObj<Router> = TestBed.get(Router); expect(routerSpy.navigateByUrl.calls.any()).toBe(false); req.flush(null, {status: 204, statusText: 'No Content'}); expect(routerSpy.navigateByUrl.calls.any()).toBe(true); httpTestController.verify(); }); }; }); ``` # 總結 單元測試是會說話的代碼,它能夠自動判斷在新增或修改一些功能后本組件是否按原預期正常運行。如果偏離了原預期,將會自動發出警告。單元測試是保障軟件質量的重要手段,是軟件開發中非常重要的一環。 # 參考文檔 | 名稱 | 鏈接 | 預計學習時長(分) | | --- | --- | --- | | 源碼地址 | [https://github.com/mengyunzhi/spring-boot-and-angular-guild/releases/tag/step3.5.6](https://github.com/mengyunzhi/spring-boot-and-angular-guild/releases/tag/step3.5.6) | - | | 帶有輸入輸出參數組件的測試 | [https://www.angular.cn/guide/testing#component-with-inputs-and-outputs](https://www.angular.cn/guide/testing#component-with-inputs-and-outputs) | 15 | | 位于測試宿主中的組件| [https://www.angular.cn/guide/testing#component-inside-a-test-host](https://www.angular.cn/guide/testing#component-inside-a-test-host) | 10 |
                  <ruby id="bdb3f"></ruby>

                  <p id="bdb3f"><cite id="bdb3f"></cite></p>

                    <p id="bdb3f"><cite id="bdb3f"><th id="bdb3f"></th></cite></p><p id="bdb3f"></p>
                      <p id="bdb3f"><cite id="bdb3f"></cite></p>

                        <pre id="bdb3f"></pre>
                        <pre id="bdb3f"><del id="bdb3f"><thead id="bdb3f"></thead></del></pre>

                        <ruby id="bdb3f"><mark id="bdb3f"></mark></ruby><ruby id="bdb3f"></ruby>
                        <pre id="bdb3f"><pre id="bdb3f"><mark id="bdb3f"></mark></pre></pre><output id="bdb3f"></output><p id="bdb3f"></p><p id="bdb3f"></p>

                        <pre id="bdb3f"><del id="bdb3f"><progress id="bdb3f"></progress></del></pre>

                              <ruby id="bdb3f"></ruby>

                              哎呀哎呀视频在线观看