• 日常搜索
  • 百度一下
  • Google
  • 在线工具
  • 搜转载

使用Jasmine在Angular中测试组件第2部分:服务

这是使用 Jasmine 在 angular 中进行测试系列的第二部分。在教程的第一部分,我们为 Pastebin 类和 Pastebin 组件编写了基本的单元测试。最初失败的测试后来变成了绿色。 

概述

这是我们将在本教程的第二部分中进行的工作的概述。

使用Jasmine在Angular中测试组件第2部分:服务  第1张

在本教程中,我们将:

  • 创建新组件并编写更多单元测试

  • 为组件的 UI 编写测试

  • 为 Pastebin 服务编写单元测试

  • 使用输入和输出测试组件

  • 使用路由测试组件

让我们开始吧!

添加粘贴(续)

我们已经完成了为 AddPaste 组件编写单元测试的过程的一半。这是我们在该系列的第一部分中停止的地方。 

  it('should display the `create Paste` button', () => {
     //There should a create button in view      expect(element.innerText).toContain("create Paste");
  });

  it('should not display the modal unless the button is clicked', () => {
      //source-model is an id for the modal. It shouldn't show up unless create button is clicked
      expect(element.innerhtml).not.toContain("source-modal");
  })

  it('should display the modal when `create Paste` is clicked', () => {

      let createPasteButton = fixture.debugElement.query(By.css("button"));
      //triggereventHandler simulates a click event on the button object
      createPasteButton.triggerEventHandler('click',null);
      fixture.detectChanges();
      expect(element.innerHTML).toContain("source-modal");
     
  })

})

如前所述,我们不会编写严格的 UI 测试。相反,我们将为 UI 编写一些基本测试,并寻找测试组件逻辑的方法。 

点击操作是使用方法触发的 DebugElement.triggerEventHandler(),该方法是 Angular 测试实用程序的一部分。 

AddPaste 组件本质上是关于创建新的粘贴;因此,组件的模板应该有一个按钮来创建一个新的粘贴。单击该按钮应生成一个 id 为“source-modal”的“模态窗口”,否则应保持隐藏状态。模态窗口将使用 Bootstrap 设计;因此,您可能会在模板中找到很多 CSS 类。

add-paste 组件的模板应如下所示:

<!--- add-paste.component.html -->

<div class="add-paste">
    <button> create Paste </button>
  <div  id="source-modal" class="modal fade in">
    <div class="modal-dialog" >
      <div class="modal-content">
        <div class="modal-header"></div>
        <div class="modal-body"></div>
         <div class="modal-footer"></div>
     </div>
    </div>
  </div>
</div>

第二个和第三个测试没有提供有关组件实现细节的任何信息。这是add-paste.component.spec.ts的修订版 

 it('should not display the modal unless the button is clicked', () => {
   
   //source-model is an id for the modal. It shouldn't show up unless create button is clicked
    expect(element.innerHTML).not.toContain("source-modal");

   //Component's showModal property should be false at the moment
    expect(component.showModal).toBeFalsy("Show modal should be initially false");
 })

 it('should display the modal when `create Paste` is clicked',() => {
   
    let createPasteButton = fixture.debugElement.query(By.css("button"));
    //create a spy on the createPaste  method
    spyOn(component,"createPaste").and.callThrough();
    
    //triggerEventHandler simulates a click event on the button object
    createPasteButton.triggerEventHandler('click',null);
    
    //spy checks whether the method was called
    expect(component.createPaste).toHaveBeenCalled();
    fixture.detectChanges();
    expect(component.showModal).toBeTruthy("showModal should now be true");
    expect(element.innerHTML).toContain("source-modal");
 })

修改后的测试更加明确,因为它们完美地描述了组件的逻辑。这是 AddPaste 组件及其模板。

<!--- add-paste.component.html -->

<div class="add-paste">
  <button (click)="createPaste()"> create Paste </button>
  <div *ngIf="showModal" id="source-modal" class="modal fade in">
    <div class="modal-dialog" >
      <div class="modal-content">
        <div class="modal-header"></div>
        <div class="modal-body"></div>
         <div class="modal-footer"></div>
     </div>
    </div>
  </div>
</div>
/* add-paste.component.ts */

export class AddPasteComponent implements OnInit {

  showModal: boolean = false;
  // Languages imported from Pastebin class
  languages: string[] = Languages;  
  constructor() { }
  ngOnInit() { }
  
  //createPaste() gets invoked from the template. 
  public createPaste():void {
  	this.showModal = true;
  }
}

测试应该仍然失败,因为间谍addPaste无法在 PastebinService 中找到这样的方法。让我们回到 PastebinService 并在上面添加一些内容。 

为服务编写测试

在我们继续编写更多测试之前,让我们向 Pastebin 服务添加一些代码。 

public addPaste(pastebin: Pastebin): Promise<any> {
    return this.http.post(this.pastebinUrl, JSON.stringify(pastebin), {headers: this.headers})
	   .toPromise()
 	   .then(response =>response.json().data)
 	   .catch(this.handleError);
}

addPaste() 是服务创建新粘贴的方法。  http.post返回一个 observable,使用该toPromise() 方法将其转换为一个 Promise。响应被转换成 JSON 格式,任何运行时异常都会被handleError().

您可能会问,我们不应该为服务编写测试吗?我的回答是肯定的。通过依赖注入 (DI) 注入到 Angular 组件中的服务也容易出错。此外,Angular 服务的测试相对容易。PastebinService 中的方法应该类似于四个 CRUD 操作,并带有一个处理错误的附加方法。方法如下:

  • 处理错误()

  • getPastebin()

  • 添加粘贴()

  • 更新粘贴()

  • 删除粘贴()

我们已经实现了列表中的前三个方法。让我们尝试为他们编写测试。这是描述块。

import { TestBed, inject } from '@angular/core/testing';
import { Pastebin, Languages } from './pastebin';
import { PastebinService } from './pastebin.service';
import { AppModule } from './app.module';
import { HttpModule } from '@angular/http';

let testService: PastebinService;
let mockPaste: Pastebin;
let responsePropertyNames, expectedPropertyNames;

describe('PastebinService', () => {
  beforeEach(() => {
   TestBed.configureTestingModule({
      providers: [PastebinService],
      imports: [HttpModule]
    });
    
    //Get the injected service into our tests
    testService= TestBed.get(PastebinService);
    mockPaste = { id:999, title: "Hello world", language: Languages[2], paste: "console.log('Hello world');"};

  });
});

我们曾经 TestBed.get(PastebinService) 将真实的服务注入到我们的测试中。 

  it('#getPastebin should return an array with Pastebin objects',async() => {
     
    testService.getPastebin().then(value => {
      //Checking the property names of the returned object and the mockPaste object
      responsePropertyNames = Object.getOwnPropertyNames(value[0]);
      expectedPropertyNames = Object.getOwnPropertyNames(mockPaste);
     
      expect(responsePropertyNames).toEqual(expectedPropertyNames);
      
    });
  });

getPastebin 返回一组 Pastebin 对象。typescript的编译时类型检查不能用于验证返回的值是否确实是 Pastebin 对象的数组。因此,我们习惯于Object.getOwnPropertNames()确保两个对象具有相同的属性名称。

第二个测试如下:

  it('#addPaste should return async paste', async() => {
    testService.addPaste(mockPaste).then(value => {
      expect(value).toEqual(mockPaste);
    })
  })

两项测试都应该通过。这是剩下的测试。

 it('#updatePaste should update', async() => {
    //Updating the title of Paste with id 1
    mockPaste.id = 1;
    mockPaste.title = "New title"
    testService.updatePaste(mockPaste).then(value => {
      expect(value).toEqual(mockPaste);
    })
  })

  it('#deletePaste should return null', async() => {
    testService.deletePaste(mockPaste).then(value => {
      expect(value).toEqual(null);
    })
  })

 使用和 方法的代码 修改pastebin.service.ts 。updatePaste()deletePaste()

//update a paste
public updatePaste(pastebin: Pastebin):Promise<any> {
	const url = `${this.pastebinUrl}/${pastebin.id}`;
	return this.http.put(url, JSON.stringify(pastebin), {headers: this.headers})
		.toPromise()
		.then(() => pastebin)
		.catch(this.handleError);
}
//delete a paste
public deletePaste(pastebin: Pastebin): Promise<void> {
	const url = `${this.pastebinUrl}/${pastebin.id}`;
	return this.http.delete(url, {headers: this.headers})
		.toPromise()
		.then(() => null )
		.catch(this.handleError);
}

返回组件

AddPaste 组件的其余要求如下:

  • 按下Save按钮应该调用 Pastebin 服务的addPaste()方法。

  • 如果addPaste操作成功,组件应该发出一个事件来通知父组件。

  • 单击关闭按钮应从dom中删除 id 'source-modal' 并将属性更新showModal为 false。

