簡體   English   中英

如何在 angular 中模擬 rxjs/Websocket 以進行單元測試

[英]How to mock rxjs/Websocket in angular for Unit Testing

我創建了一個服務來處理 websocket 與服務器的通信,我試圖為它創建單元測試,但我找不到模擬rxjs/Websocket的方法。

我在這里發現了一個類似的問題,但是我不能將它與新版本的 rxjs 一起使用。 任何幫助都會非常有幫助。

我也可以將 WebSocket 作為服務注入並在我的測試中模擬該服務,但這似乎是一種解決方法,我更喜歡更好的解決方案

這是mi代碼:

socket.service.ts

//imports 

@Injectable()
export class SocketService {
  private baseUrl = ENV.WS_WORKSPACES_URL;

  socket$: WebSocketSubject<any>;

  constructor(
    private userService: UserService
  ) { }

  initSocket(): void {
    this.socket$ = webSocket(`${this.baseUrl}clusters`);
    const user = this.userService.getUser();

    if (user) {
      this.send({token: user.token});
    }
  }

  send(data: any): void {
    this.socket$.next(data);
  }
}

socket.service.spec.ts

//imports

describe("SocketService", () => {
  let service: SocketService;
  let userServiceSpy;
  let socket;
  const token = "whatever";

  beforeEach(() => {
    userServiceSpy = jasmine.createSpyObj("UserService", ["getUser"]);
    userServiceSpy.getUser.and.returnValue(null);
    socket = {next: jasmine.createSpy()};
    TestBed.configureTestingModule({
      providers: [
        {provide: UserService, useValue: userServiceSpy},
        SocketService
      ]
    });

    service = TestBed.inject(SocketService);

    socket = {} //this should be my mock
  });

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

  it("should open connection", () => {
    service.initSocket();

    expect(socket.next).not.toHaveBeenCalled();
    expect(service.socket$).toBeDefined();
  });

  it("should open connection with auth", () => {
    const user = {token};
    userServiceSpy.getUser.and.returnValue(user);

    service.initSocket();
    expect(socket.next).toHaveBeenCalledWith(user);
  });

  it("should send a message", () => {
    service.initSocket();

    const message = {};
    service.send(message);

    expect(socket.next).toHaveBeenCalledWith(message);
  });
});

這是一個特別難以解決的問題,但這里的答案實際上是讓你想要模擬的東西可以注入。

感覺像是一種解決方法,因為它似乎是不必要的工作。 大多數特定於 Angular 的庫會導出一個服務,該服務就像一個工廠來解耦它,這太棒了。 但是 rxjs不是Angular 庫,盡管框架非常依賴它。

TLDR; 您將不得不將webSocket包裹在可注射的東西中。

一般問題的簡短背景

由於模塊的工作方式, webSocket構造函數通常不可模擬。 根據您使用的模塊類型以及您是否可以控制導出模塊,您可以進行一些調整,但它們只是解決方法。 演變一直朝着導入只讀的方向發展,這逐漸打破了大多數現有的解決方法。 jasmine 的 github 上的這個問題涵蓋了解決方法以及(最終)通用解決方案難以捉摸的原因。

那我該怎么辦?

Jasmine在其常見問題解答中提供了一些官方指導

  1. 對要模擬的東西使用依賴注入,並從規范中注入間諜或模擬 object。 這種方法通常會導致規范和被測代碼的可維護性改進。 需要模擬模塊通常是緊密耦合代碼的標志,修復耦合而不是使用測試工具解決它可能是明智的。

總是很好的建議,這當然是一種依賴。 並且我們要模擬的功能與構造函數非常耦合:旁注,這里很難指責 rxjs 團隊。 令人沮喪。 這是一個需要特定於框架的解決方案的問題。

這里有兩個選擇,都不錯。

  1. 做一個服務。
  2. 做一個工廠function。

提供服務

這很容易,而且令人尷尬的是,這不是我嘗試的第一件事。 只需創建一個新服務並為其提供一個具有相同簽名的公共方法:

@Injectable()
export class WebSocketFactoryService {

  constructor(){}

  public makeSocket<T>(urlConfigOrSource: string | WebSocketSubjectConfig<T>): WebSocketSubject<T> {
    return webSocket<T>(urlConfigOrSource);
  }
}

注入構造函數

這個稍微麻煩一些,但您不必為工廠服務創建新文件。 帶上不止一種工具也很好。

這是一個測試套件的Stackblitz 鏈接,該應用程序的服務需要創建 websocket 和注入所述服務的組件(它也沒有“缺少核心 js”問題。) 這個確切場景的指南,雖然有點難找。

首先你制作一個InjectionToken ,因為這不是 class:

// I only imported as rxjsWebsocket because I wanted to use webSocket in my service
import { webSocket as rxjsWebsocket, WebSocketSubject } from 'rxjs/webSocket';

// Fun fact: You can use "typeof rxjsWebsocket" as the type to cleanly say "whatever that thing is"
export const WEBSOCKET_CTOR = new InjectionToken<typeof rxjsWebsocket>(
  'rxjs/webSocket.webSocket', // This is what you'll see in the error when it's missing
  {
    providedIn: 'root',
    factory: () => rxjsWebsocket, // This is how it will create the thing needed unless you offer your own provider, which we'll do in the spec
  }
);

然后你需要告訴你的服務像任何其他依賴一樣注入它:

@Injectable({
  providedIn: 'root',
})
export class SocketService {
  socket$: WebSocketSubject<any>;

  constructor(
    @Inject(WEBSOCKET_CTOR) private _webSocket: typeof rxjsWebsocket
  ) {
    this.socket$ = this._webSocket<any>('https://stackoverflow.com/');
  }

  messages(): Observable<any> {
    return this.socket$.asObservable();
  }

  send(data: any): void {
    this.socket$.next(data);
  }
}

問題解決了!

編寫測試

哦,等等,測試。 首先,您需要實際制作一個模擬。 有幾種方法,但我將其用於我的注入令牌版本:

// Mocking the websocket
let fakeSocket: Subject<any>; // Exposed so we can spy on it and simulate server messages
const fakeSocketCtor = jasmine
  .createSpy('WEBSOCKET_CTOR')
  .and.callFake(() => fakeSocket); // need to call fake so we can keep re-assigning to fakeSocket

如果您改為提供服務,您可能需要一個間諜 object

const fakeSocketFactory = jasmine.createSpyObj(WebSocketFactoryService, 'makeSocket');
fakeSocketFactory.makeSocket.and.callFake(() => fakeSocket);

你仍然想要一個可以不斷重置的暴露主題。

創建服務很簡單,只需使用構造函數!

  beforeEach(() => {
    // Make a new socket so we don't get lingering values leaking across tests
    fakeSocket = new Subject<any>();
    // Spy on it so we don't have to subscribe to verify it was called
    spyOn(fakeSocket, 'next').and.callThrough();

    // Reset your spies
    fakeSocketCtor.calls.reset();

    // Make the service using the ctor
    service = new SocketService(fakeSocketCtor);
    // or service = new SocketService(fakeSocketFactory); if you did that one
  });

現在你可以像幾個小時前想做的那樣自由地測試 Observables!

暫無
暫無

聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM