Angular2 之 單元測試

單元測試需要掌握的知識點

  • karma.conf.js的配置 具體了解到每一項的意義,這樣才能真正的了解這個配置是如何配置的,甚至才可以做到自己的配置。
  • 組件的測試
  • 單獨的service測試

Angular的測試工具

Angular的測試工具類包含了TestBed類和一些輔助函數(shù)方法,當時這不是唯一的,你可以不依賴Angular 的DI(依賴注入)系統(tǒng),自己new出來測試類的實例。

孤立的單元測試

describe('Service: base-data-remote', () => {
  let service = new BaseDataRemoteService();
  
  it('should be created',() => {
    expect(service).toBeTruthy();
  });
});

利用Angular測試工具進行測試知識點總結

測試工具包含了TestBed類和@angular/core/testing中的一些方法。

  • 在每個spec之前 ,TestBed將自己重設為初始狀態(tài)。

測試組件

import { Component }   from '@angular/core';

@Component({
  selector: 'app-banner',
  template: '<h1>{{title}}</h1>'
})
export class BannerComponent {
  title = 'Test Tour of Heroes';
}

let comp:    BannerComponent;
let fixture: ComponentFixture<BannerComponent>;
let de:      DebugElement;
let el:      HTMLElement;

describe('BannerComponent', () => {
  beforeEach(() => {
    TestBed.configureTestingModule({
      declarations: [ BannerComponent ], // declare the test component
    });

    fixture = TestBed.createComponent(BannerComponent);

    comp = fixture.componentInstance; // BannerComponent test instance

    // query for the title <h1> by CSS element selector
    de = fixture.debugElement.query(By.css('h1'));
    el = de.nativeElement;

  });
});
  • 組件測試
  • TestBed.createComponent創(chuàng)建BannerComponent組件的實例,可以用來測試和返回fixture。
  • TestBed.createComponent關閉當前TestBed實例,讓它不能再被配置。
  • query方法接受predicate函數(shù),并搜索fixture的整個DOM樹,試圖尋找第一個滿足predicate函數(shù)的元素。
  • queryAll方法返回一列數(shù)組,包含所有DebugElement中滿足predicate的元素。
  • By類是Angular測試工具之一,它生成有用的predicate。 它的By.css靜態(tài)方法產(chǎn)生標準CSS選擇器 predicate,與JQuery選擇器相同的方式過濾。
  • detectChanges:在測試中的Angular變化檢測。
    每個測試程序都通過調(diào)用fixture.detectChanges()
    來通知Angular執(zhí)行變化檢測。

測試有依賴的組件,這個依賴的測試

這個依賴的模擬方式有兩種:偽造服務實例(提供服務復制品)、刺探真實服務。這兩種方式都不錯,只需要挑選一種最適合你當前測試文件的測試方式來做最好。

偽造服務實例

被測試的組件不一定要注入真正的服務。實際上,服務的復制品(stubs, fakes, spies或者mocks)通常會更加合適。 spec的主要目的是測試組件,而不是服務。真實的服務可能自身有問題。

這個測試套件提供了最小化的UserServiceStub類,用來滿足組件和它的測試的需求。

userServiceStub = {
  isLoggedIn: true,
  user: { name: 'Test User'}
};

獲取注入的服務

測試程序需要訪問被注入到組件中的UserService(stub類)。

Angular的注入系統(tǒng)是層次化的。 可以有很多層注入器,從根TestBed創(chuàng)建的注入器下來貫穿整個組件樹。

最安全并總是有效的獲取注入服務的方法,是從被測試的組件的注入器獲取。 組件注入器是fixture的DebugElement的屬性。

出人意料的是,請不要引用測試代碼里提供給測試模塊的userServiceStub對象。它是行不通的! 被注入組件的userService實例是徹底不一樣的對象,是提供的userServiceStub
的克隆。

  • TestBed.get方法從根注入器中獲取服務。
    例如:
    dataService = testBed.get(DataService);

測試代碼

  beforeEach(() => {
    // stub UserService for test purposes
    userServiceStub = {
      isLoggedIn: true,
      user: { name: 'Test User'}
    };

    TestBed.configureTestingModule({
       declarations: [ WelcomeComponent ],
       // 重點
       providers:    [ {provide: UserService, useValue: userServiceStub } ] 
    });

    fixture = TestBed.createComponent(WelcomeComponent);
    comp    = fixture.componentInstance;

    // UserService from the root injector
    // 重點
    userService = TestBed.get(UserService);

    //  get the "welcome" element by CSS selector (e.g., by class name)
    de = fixture.debugElement.query(By.css('.welcome'));
    el = de.nativeElement;
  });

刺探(Spy)真實服務

注入了真是的服務,并使用Jasmine的spy替換關鍵的getXxxx方法。

spy = spyOn(remoteService, 'getTodos').and.returnValues([Promise.resolve(datas), Promise.resolve(datas2)]);

Spy的設計是,所有調(diào)用getTodos的方法都會受到立刻解析的承諾,得到一條預設的名言。

it方法中的幾個函數(shù)

寫單元測試時,it里經(jīng)常會有幾個常見的方法,async(),fakeAsync(),tick(),jasmine.done()方法等。
這幾個方法,都幫助我們簡化了異步測試程序的代碼。但是需要正確的使用這幾個方法。

組件

@Component({
  selector: 'twain-quote',
  template: '<p class="twain"><i>{{quote}}</i></p>'
})
export class TwainComponent  implements OnInit {
  intervalId: number;
  quote = '...';
  constructor(private twainService: TwainService) { }

  ngOnInit(): void {
    this.twainService.getQuote().then(quote => this.quote = quote);
  }
}
  • async
    • Angular TestBed的一部分。通過將測試代碼放到特殊的異步測試區(qū)域來運行,async函數(shù)簡化了異步測試程序的代碼。
  • 接受無參數(shù)的函數(shù)方法,返回無參數(shù)的函數(shù)方法,變成Jasmine的it函數(shù)的參數(shù)。
  • 它的參數(shù)看起來和普通的it參數(shù)主體一樣。 沒有任何地方顯示異步特征。 比如,它不返回承諾,并且沒有done方法可調(diào)用,因為它是標準的Jasmine異步測試程序。

使用例子:

it('should show quote after getQuote promise (async)', async(() => {
  fixture.detectChanges();

  fixture.whenStable().then(() => { // wait for async getQuote
    fixture.detectChanges();        // update view with quote
    expect(el.textContent).toBe(testQuote);
  });
}));
  • 簡單介紹一下whenStable()方法
  • 測試程序必須等待getQuote在JavaScript引擎的下一回合中被解析。
  • ComponentFixture.whenStable方法返回它自己的承諾,它getQuote
    承諾完成時被解析。實際上,“stable”的意思是當所有待處理異步行為完成時的狀態(tài),在“stable”后whenStable承諾被解析。
  • 然后測試程序繼續(xù)運行,并開始另一輪的變化檢測(fixture.detectChanges
    ),通知Angular使用名言來更新DOM。 getQuote
    輔助方法提取出顯示元素文本,然后expect語句確認這個文本與預備的名言相符。
  • fakeAsync
  • fakeAsync是另一種Angular測試工具。
  • async一樣,它也接受無參數(shù)函數(shù)并返回一個函數(shù),變成Jasmine的it
    函數(shù)的參數(shù)。
  • fakeAsync函數(shù)通過在特殊的fakeAsync測試區(qū)域運行測試程序,讓測試代碼更加簡單直觀。
  • 對于async來說,fakeAsync最重要的好處是測試程序看起來像同步的。里面沒有任何承諾。 沒有then(...)鏈來打斷控制流。
  • tick
    tick函數(shù)是Angular測試工具之一,是fakeAsync的同伴。 它只能在fakeAsync的主體中被調(diào)用。
  • 調(diào)用tick()模擬時間的推移,直到全部待處理的異步任務都已完成,在這個測試案例中,包含getQuote承諾的解析。

使用例子

it('should show quote after getQuote promise (fakeAsync)', fakeAsync(() => {
  fixture.detectChanges();
  tick();                  // wait for async getQuote
  fixture.detectChanges(); // update view with quote
  expect(el.textContent).toBe(testQuote);
}));
  • jasmine.done
    雖然async和fakeAsync函數(shù)大大簡化了異步測試,但是你仍然可以使用傳統(tǒng)的Jasmine異步測試技術。
    你仍然可以將接受 done回調(diào)的函數(shù)傳給it。 但是,你必須鏈接承諾、處理錯誤,并在適當?shù)臅r候調(diào)用done。
    使用例子
it('should show quote after getQuote promise (done)', done => {
  fixture.detectChanges();

  // get the spy promise and wait for it to resolve
  spy.calls.mostRecent().returnValue.then(() => {
    fixture.detectChanges(); // update view with quote
    expect(el.textContent).toBe(testQuote);
    done();
  });
});

以上這三個測試例子是等價的,也就是說,你可以隨你喜好選擇你喜歡的測試方式來進行單元測試的編寫。

測試有外部模板的組件

使用例子

// async beforeEach
beforeEach( async(() => {
  TestBed.configureTestingModule({
    declarations: [ DashboardHeroComponent ],
  })
  .compileComponents(); // compile template and css
}));

beforeEach里的async函數(shù)

注意beforeEach里面對async的調(diào)用,因為異步方法TestBed.compileComponents而變得必要。

compileComponents

  • 在本例中,TestBed.compileComponents編譯了組件,那就是DashbaordComponent。 它是這個測試模塊唯一的聲明組件。
  • 本章后面的測試程序有更多聲明組件,它們中間的一些導入應用模塊,這些模塊有更多的聲明組件。 一部分或者全部組件可能有外部模板和CSS文件。 TestBed.compileComponents一次性異步編譯所有組件。
  • compileComponents方法返回承諾,可以用來在它完成時候,執(zhí)行更多額外任務。

測試帶有inputs和outputs的組件

測試前期代碼

  // async beforeEach
  beforeEach( async(() => {
    TestBed.configureTestingModule({
      declarations: [ DashboardHeroComponent ],
    })
    .compileComponents(); // compile template and css
  }));

  // synchronous beforeEach
  beforeEach(() => {
    fixture = TestBed.createComponent(DashboardHeroComponent);
    comp    = fixture.componentInstance;
    heroEl  = fixture.debugElement.query(By.css('.hero')); // find hero element

    // pretend that it was wired to something that supplied a hero
    expectedHero = new Hero(42, 'Test Name');
    comp.hero = expectedHero;
    fixture.detectChanges(); // trigger initial data binding
  });

屬性

測試代碼是將模擬英雄(expectedHero)賦值給組件的hero屬性的。

// pretend that it was wired to something that supplied a hero 
expectedHero = new Hero(42, 'Test Name'); 
comp.hero = expectedHero;

點擊事件

  it('should raise selected event when clicked', () => {
    let selectedHero: Hero;
    comp.selected.subscribe((hero: Hero) => selectedHero = hero);

    heroEl.triggerEventHandler('click', null);
    expect(selectedHero).toBe(expectedHero);
  });

這個組件公開EventEmitter屬性。測試程序像宿主組件那樣來描述它。

heroEl是個DebugElement,它代表了英雄所在的<div>。 測試程序用"click"事件名字來調(diào)用triggerEventHandler。 調(diào)用DashboardHeroComponent.click()時,"click"事件綁定作出響應。

如果組件想期待的那樣工作,click()通知組件的selected屬性發(fā)出hero對象,測試程序通過訂閱selected事件而檢測到這個值,所以測試應該成功。

triggerEventHandler

Angular的DebugElement.triggerEventHandler可以用事件的名字觸發(fā)任何數(shù)據(jù)綁定事件。 第二個參數(shù)是傳遞給事件處理器的事件對象。


自己遇到的坑兒

下面都是自己在實際的編寫單元測試時,真實遇到的問題,自己真的是在這上面花費了很多時間?。。?!為什么沒有說花冤枉時間呢?就是因為是自己對單元測試還沒喲掌握,所以出了錯,不要緊,重要的是以后不能再犯!

service的注入

剛剛接觸angular2吧,對很多service的寫法不是很了解,以至于真的是白白浪費了很多時間,尤其是在這個service的模擬上??赡苈斆魅缒?,不會犯我這樣簡單卻又致命的錯誤吧,只希望,以后的賀賀也可以不再犯這樣的錯!??自己一把... ...

首先來看一下,我創(chuàng)建的這個service的用法。

@Injectable()
export class BaseDataService {
    remoteService: BaseDataRemoteService;
    private datasMap = {}; // 用于存儲所有的數(shù)據(jù)
    private todosCache = {};   // 待辦數(shù)據(jù)的id的臨時存儲
    private draftsCache = {};  // 草稿數(shù)據(jù)臨時存儲
    private relatedCache = {}; // 已辦理數(shù)據(jù)臨時存儲

    constructor(private config: any, private http: Http) {
        this.config = config;
        this.config.baseUrl = config.baseUrl || config.name;
        this.remoteService = new BaseDataRemoteService(this.config.baseUrl, this.config.idPropertyName, this.http);
    }

    getTodos(userId: String, pageNo?: number): any {
        pageNo = pageNo || 0;
        return this.remoteService.getTodos(userId, pageNo).then(datas => {
            let todos = datas.content;
            todos.forEach(element => {
                this.datasMap[element[this.config.idPropertyName]] = element;
                if (pageNo === 0) {
                    this.todosCache = {};
                }
            });

            this.todosCache[pageNo] = this.getRecordIds(todos);
            return todos;
        }, () => {
            return [];
        });

    }
}

其中的BaseDataRemoteService我是自己new出來的,而且這個BaseDataService也是我自己new出來的,所以首先第一點,我應該自己創(chuàng)建,而不能使用angular的DI系統(tǒng)來幫助我創(chuàng)建。

第二點就是在模擬的時候,我竟然傻傻的自己去在spec文件中自己去new了BaseDataRemoteService,所以我根本沒有辦法去執(zhí)行spyOn(foo, "getBar")這樣的模擬,然后就是一直的出錯。錯樣百出了!

正確的單元測試:

function makeEnvironment() {
  return TestBed.configureTestingModule({
    providers: [
      MockBackend,
      BaseRequestOptions,
      {
        provide: Http,
        useFactory: (backend, options) => {
          return new Http(backend, options);
        }, deps: [MockBackend, BaseRequestOptions],
      },
    ],
    imports: [HttpModule],
  });
}

const userId: String = '123';
let pageNo: number;

describe('Service: base-data', () => {

  const config = {
    name: 'archives/out',
    baseUrl: '/archives/out',
    idPropertyName: 'outId',
    subflagPropertyName: 'subflag',
  };

  const datas = [];

  const datas2 = [];

  let service: BaseDataService;
  let spy: jasmine.Spy;
  let http: Http; // 還應該是DI系統(tǒng)的

  beforeEach(() => {
    const testBed = makeEnvironment();
    http = testBed.get(Http);

    service = new BaseDataService(config, http); //這是自己new出來的
// 但是自己不能new出來BaseDataRemoteService
  });

  it('should be created', () => {
    expect(service).toBeTruthy();
  });

    it('獲取到的數(shù)據(jù)為空', async(() => {
      // 這樣的使用才是正確的!?。?      spy = spyOn(service.remoteService, 'getTodos').and.returnValue(Promise.resolve({content: []}));
      service.getTodos(userId).then(todos => {
        expect(todos.length).toBe(0);
        expect(todos).toEqual([]);
        expect(service.getTodosCache(0).length).toBe(0);
      });
    }));

下次一定要注意,不要瞎寫?。?!


多次調(diào)用同一個異步方法

相信大家對這段單元測試的代碼很熟悉,這里就是模擬多次調(diào)用同一個方法時,返回不同的值。

這里是同步方法的模擬返回數(shù)據(jù),那么異步方法同樣可以。

describe("A spy, when configured to fake a series of return values", function() {
  var foo, bar;

  beforeEach(function() {
    foo = {
      setBar: function(value) {
        bar = value;
      },
      getBar: function() {
        return bar;
      }
    };

    // 多次調(diào)用時,返回不同的值!
    spyOn(foo, "getBar").and.returnValues("fetched first", "fetched second"); 

    foo.setBar(123);
  });

  it("when called multiple times returns the requested values in order", function() {
    expect(foo.getBar()).toEqual("fetched first");
    expect(foo.getBar()).toEqual("fetched second");
    expect(foo.getBar()).toBeUndefined();
  });
});

下面是出錯的代碼。

    it('when the baseUrl is exist and pageNo is exist', async(() => {
      // 模擬多次進行異步調(diào)用時的返回值
      spyOn(service.remoteService, 'getRelatedList').and
      .returnValues(Promise.resolve({content: datas}), Promise.resolve({content: datas2}));
      pageNo = 0;
      // 第一次調(diào)用
      service.getRelatedList(userId, pageNo).then(relate => {
        expect(relate.length).toBe(2);
        expect(relate).toEqual(datas);
        expect(service.getRelatedCache(0).length).toBe(2, 'the length should be 2.');
      });
      // 第二次調(diào)用
      service.getRelatedList(userId).then(relate => {
        expect(relate.length).toBe(3);
        expect(relate).toEqual(datas2);
        expect(relate.length).not.toBe(2);
        expect(relate).not.toEqual(datas);
        expect(service.getRelatedCache(0).length).toBe(3);
      });
    }));

下面是單元測試的結果:

雖然第一個、第二個expect通過了,但是第三個無論如何也通不過。其實不是代碼寫的有問題,是單元測試寫的有有問題,在第一個expect去判斷的時候,第二個 service.getRelatedList已經(jīng)執(zhí)行完了,所以才會出錯。

這個錯誤,我意識到了,所以我再第二次調(diào)用的地方添加了一個延時執(zhí)行的函數(shù),這樣單元測試是完全正確的,但是這并不是一個好的解決辦法。

      setTimeout(function() {
        
      }, 200);

最好的解決辦法是,是使用fakeAsynctick來解決。

tick函數(shù)是Angular測試工具之一,是fakeAsync的同伴。 它只能在fakeAsync的主體中被調(diào)用。

調(diào)用tick()模擬時間的推移,直到全部待處理的異步任務都已完成。

下面是正確的代碼:

    it('when the baseUrl is exist and pageNo is exist', fakeAsync(() => {
      spyOn(service.remoteService, 'getRelatedList').and
      .returnValues(Promise.resolve({content: datas}), Promise.resolve({content: datas2}));
      pageNo = 0;

      service.getRelatedList(userId, pageNo).then(relate => {
        expect(relate.length).toBe(2);
        expect(relate).toEqual(datas);
        expect(service.getRelatedCache(0).length).toBe(2, 'the length should be 2.');
      });

      tick(); // 基本的意思就是,前后分開來執(zhí)行

      service.getRelatedList(userId).then(relate => {
        expect(relate.length).toBe(3);
        expect(relate).toEqual(datas2);
        expect(relate.length).not.toBe(2);
        expect(relate).not.toEqual(datas);
        expect(service.getRelatedCache(0).length).toBe(3);
      });
    }));

其實這之前我是把tick()方法都看過一遍的,可是還是不理解其中的意思,所以記錄下來吧,所謂書讀百遍變其義自見,加油啊!??????


未完待續(xù)...

最后編輯于
?著作權歸作者所有,轉載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。

相關閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容