由于上述测试用例与模态窗口有关,因此使用嵌套的描述块可能是个好主意。

describe('AddPasteComponent', () => {
  .
  .
  .
  describe("AddPaste Modal", () => {
  
    let inputTitle: HTMLInputElement;
    let selectLanguage: HTMLSelectElement;
    let textAreaPaste: HTMLTextAreaElement;
    let mockPaste: Pastebin;
    let spyOnAdd: jasmine.Spy;
    let pastebinService: PastebinService;
    
    beforeEach(() => {
      
      component.showModal = true;
      fixture.detectChanges();

      mockPaste = { id:1, title: "Hello world", language: Languages[2], paste: "console.log('Hello world');"};
      //Create a jasmine spy to spy on the addPaste method
      spyOnAdd = spyOn(pastebinService,"addPaste").and.returnValue(Promise.resolve(mockPaste));
      
    });
  
  });
});

在 describe 块的根处声明所有变量是一种很好的做法,原因有两个。这些变量可以在声明它们的描述块内访问,这使测试更具可读性。 

  it("should accept input values", () => {
      //Query the input selectors
      inputTitle = element.queryselector("input");
      selectLanguage = element.querySelector("select");
      textAreaPaste = element.querySelector("textarea");
      
      //Set their value
      inputTitle.value = mockPaste.title;
      selectLanguage.value = mockPaste.language;
      textAreaPaste.value = mockPaste.paste;
      
      //Dispatch an event
      inputTitle.dispatchEvent(new Event("input"));
      selectLanguage.dispatchEvent(new Event("change"));
      textAreaPaste.dispatchEvent(new Event("input"));

      expect(mockPaste.title).toEqual(component.newPaste.title);
      expect(mockPaste.language).toEqual(component.newPaste.language);
      expect(mockPaste.paste).toEqual(component.newPaste.paste);
    });

上面的测试使用querySelector()方法来分配inputTitleSelectLanguage以及textAreaPaste它们各自的 HTML 元素(<input><select><textArea>)。接下来,将这些元素的值替换为dmockPaste的属性值。这相当于用户通过浏览器填写表单。 

element.dispatchEvent(new Event("input"))触发一个新的输入事件,让模板知道输入字段的值已经改变。测试期望输入值应该传播到组件的newPaste属性中。

声明newPaste属性如下:

    newPaste : PasteBin = new PasteBin ( ) ;

并使用以下代码更新模板:

<!--- add-paste.component.html -->
<div class="add-paste">
  <button type="button" (click)="createPaste()"> create Paste </button>
  <div *ngIf="showModal"  id="source-modal" class="modal fade in">
    <div class="modal-dialog" >
      <div class="modal-content">
        <div class="modal-header">
           <h4 class="modal-title"> 
        	 <input  placeholder="Enter the Title" name="title" [(ngModel)] = "newPaste.title" />
          </h4>
        </div>
        <div class="modal-body">
      	 <h5> 
      		<select name="category"  [(ngModel)]="newPaste.language" >
      			<option  *ngFor ="let language of languages" value={{language}}> {{language}} </option>
        	</select>
         </h5>     	
      	 <textarea name="paste" placeholder="Enter the code here" [(ngModel)] = "newPaste.paste"> </textarea>
      	</div>
      <div class="modal-footer">
        <button type="button" (click)="onClose()">Close</button>
        <button type="button" (click) = "onSave()">Save</button>
      </div>
     </div>
    </div>
  </div>
</div>

额外的 div 和类用于 Bootstrap 的模式窗口。 [(ngModel)]是一个实现双向数据绑定的 Angular 指令。(click) = "onClose()"并且(click) = "onSave()"是用于将点击事件绑定到组件中的方法的事件绑定技术的示例。您可以在 Angular 的官方模板语法指南中阅读有关不同数据绑定技术的更多信息 。 

如果您遇到模板解析错误, 那是因为您尚未将其FormsModule导入 AppComponent。 

让我们在测试中添加更多规范。

 it("should submit the values", async() => {   
   component.newPaste = mockPaste;
   component.onSave();
    fixture.detectChanges();
    fixture.whenStable().then( () => {
        fixture.detectChanges();
        expect(spyOnAdd.calls.any()).toBeTruthy();
    });

 });
 
 it("should have a onClose method", () => {
    component.onClose();
    fixture.detectChanges();
    expect(component.showModal).toBeFalsy();
  })

component.onSave()类似于调用triggerEventHandler()Save 按钮元素。由于我们已经为按钮添加了 UI,因此调用component.save()听起来更有意义。expect 语句检查是否对间谍进行了任何调用。这是 AddPaste 组件的最终版本。

import { Component, OnInit, Input, Output, EventEmitter } from '@angular/core';
import { Pastebin, Languages } from '../pastebin';
import { PastebinService } from '../pastebin.service';

@Component({
  selector: 'app-add-paste',
  templateUrl: './add-paste.component.html',
  styleUrls: ['./add-paste.component.css']
})
export class AddPasteComponent implements OnInit {

  @Output() addPasteSuccess: EventEmitter<Pastebin> = new EventEmitter<Pastebin>();
  showModal: boolean = false;
  newPaste: Pastebin = new Pastebin();
  languages: string[] = Languages;

  constructor(private pasteServ: PastebinService) { }

  ngOnInit() {  }
  //createPaste() gets invoked from the template. This shows the Modal
  public createPaste():void {
    this.showModal = true;
    
  }
  //onSave() pushes the newPaste property into the server
  public onSave():void {
    this.pasteServ.addPaste(this.newPaste).then( () => {
      console.log(this.newPaste);
        this.addPasteSuccess.emit(this.newPaste);
        this.onClose();
    });
  }
  //Used to close the Modal
  public onClose():void {
    this.showModal=false;
  }
}

如果onSave操作成功,组件应该发出一个事件通知父组件(Pastebin 组件)更新其视图。addPasteSuccess,这是一个用@Output装饰器装饰的事件属性,用于此目的。 

测试发出输出事件的组件很容易。 

 describe("AddPaste Modal", () => {
   
    beforeEach(() => {
    .
    .
   //Subscribe to the event emitter first
   //If the emitter emits something, responsePaste will be set
   component.addPasteSuccess.subscribe((response: Pastebin) => {responsePaste = response},)
      
    });
    
    it("should accept input values", async(() => {
    .
    .
      component.onSave();
      fixture.detectChanges();
      fixture.whenStable().then( () => {
        fixture.detectChanges();
        expect(spyOnAdd.calls.any()).toBeTruthy();
        expect(responsePaste.title).toEqual(mockPaste.title);
      });
    }));
  
  });

测试订阅addPasteSuccess属性就像父组件一样。对最后的期望证实了这一点。我们在 AddPaste 组件上的工作已经完成。 

在pastebin.component.html中取消注释这一行 

<app-add-paste (addPasteSuccess)= 'onAddPaste($event)'> </app-add-paste>

并使用以下代码更新 pastebin.component.ts 。

 //This will be invoked when the child emits addPasteSuccess event
 public onAddPaste(newPaste: Pastebin) {
    this.pastebin.push(newPaste);
  }

如果遇到错误,那是因为您没有AddPaste在 Pastebin 组件的规范文件中声明该组件。如果我们可以在一个地方声明测试所需的所有内容并将其导入到我们的测试中,那不是很好吗?为了实现这一点,我们可以将其导入AppModule到我们的测试中,或者为我们的测试创建一个新模块。创建一个新文件并将其命名为app-testing- module.ts

import { BrowserModule } from '@angular/platform-browser';
import { NgModule } from '@angular/core';

//Components
import { AppComponent } from './app.component';
import { PastebinComponent } from './pastebin/pastebin.component';
import { AddPasteComponent } from './add-paste/add-paste.component';
//Service for Pastebin

import { PastebinService } from "./pastebin.service";

//Modules used in this tutorial
import { HttpModule }    from '@angular/http';
import { FormsModule } from '@angular/forms';

//In memory Web api to simulate an http server
import { InMemoryWebApiModule } from 'angular-in-memory-web-api';
import { InMemoryDataService }  from './in-memory-data.service';

@NgModule({
  declarations: [
    AppComponent,
    PastebinComponent,
    AddPasteComponent,
  ],
  
  imports: [
    BrowserModule, 
    HttpModule,
    FormsModule,
    InMemoryWebApiModule.forRoot(InMemoryDataService),
  ],
  providers: [PastebinService],
  bootstrap: [AppComponent]
})
export class AppTestingModule { }

现在您可以替换:

 beforeEach(async(() => {
    TestBed.configureTestingModule({
      declarations: [ AddPasteComponent ],
      imports: [ HttpModule, FormsModule ],
      providers: [ PastebinService ],
    })
    .compileComponents();
}));

