如何使用Karma(Angular 7)模拟Image对象?

时间:2018-11-13 09:07:14

标签: angular image dom karma-jasmine angular7

我正在尝试模拟默认的DOM Image对象,以对Angular服务进行单元测试。

该服务很简单,它检查“ webP”格式的支持:

import {Injectable} from '@angular/core';
import {Promise} from 'es6-promise';
import {environment} from '../../../../environments/environment';

@Injectable({
  providedIn: 'root'
})
export class AppLoadService {

  constructor() {
  }

  initializeApp(): Promise<any> {
    return new Promise((resolve) => {
      console.log(`initializeApp:: inside promise`);
      if (typeof Image === 'undefined') {
        console.log(`initializeApp:: Image undefined`);
        resolve();
        return;
      }
      const webP = new Image();
      webP.onload = () => {
        console.log(`initializeApp:: WebP support: true`);
        environment.webP = true;
        resolve();
      };
      webP.onerror = () => {
        console.log(`initializeApp:: WebP support: false`);
        resolve();
      };
      webP.src = 'data:image/webp;base64,UklGRjoAAABXRUJQVlA4IC4AAACyAgCdASoCAAIALmk0mk0iIiIiIgBoSygABc6WWgAA/veff/0PP8bA//LwYAAA';
    });
  }
}

我找到了一种检查webP支持的方法(运行Karma的铬是默认设置),以及一种检查Image undefined上的后备情况的方法。

但是我找不到一种方法来检查onerror后备...

这是我的规格文件:

import {TestBed} from '@angular/core/testing';
import {AppLoadService} from './app-load.service';
import {environment} from '../../../../environments/environment';

describe('AppLoadService', () => {
  let service: AppLoadService;
  const originalImage = Image;

  beforeEach(() => TestBed.configureTestingModule({}));

  beforeEach(() => {
    service = TestBed.get(AppLoadService);
  });

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

  it('should resolve with webP', (done) => {
    const test = () => {
      Image = originalImage;
      service.initializeApp().then(() => {
        expect(environment.webP).toBe(true);
        done();
      });
    };
    test();
  });

  it('should resolve without webP (A)', (done) => {
    const test = () => {
      Image = undefined;
      service.initializeApp().then(() => {
        expect(environment.webP).toBe(false);
        done();
      });
    };
    test();
  });

  it('should resolve without webP (B)', (done) => {
    // How to force Image to throw "onerror" ?
    const test = () => {
      Image = originalImage;
      service.initializeApp().then(() => {
        expect(environment.webP).toBe(false);
        done();
      });
    };
    test();
  });
});

问题在文件末尾的should resolve without webP (B)测试中。

此外,还有更好的方法来检查未定义的Image对象或onload回调吗?

谢谢!


编辑

无法按原样工作,因此我更改了服务构造函数以提供“图像”依赖项。

constructor(@Inject('Image') image: typeof Image) {
  this.image = image;
}

必须像这样加载模块:

providers: [
  AppLoadService,
  // [...]
  {provide: 'Image', useValue: Image},
]

每个resolve()现在都包含environment.webP个结果。否则,个别测试会很痛苦,environment在测试之前会被随机重写。

使用一个简单的模拟,它的工作原理如下:

import {TestBed} from '@angular/core/testing';
import {AppLoadService} from './app-load.service';

class MockImageFail {
  public onerror: Function;
  private _src: string;

  set src(src) {
    this._src = src;
    if (this.onerror) {
      this.onerror();
    }
  }
}

describe('AppLoadService', () => {

  beforeEach(() => {
    TestBed.configureTestingModule({
      providers: [{provide: 'Image', useValue: Image}]
    });
  });

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

  it('should resolve with webP', (done) => {
    const service = TestBed.get(AppLoadService);
    service.initializeApp().then((supportWebP) => {
      expect(supportWebP).toBe(true);
      done();
    });
  });

  it('should not resolve without webP (A)', (done) => {
    TestBed.overrideProvider('Image', {useValue: undefined});
    const service = TestBed.get(AppLoadService);
    service.initializeApp().then((supportWebP) => {
      expect(supportWebP).toBe(false);
      done();
    });
  });

  it('should not resolve without webP (B)', (done) => {
    TestBed.overrideProvider('Image', {useValue: MockImageFail});
    const service = TestBed.get(AppLoadService);
    service.initializeApp().then((supportWebP) => {
      expect(supportWebP).toBe(false);
      done();
    });
  });
});

我对此并不满意,我敢肯定还有另一种更好的方法:/

1 个答案:

答案 0 :(得分:0)

我有类似的需求,但我不想为 Image 创建一个提供程序,因为我只在一个地方需要它。但我仍然需要模拟 Image 才能编写我的规范。

我确实有 window 的现有提供程序,因此我决定改用它。我正在编写一个网络应用程序,所以我的“全局”实际上是 window。这是根据 MDN:"In a web browser, any code which the script doesn't specifically start up as a background task has a Window as its global object. This is the vast majority of JavaScript code on the Web." 我将 new Image() 更改为 new this.window.Image() 并在我的构造函数中注入窗口,如下所示:

constructor(@Inject(WINDOW) private readonly window: any) {}

我的支票看起来像这样:

public canLoadImage(uri: string): Promise<boolean> {
  const image = new this.window.Image();
  this.promise = new Promise((resolve) => {
    image.onload = () => resolve(true);
    image.onerror = () => resolve(false);
  });
  image.src = `${uri}?${Math.random()}`;
  return this.promise;
}

所以我能够通过像这样模拟和注入 window 来测试这个:

beforeEach(() => {
  mockImage = {};
  mockWindow = { Image: () => mockImage };
  service = new MyService(mockWindow);
});
 
it('resolves false when youtube is not reachable', fakeAsync(() => {
  const successSpy = jasmine.createSpyObject('success');
  service.canLoadImage('https://example.com').then(successSpy);
  mockImage.onerror();
  flushMicrotasks();

  expect(successSpy).toHaveBeenCalledWith(false);
}));

it('resolves true when youtube is reachable', fakeAsync(() => {
  const successSpy = jasmine.createSpyObject('success');
  service.canLoadImage('https://example.com').then(successSpy);
  mockImage.onload();
  flushMicrotasks();

  expect(successSpy).toHaveBeenCalledWith(true);
}));

请注意,您可以轻松地将代码更改为在失败时拒绝,并通过直接调用 mockImage.onerror() 进行测试。

另请注意,您必须在调用 flushMicrotaskt(或 tick)的同一区域中创建承诺。这就是为什么我设置成功间谍,调用 canLoadImage,并在同一个 fakeAsync 方法调用中调用模拟图像方法。

我这样做的一个原因是为了避免实际的外部调用来获取图像。因为我的 Image 实例只是一个可以累积效果的对象,所以我不必担心它实际加载图像。