如何在使用Jasmine进行Angular单元测试时模拟window.screen.width?

13

我有一个 BreakpointService,根据屏幕宽度,告诉我应该以哪种 SidebarMode(关闭 - 最小化 - 打开)显示我的侧边栏。

这是服务的主要部分:

constructor(private breakpointObserver: BreakpointObserver) {
    this.closed$ = this.breakpointObserver.observe(['(min-width: 1024px)']).pipe(
      filter((state: BreakpointState) => !state.matches),
      mapTo(SidebarMode.Closed)
    );

    this.opened$ = this.breakpointObserver.observe(['(min-width: 1366px)']).pipe(
      filter((state: BreakpointState) => state.matches),
      mapTo(SidebarMode.Open)
    );

    const minifiedStart$: Observable<boolean> = this.breakpointObserver.observe(['(min-width: 1024px)']).pipe(map(state => state.matches));

    const minifiedEnd$: Observable<boolean> = this.breakpointObserver.observe(['(max-width: 1366px)']).pipe(map(state => state.matches));

    this.minified$ = minifiedStart$.pipe(
      flatMap(start => minifiedEnd$.pipe(map(end => start && end))),
      distinctUntilChanged(),
      filter(val => val === true),
      mapTo(SidebarMode.Minified)
    );

    this.observer$ = merge(this.closed$, this.minified$, this.opened$);
  }

通过这行代码,我可以订阅事件:

this.breakpointService.observe().subscribe();

现在,我想测试单元测试中的不同模式,但我不知道如何在测试中模拟window.screen.width属性。

我尝试了几种方法,但都没有成功。

这是目前我的测试设置:

describe('observe()', () => {
    function resize(width: number): void {
      // did not work
      // window.resizeTo(width, window.innerHeight);
      // (<any>window).screen = { width: 700 };
      // spyOn(window, 'screen').and.returnValue(...)
    }

    let currentMode;
    beforeAll(() => {
      service.observe().subscribe(mode => (currentMode = mode));
    });

    it('should return Observable<SidebarMode>', async () => {
      resize(1000);

      expect(Object.values(SidebarMode).includes(SidebarMode[currentMode])).toBeTruthy();
    });

    xit('should return SidebarMode.Closed', async () => {
      resize(600);

      expect(currentMode).toBe(SidebarMode.Closed);
    });

    xit('should return SidebarMode.Minified', async () => {
      resize(1200);

      expect(currentMode).toBe(SidebarMode.Minified);
    });

    xit('should return SidebarMode.Open', async () => {
      resize(2000);

      expect(currentMode).toBe(SidebarMode.Open);
    });
  });
3个回答

18

我猜测BreakPointObserver监听了resize事件,因此您可以尝试使用jasmine模拟window.innerWidth / window.outerWidth?

spyOnProperty(window,'innerWidth').and.returnValue(760);

然后手动触发一次resize事件:

window.dispatchEvent(new Event('resize'));

它会像这样:

    it('should mock window inner width', () => {
        spyOnProperty(window, 'innerWidth').and.returnValue(760);
        window.dispatchEvent(new Event('resize'));
    });

10

模拟Angular Material的BreakpointObserver

我猜想你并不真正想要模拟window.screen,而是想要模拟BreakpointObserver。毕竟,没必要测试他们的代码,你只需要测试你的代码如何正确响应由BreakpointObserver.observe()返回的可观察对象与不同屏幕大小的区别。

有很多不同的方法可以做到这一点。为了阐明一种方法,我在STACKBLITZ上编写了一个示例代码,展示了我的方法。需要注意的几点与你提供的代码有所不同:

  • 你的代码在构造函数中设置了可观察对象。因此,在服务实例化之前必须更改模拟对象,因此你会看到在service = TestBed.get(MyService);调用之前进行了调用resize()
  • 我使用spyObj来模拟BreakpointObserver,并在BreakpointObserver.observe()方法的位置调用了一个虚假函数。这个虚假函数使用我设置的筛选器,从各种匹配中获得所需的结果。它们都以false开头,因为值将根据需要模拟的屏幕大小而改变,而这是由你在上面提供的resize()函数设置的。

注意:肯定有其他方法来解决这个问题。查看angular material在github上的breakpoints-observer.spec.ts 文件。这是比我在这里概述的更好的一般方法,我的方法只是为了测试你提供的函数。

以下是新建议的describe函数的示例代码:

describe(MyService.name, () => {
  let service: MyService;
  const matchObj = [
    // initially all are false
    { matchStr: '(min-width: 1024px)', result: false },
    { matchStr: '(min-width: 1366px)', result: false },
    { matchStr: '(max-width: 1366px)', result: false },
  ];
  const fakeObserve = (s: string[]): Observable<BreakpointState> =>
    from(matchObj).pipe(
      filter(match => match.matchStr === s[0]),
      map(match => ({ matches: match.result, breakpoints: {} })),
    );
  const bpSpy = jasmine.createSpyObj('BreakpointObserver', ['observe']);
  bpSpy.observe.and.callFake(fakeObserve);
  beforeEach(() => {
    TestBed.configureTestingModule({
      imports: [],
      providers: [MyService, { provide: BreakpointObserver, useValue: bpSpy }],
    });
  });

  it('should be createable', () => {
    service = TestBed.inject(MyService);
    expect(service).toBeTruthy();
  });

  describe('observe()', () => {
    function resize(width: number): void {
      matchObj[0].result = width >= 1024;
      matchObj[1].result = width >= 1366;
      matchObj[2].result = width <= 1366;
    }

    it('should return Observable<SidebarMode>', () => {
      resize(1000);
      service = TestBed.inject(MyService);
      service.observe().subscribe(mode => {
        expect(
          Object.values(SidebarMode).includes(SidebarMode[mode]),
        ).toBeTruthy();
      });
    });

    it('should return SidebarMode.Closed', () => {
      resize(600);
      service = TestBed.inject(MyService);
      service
        .observe()
        .subscribe(mode => expect(mode).toBe(SidebarMode.Closed));
    });

    it('should return SidebarMode.Minified', () => {
      resize(1200);
      service = TestBed.inject(MyService);
      service
        .observe()
        .subscribe(mode => expect(mode).toBe(SidebarMode.Minified));
    });

    it('should return SidebarMode.Open', () => {
      resize(2000);
      service = TestBed.inject(MyService);
      service.observe().subscribe(mode => expect(mode).toBe(SidebarMode.Open));
    });
  });
});

在我的情况下,它运行得非常好。我没有想到使用callFake方法。相反,我尝试模拟BreakpointObserver的依赖关系失败了。 - kauppfbi

1
如果您查看BreakpointObserver的测试,您将得到答案。您不需要模拟BreakpointObserver,而是需要模拟注入其中的MediaMatcher。这是我其中一个测试的内容。
        let mediaMatcher: FakeMediaMatcher;

        class FakeMediaQueryList {
            /** The callback for change events. */
            private listeners: ((mql: MediaQueryListEvent) => void)[] = [];

            constructor(public matches: boolean, public media: string) {}

            /** Toggles the matches state and "emits" a change event. */
            setMatches(matches: boolean): void {
                this.matches = matches;

                /** Simulate an asynchronous task. */
                setTimeout(() => {
                    // tslint:disable-next-line: no-any
                    this.listeners.forEach((listener) => listener(this as any));
                });
            }

            /** Registers a callback method for change events. */
            addListener(callback: (mql: MediaQueryListEvent) => void): void {
                this.listeners.push(callback);
            }

            /** Removes a callback method from the change events. */
            removeListener(callback: (mql: MediaQueryListEvent) => void): void {
                const index = this.listeners.indexOf(callback);

                if (index > -1) {
                    this.listeners.splice(index, 1);
                }
            }
        }

        @Injectable()
        class FakeMediaMatcher {
            /** A map of match media queries. */
            private queries = new Map<string, FakeMediaQueryList>();

            /** The number of distinct queries created in the media matcher during a test. */
            get queryCount(): number {
                return this.queries.size;
            }

            /** Fakes the match media response to be controlled in tests. */
            matchMedia(query: string): FakeMediaQueryList {
                const mql = new FakeMediaQueryList(true, query);
                this.queries.set(query, mql);
                return mql;
            }

            /** Clears all queries from the map of queries. */
            clear(): void {
                this.queries.clear();
            }

            /** Toggles the matching state of the provided query. */
            setMatchesQuery(query: string, matches: boolean): void {
                const mediaListQuery = this.queries.get(query);
                if (mediaListQuery) {
                    mediaListQuery.setMatches(matches);
                } else {
                    throw Error('This query is not being observed.');
                }
            }
        }

        beforeEach(async () => {
            await TestBed.configureTestingModule({
                providers: [
                    { provide: MediaMatcher, useClass: FakeMediaMatcher },
                ],
            });
        });

        beforeEach(inject([MediaMatcher], (mm: FakeMediaMatcher) => {
            mediaMatcher = mm;
        }));

        afterEach(() => {
            mediaMatcher.clear();
        });

        describe('get isSideNavClosable$', () => {
            beforeEach(() => {
                // (Andrew Alderson Jan 1, 2020) need to do this to register the query
                component.isSideNavClosable$.subscribe();
            });
            it('should emit false when the media query does not match', (done) => {
                mediaMatcher.setMatchesQuery('(max-width: 1280px)', false);

                component.isSideNavClosable$.subscribe((closeable) => {
                    expect(closeable).toBeFalsy();
                    done();
                });
            });
            it('should emit true when the media query does match', (done) => {
                mediaMatcher.setMatchesQuery('(max-width: 1280px)', true);

                component.isSideNavClosable$.subscribe((closeable) => {
                    expect(closeable).toBeTruthy();
                    done();
                });
            });
        });

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接