和:

beforeEach(async(() => {
    TestBed.configureTestingModule({
      imports: [AppTestingModule]
    })
    .compileComponents();
  }));

定义 providers 并 declarations  消失的元数据,取而代之的是 AppTestingModule 被导入。那很整齐!  TestBed.configureTestingModule() 看起来比以前更时尚。 

查看、编辑和删除粘贴

ViewPaste 组件处理查看、编辑和删除粘贴的逻辑。这个组件的设计类似于我们对 AddPaste 组件所做的。 

使用Jasmine在Angular中测试组件第2部分:服务  第2张编辑使用Jasmine在Angular中测试组件第2部分:服务  第3张查看模式

ViewPaste 组件的目标如下:

  • 组件的模板应该有一个名为View Paste的按钮。

  • 单击“查看粘贴”按钮应显示一个带有 id 'source-modal' 的模式窗口。 

  • 粘贴数据应从父组件传播到子组件,并应显示在模态窗口内。

  • 按下编辑按钮应设置component.editEnabled为 true(editEnabled用于在编辑模式和查看模式之间切换)

  • 单击Save按钮应该调用 Pastebin 服务的updatePaste()方法。

  • 单击Delete按钮应调用 Pastebin 服务的deletePaste()方法。

  • 成功的更新和删除操作应该发出一个事件来通知父组件子组件的任何更改。 

让我们开始吧!前两个规范与我们之前为 AddPaste 组件编写的测试相同。 

 it('should show a button with text View Paste', ()=> {
    expect(element.textContent).toContain("View Paste");
  });

  it('should not display the modal until the button is clicked', () => {
      expect(element.textContent).not.toContain("source-modal");
  });

与我们之前所做的类似,我们将创建一个新的描述块并将其余的规范放在其中。以这种方式嵌套 describe 块使 spec 文件更具可读性,并且 describe 函数的存在更有意义。  

嵌套的 describe 块将有一个beforeEach()函数,我们将在其中初始化两个间谍,一个用于updatePaste() 方法,另一个用于该deletePaste()方法。不要忘记创建一个mockPaste 对象,因为我们的测试依赖它。 

beforeEach(()=> {
      //Set showPasteModal to true to ensure that the modal is visible in further tests
      component.showPasteModal = true;
      mockPaste = {id:1, title:"New paste", language:Languages[2], paste: "console.log()"};
      
      //Inject PastebinService
      pastebinService = fixture.debugElement.injector.get(PastebinService);
      
      //Create spies for deletePaste and updatePaste methods
      spyOnDelete = spyOn(pastebinService,'deletePaste').and.returnValue(Promise.resolve(true));
      spyOnUpdate = spyOn(pastebinService, 'updatePaste').and.returnValue(Promise.resolve(mockPaste));
     
      //component.paste is an input property 
      component.paste = mockPaste;
      fixture.detectChanges();
     
    })

这是测试。

 it('should display the modal when the view Paste button is clicked',() => {
    
    fixture.detectChanges();
    expect(component.showPasteModal).toBeTruthy("Show should be true");
    expect(element.innerHTML).toContain("source-modal");
})

it('should display title, language and paste', () => {
    expect(element.textContent).toContain(mockPaste.title, "it should contain title");
    expect(element.textContent).toContain(mockPaste.language, "it should contain the language");
    expect(element.textContent).toContain(mockPaste.paste, "it should contain the paste");
});

该测试假定组件具有paste接受来自父组件的输入的属性。早些时候,我们看到了一个示例,说明如何可以测试从子组件发出的事件,而无需将宿主组件的逻辑包含到我们的测试中。类似地,为了测试输入属性,通过将属性设置为模拟对象并期望模拟对象的值显示在 HTML 代码中更容易做到这一点。

模态窗口将有很多按钮,编写规范以保证模板中的按钮可用并不是一个坏主意。 

it('should have all the buttons',() => {
      expect(element.innerHTML).toContain('Edit Paste');
      expect(element.innerHTML).toContain('Delete');
      expect(element.innerHTML).toContain('Close');
});

在进行更复杂的测试之前,让我们修复失败的测试。

