繁体   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