<!--- view-paste.component.html -->
<div class="view-paste">
    <button class="text-primary button-text"  (click)="showPaste()"> View Paste </button>
  <div *ngIf="showPasteModal" id="source-modal" class="modal fade in">
    <div class="modal-dialog">
      <div class="modal-content">
        <div class="modal-header">
          <button type="button" class="close" (click)='onClose()' aria-hidden="true">&times;</button>
          <h4 class="modal-title">{{paste.title}} </h4>
        </div>
        <div class="modal-body">
      	  <h5> {{paste.language}} </h5>     	
      	  <pre><code>{{paste.paste}}</code></pre>
        </div>
        <div class="modal-footer">
          <button type="button" class="btn btn-default" (click)="onClose()" data-dismiss="modal">Close</button>
          <button type="button" *ngIf="!editEnabled" (click) = "onEdit()" class="btn btn-primary">Edit Paste</button>
           <button type = "button"  (click) = "onDelete()" class="btn btn-danger"> Delete Paste </button>
        </div>
      </div>
    </div>
  </div>
</div>
/* view-paste.component.ts */

export class ViewPasteComponent implements OnInit {

  @Input() paste: Pastebin;
  @Output() updatePasteSuccess: EventEmitter<Pastebin> = new EventEmitter<Pastebin>();
  @Output() deletePasteSuccess: EventEmitter<Pastebin> = new EventEmitter<Pastebin>();

  showPasteModal:boolean ;
  readonly languages = Languages;
  
  constructor(private pasteServ: PastebinService) { }

  ngOnInit() {
      this.showPasteModal = false;
  }
  //To make the modal window visible
  public showPaste() {
  	this.showPasteModal = true;
  }
  //Invoked when edit button is clicked
  public onEdit() { }
  
  //invoked when save button is clicked
  public onSave() { }
  
  //invoked when close button is clicked
  public onClose() {
  	this.showPasteModal = false;
  }
  
  //invoked when Delete button is clicked
  public onDelete() { }
  
}

能够查看粘贴是不够的。该组件还负责编辑、更新和删除粘贴。该组件应该有一个属性,当用户单击“编辑粘贴”按钮 editEnabled时,该属性将设置为 true 。

it('and clicking it should make the paste editable', () => {

    component.onEdit();
    fixture.detectChanges();
    expect(component.editEnabled).toBeTruthy();
    //Now it should have a save button
    expect(element.innerHTML).toContain('Save');
      
});

添加editEnabled=true;到 onEdit()方法以清除第一个期望语句。 

下面的模板使用ngIf指令在查看模式和编辑模式之间切换。<ng-container>是一个逻辑容器,用于对多个元素或节点进行分组。

   <div *ngIf="showPasteModal" id="source-modal" class="modal fade in" >

    <div class="modal-dialog">
      <div class="modal-content">
        <!---View mode -->
        <ng-container *ngIf="!editEnabled">
        
          <div class="modal-header">
            <button type="button" class="close" (click)='onClose()' data-dismiss="modal" aria-hidden="true">&times;</button>
            <h4 class="modal-title"> {{paste.title}} </h4>
          </div>
          <div class="modal-body">
              <h5> {{paste.language}} </h5>
      		  <pre><code>{{paste.paste}}</code>
            </pre>
      	
      	  </div>
          <div class="modal-footer">
            <button type="button" class="btn btn-default" (click)="onClose()" data-dismiss="modal">Close</button>
            <button type="button" (click) = "onEdit()" class="btn btn-primary">Edit Paste</button>
            <button type = "button"  (click) = "onDelete()" class="btn btn-danger"> Delete Paste </button>

          </div>
        </ng-container>
        <!---Edit enabled mode -->
        <ng-container *ngIf="editEnabled">
          <div class="modal-header">
             <button type="button" class="close" (click)='onClose()' data-dismiss="modal" aria-hidden="true">&times;</button>
             <h4 class="modal-title"> <input *ngIf="editEnabled" name="title" [(ngModel)] = "paste.title"> </h4>
          </div>
          <div class="modal-body">
            <h5>
                <select name="category"  [(ngModel)]="paste.language">
                  <option   *ngFor ="let language of languages" value={{language}}> {{language}} </option>
                </select>
            </h5>

           <textarea name="paste" [(ngModel)] = "paste.paste">{{paste.paste}} </textarea>
          </div>
          <div class="modal-footer">
             <button type="button" class="btn btn-default" (click)="onClose()" data-dismiss="modal">Close</button>
             <button type = "button" *ngIf="editEnabled" (click) = "onSave()" class="btn btn-primary"> Save Paste </button>
             <button type = "button"  (click) = "onDelete()" class="btn btn-danger"> Delete Paste </button>      
          </div>
        </ng-container>
      </div>
    </div>
  </div>

该组件应该有两个Output()事件发射器,一个用于updatePasteSuccess属性,另一个用于deletePasteSuccess下面的测试验证以下内容:

  1. 组件的模板接受输入。

  2. 模板输入绑定到组件的 paste属性。

  3. 如果更新操作成功,updatePasteSuccess则使用更新的粘贴发出事件。 

it('should take input values', fakeAsync(() => {
      component.editEnabled= true;
      component.updatePasteSuccess.subscribe((res:any) => {response = res},)
      fixture.detectChanges();

      inputTitle= element.querySelector("input");
      inputTitle.value = mockPaste.title;
      inputTitle.dispatchEvent(new Event("input"));
      
      expect(mockPaste.title).toEqual(component.paste.title);
    
      component.onSave();
       //first round of detectChanges()
      fixture.detectChanges();

      //the tick() operation. Don't forget to import tick
      tick();

      //Second round of detectChanges()
      fixture.detectChanges();
      expect(response.title).toEqual(mockPaste.title);
      expect(spyOnUpdate.calls.any()).toBe(true, 'updatePaste() method should be called');
      
}))

本次测试与之前的测试明显不同的是 fakeAsync 函数的使用。fakeAsync 与 async 相当,因为这两个函数都用于在异步测试区域中运行测试。但是,fakeAsync使您的外观测试看起来更加同步。 

tick()方法替换了fixture.whenStable().then(),并且从开发人员的角度来看,代码更具可读性。不要忘记导入fakeAsync 并从 @angular/core/testing.

最后,这是删除粘贴的规范。

it('should delete the paste', fakeAsync(()=> {
      
      component.deletePasteSuccess.subscribe((res:any) => {response = res},)
      component.onDelete();
      fixture.detectChanges();
      tick();
      fixture.detectChanges();
      expect(spyOnDelete.calls.any()).toBe(true, "Pastebin deletePaste() method should be called");
      expect(response).toBeTruthy();
}))

我们几乎完成了组件。这是组件的最终草案ViewPaste

/*view-paste.component.ts*/
export class ViewPasteComponent implements OnInit {

  @Input() paste: Pastebin;
  @Output() updatePasteSuccess: EventEmitter<Pastebin> = new EventEmitter<Pastebin>();
  @Output() deletePasteSuccess: EventEmitter<Pastebin> = new EventEmitter<Pastebin>();

  showPasteModal:boolean ;
  editEnabled: boolean;
  readonly languages = Languages;
  
  constructor(private pasteServ: PastebinService) { }

  ngOnInit() {
      this.showPasteModal = false;
  	  this.editEnabled = false;
  }
  //To make the modal window visible
  public showPaste() {
  	this.showPasteModal = true;
  }
  //Invoked when the edit button is clicked
  public onEdit() {
  	this.editEnabled=true;
  }
  //Invoked when the save button is clicked
  public onSave() {
 	this.pasteServ.updatePaste(this.paste).then( () => {
  		this.editEnabled= false;
        this.updatePasteSuccess.emit(this.paste);
  	})
  }
 //Invoked when the close button is clicked
  public onClose() {
  	this.showPasteModal = false;
  }
 
 //Invoked when the delete button is clicked
  public onDelete() {
	  this.pasteServ.deletePaste(this.paste).then( () => {
        this.deletePasteSuccess.emit(this.paste);
 	    this.onClose();
 	  })
  }
  
}

父组件 ( pastebin.component.ts ) 需要使用处理子组件发出的事件的方法进行更新。

/*pastebin.component.ts */
  public onUpdatePaste(newPaste: Pastebin) {
    this.pastebin.map((paste)=> { 
       if(paste.id==newPaste.id) {
         paste = newPaste;
       } 
    })
  }

  public onDeletePaste(p: Pastebin) {
   this.pastebin= this.pastebin.filter(paste => paste !== p);
   
  }

这是更新的pastebin.component.html

<tbody>
    	<tr *ngFor="let paste of pastebin">
			<td> {{paste.id}} </td>
			<td> {{paste.title}} </td>
			<td> {{paste.language}} </td>
			
			<td> <app-view-paste [paste] = paste (updatePasteSuccess)= 'onUpdatePaste($event)' 
			(deletePasteSuccess)= 'onDeletePaste($event)'> 
			</app-view-paste></td> 
		</tr>
	</tbody>
	<app-add-paste (addPasteSuccess)= 'onAddPaste($event)'> </app-add-paste>

设置路线

要创建路由应用程序,我们需要更多库存组件,以便我们可以创建通向这些组件的简单路由。我创建了一个 About 组件和一个 Contact 组件,以便我们可以将它们放入导航栏中。AppComponent将保存路由的逻辑。完成后,我们将编写路由测试。 

首先,将RouterModuleandRoutes导入AppModule(and AppTestingModule)。 

import { RouterModule, Routes } from '@angular/router';

接下来,定义您的路由并将路由定义传递给该 RouterModule.forRoot方法。

const appRoutes :Routes = [
  { path: '', component: PastebinComponent },
  { path: 'about', component: AboutComponent },
  { path: 'contact', component: ContactComponent},
  ];
 
 imports: [
    BrowserModule, 
    FormsModule,
    HttpModule,
    InMemoryWebApiModule.forRoot(InMemoryDataService),
    RouterModule.forRoot(appRoutes),
   
  ],

对 的任何更改AppModule也应该对AppTestingModule但是,如果您在执行测试时遇到 No base  href  set 错误,请将以下行添加到 AppTestingModule 的providers数组中。

{provide: APP_BASE_HREF, useValue: '/'}

现在将以下代码添加到app.component.html

<nav class="navbar navbar-inverse">
   <div class="container-fluid">
       <div class="navbar-header">
      	   <div class="navbar-brand" >{{title}}</div>
      </div>
   	  <ul class="nav navbar-nav bigger-text">
    	  <li>
	    	 <a routerLink="" routerLinkActive="active">Pastebin Home</a>
	      </li>
	      <li>
	     	 <a routerLink="/about" routerLinkActive="active">About Pastebin</a>
	      </li>
	      <li>
	     	 <a routerLink="/contact" routerLinkActive="active"> Contact </a>
	       </li>
	  </ul>
   </div>
</nav>
  <router-outlet></router-outlet>

routerLink是用于将 HTML 元素与路由绑定的指令。我们在此处将它与 HTML 锚标记一起使用。  RouterOutlet是另一个指令,它在模板中标记应该显示路由器视图的位置。 

测试路线有点棘手,因为它涉及更多的 UI 交互。这是检查锚链接是否正常工作的测试。

describe('AppComponent', () => {
  beforeEach(async(() => {
    TestBed.configureTestingModule({
      imports: [AppTestingModule],
      
    }).compileComponents();
  }));


  it(`should have as title 'Pastebin Application'`, async(() => {
    const fixture = TestBed.createComponent(AppComponent);
    const app = fixture.debugElement.componentInstance;
    expect(app.title).toEqual('Pastebin Application');
  }));


  it('should go to url',
    fakeAsync((inject([Router, Location], (router: Router, location: Location) => {
      let anchorLinks,a1,a2,a3;
    let fixture = TestBed.createComponent(AppComponent);
    fixture.detectChanges();
     //Create an array of anchor links
     anchorLinks= fixture.debugElement.queryAll(By.css('a'));
     a1 = anchorLinks[0];
     a2 = anchorLinks[1];
     a3 = anchorLinks[2];
     
     //Simulate click events on the anchor links
     a1.nativeElement.click();
     tick();
     
     expect(location.path()).toEqual("");

     a2.nativeElement.click();
     tick()
     expect(location.path()).toEqual("/about");

      a3.nativeElement.click();
      tick()
      expect(location.path()).toEqual("/contact");
    
  }))));
});

如果一切顺利,您应该会看到类似这样的内容。

使用Jasmine在Angular中测试组件第2部分:服务  第4张

最后的润色

为您的项目添加一个漂亮的 Bootstrap 设计,并为您的项目提供服务(如果您还没有这样做的话)。 

ng serve

概括

我们在测试驱动的环境中从头开始编写了一个完整的应用程序。那不是什么东西吗?在本教程中,我们学习了:

  • 如何使用测试优先方法设计组件

  • 如何为组件编写单元测试和基本 UI 测试

  • 关于 Angular 的测试工具以及如何将它们整合到我们的测试中

  • 关于使用async()fakeAsync()运行异步测试

  • Angular 中路由的基础知识和编写路由测试



文章目录
  • 概述
  • 添加粘贴(续)
  • 为服务编写测试
  • 返回组件
  • 查看、编辑和删除粘贴
  • 设置路线
  • 最后的润色
  • 概括