【问题标题】:Angular Dependency Injection - @Injectable() fails in test, while @Inject() worksAngular Dependency Injection - @Injectable() 在测试中失败,而 @Inject() 有效
【发布时间】:2019-01-17 09:28:22
【问题描述】:

一般问题

我刚刚开始将 angular 5 的 webpacker 添加到现有的 rails 应用程序中。一切都很好,除了测试中的 DI 出现了一个奇怪的问题。

似乎我的 Angular 组件在使用浏览器创建时才正常工作,但在使用 Jasmine/Karma 进行测试时,依赖注入器无法识别注入令牌。用伪代码:

@Component({...})
export class SomeComponent {
  constructor(private service: SomeService) {}
}

以上在浏览器中工作,但在测试中给出Error: Can't resolve all parameters for SomeComponent: (?).。到目前为止,我注意到它适用于所有 @Injectable(),但是一旦我用显式 @Inject 替换每个注入:

@Component({...})
export class SomeComponent {
  constructor(@Inject(SomeService) private service: SomeService) {}
}

一切正常(但显然很麻烦)。有什么明显的原因可能导致这种情况吗?

实际代码

我有一个使用 HttpClient 运行的非常简单的服务:

import { Injectable } from "@angular/core";
import { HttpClient } from "@angular/common/http";

import 'rxjs/add/operator/map'

@Injectable()
export class GeneralStatsService {
  constructor(
    private http : HttpClient
  ) {}

  getMinDate() {
    return this.http.get("/api/v1/general_stats/min_date")
      .map(r => new Date(r))
  }
}

当我导航到使用所述服务的组件时,它按预期工作。但是,在使用 Jasmine 进行测试时它不起作用:

import { TestBed } from "@angular/core/testing";
import { HttpClientTestingModule, HttpTestingController } from "@angular/common/http/testing";
import { GeneralStatsService } from "./general-stats.service";


describe('GeneralStatsService', () => {
  let service : GeneralStatsService;
  let httpMock : HttpTestingController;

  beforeEach(()=> {
    TestBed.configureTestingModule({
      imports: [
        HttpClientTestingModule
      ],
      providers: [
        GeneralStatsService
      ]
    })
  });

  beforeEach(() => {
    service = TestBed.get(GeneralStatsService);
    httpMock = TestBed.get(HttpTestingController);
  });

  afterEach(() => {
    httpMock.verify();
  });

  describe('getMinDate()', () => {
    let fakeResponse : string = "2015-03-05T12:39:11.467Z";

    it('returns instance of Date', (done) => {
      service.getMinDate().subscribe((result : Date) => {
        expect(result.getFullYear()).toBe(2015);
        expect(result.getMonth()).toBe(2); // January is 0
        expect(result.getDate()).toBe(5);
        done();
      });

      const req = httpMock.expectOne("/api/v1/general_stats/min_date");
      expect(req.request.method).toBe('GET');
      req.flush(fakeResponse);
    })
  });
});

如上所述,添加显式 @Inject(HttpClient) 可以修复测试,但我更愿意避免这种情况。

配置

业力:

const webpackConfig = require('./config/webpack/test.js');

module.exports = function(config) {
  config.set({
    basePath: '',
    frameworks: [ 'jasmine' ],
    plugins: [
      require('karma-webpack'),
      require('karma-jasmine'),
      require('karma-chrome-launcher'),
      require('karma-jasmine-html-reporter'),
      require('karma-coverage-istanbul-reporter'),
      require('karma-spec-reporter')
    ],
    files: [
      'config/webpack/angular-bundle.ts'
    ],
    webpack: webpackConfig,
    preprocessors: {
      'config/webpack/angular-bundle.ts': ["webpack"]
    },
    mime: { "text/x-typescript": ["ts"]},
    coverageIstanbulReporter: {
      reports: [ 'html', 'lcovonly' ],
      fixWebpackSourcePaths: true
    },
    client: { clearContext: false },

    reporters: [ 'progress', 'kjhtml', 'coverage-istanbul' ],
    port: 9876,
    colors: true,

    logLevel: config.LOG_INFO,
    autoWatch: true,
    browsers: [ 'Chrome' ],
    singleRun: false,
    concurrency: Infinity
  })
};

config/webpack/test.js:

const environment = require('./environment');
environment.plugins.get('Manifest').opts.writeToFileEmit = process.env.NODE_ENV !== 'test';
environment.loaders.set('istanbul-instrumenter', {
  test: /\.ts$/,
  enforce: 'post',
  loader: 'istanbul-instrumenter-loader',
  query: {
    esModules: true
  },
  exclude: ["node_modules", /\.spec.ts$/]
});

module.exports = environment.toWebpackConfig()

config/webpack/angular-bundle.ts:

import 'zone.js/dist/zone'
import 'zone.js/dist/long-stack-trace-zone';
import 'zone.js/dist/proxy.js';
import 'zone.js/dist/sync-test';
import 'zone.js/dist/jasmine-patch';
import 'zone.js/dist/async-test';
import 'zone.js/dist/fake-async-test';
import { getTestBed } from '@angular/core/testing';
import {
    BrowserDynamicTestingModule,
    platformBrowserDynamicTesting
} from '@angular/platform-browser-dynamic/testing';

declare const require: any;

jasmine.MAX_PRETTY_PRINT_DEPTH = 3;

getTestBed().initTestEnvironment(
    BrowserDynamicTestingModule,
    platformBrowserDynamicTesting()
);

const context = (require as any).context('../../app/javascript', true, /\.spec\.ts$/);
context.keys().map(context);

tsconfig.json:

{
  "compilerOptions": {
    "declaration": false,
    "emitDecoratorMetadata": true,
    "experimentalDecorators": true,
    "lib": ["es6", "dom"],
    "module": "es6",
    "moduleResolution": "node",
    "sourceMap": true,
    "target": "es5"
  },
  "exclude": [
    "**/*.spec.ts",
    "node_modules",
    "vendor",
    "public",
    "config/**/*.ts"
  ],
  "compileOnSave": false
}

环境.js:

const environment = require('@rails/webpacker').environment;

const typescript =  require('./loaders/typescript');
const erb =  require('./loaders/erb');
const elm =  require('./loaders/elm');
const html =  require('./loaders/html');

environment.loaders.append('elm', elm);
environment.loaders.append('erb', erb);
environment.loaders.append('typescript', typescript);
environment.loaders.append('html', html);

module.exports = environment;

以防万一加载器/打字稿:

module.exports = {
  test: /\.(ts|tsx)?(\.erb)?$/,
  use: [{
    loader: 'ts-loader'
  }]
}

【问题讨论】:

  • 你有 tsconfig.test.json 文件吗?如果是这样,是否有“emitDecoratorMetadata”属性设置为 true?
  • @chrisvietor - 是的,这是问题中的最后一个 sn-p。这也是我第一次去,我一直想知道我的业力设置中是否有什么影响它。
  • 您可以添加您的./environment 文件吗?
  • @GregRozmarynowycz - 与打字稿加载器一起添加。

标签: ruby-on-rails angular typescript dependency-injection webpacker


【解决方案1】:

尝试使用注射器和 spyOn。

您必须创建一个没有“HttpClient”的模拟服务,该服务具有您要模拟的服务的所有方法。然后使用 spyOn 你可以返回你想要的。

TestBed.configureTestingModule({
      imports: [
        FormsModule,
        BrowserAnimationsModule
      ],
      providers: [
        {
          provide: YourService,
          useValue: mockedYourService
        }
      ]
      ....

 beforeEach(() => {
   fixture = TestBed.createComponent(YourTestingComponent);
   component = fixture.componentInstance;
   element = fixture.nativeElement;
   fixture.detectChanges();
 });

 ...
      
describe('methodName', () => {
  it('message to print',
    () => {
      const your_Service = fixture.debugElement.injector.get(YourService);
      spyOn(your_Service, 'methodName').and.returnValue(true);
        
        .....

希望对您有所帮助!

【讨论】:

  • 不幸的是,它没有回答为什么 @Inject() 有效而 @Injectable() 无效的问题。我已经设法让测试工作(使用@Inject()),但我更喜欢依赖标准的@Injectable() 方式。
  • 所以可能会有所帮助...? stackoverflow.com/questions/37315317/…
  • 那里的答案实际上使我的问题更加清楚。它声明obj:SomeType is equivalent to @Inject(SomeType) obj - 这是我所期望的,它显然不起作用因为即使指定了参数的类型,我也必须指定注入令牌
【解决方案2】:

看看使用@Inject 生成的JavaScript,以及仅使用@Component@Injectable 生成的JavaScript(从完整的装饰器中提取):

__param(0, core_1.Inject(http_1.HttpClient)), // via @Inject
__metadata("design:paramtypes", [http_1.HttpClient]) // with @Component, @Injectable only

这是来自最新版本的 Angular 5,但可能一直适用于 Angular 2。您可以看到 @Inject 生成显式参数注入,而其他注入仅依赖于元数据。这似乎强烈表明您的问题与您所建议的 emitDecoratorMetadata 标志有关。

由于emitDecoratorMetadata 不是默认启用的选项,您的tsconfig.json 可能不会包含在构建中。您可以使用 ts-loader configFile 属性显式指定其位置:

use: [{
        loader: 'ts-loader', 
        options: {
          configFile: 'tsconfig.json' // default
        }
      }] 

如文档所述,指定文件名与相对路径不同。对于文件名ts-node 将遍历文件夹树以尝试定位文件,但对于相对路径它只会尝试相对于您的条目文件。您还可以指定绝对路径(对于诊断,只需删除硬编码路径可能很有用)。

如果失败,我还可以阅读Angular Webpack guide,其中详细说明了awesome-typescript-loader 的用法(是的,在我相信它是真实的之前我必须查一下...)而不是ts-loader。它还显式定义了tsconfig 路径,使用帮助程序生成绝对路径。

【讨论】:

  • 我同意,很确定 emitDecoratorMetadata 有一些东西,但是我已经测试过 tsconfig.json 包含在构建中(通过将其更改为无效的 json 构建爆炸)。设置 configFile 选项不能解决问题。 :(
  • 能否在生成的JS中搜索design:paramtypes,看看是否正在生成元数据调用?我相信即使在缩小的代码中也应该存在完整的字符串键
  • 好的,这很有趣,因为这个元数据存在于通过 karma 运行的代码中:GeneralStatsService=__decorate([Object(__WEBPACK_IMPORTED_MODULE_0__angular_core__["w" /* Injectable */])(),__metadata("design:paramtypes",[__WEBPACK_IMPORTED_MODULE_1__angular_common_http__["b" /* HttpClient */]])],GeneralStatsService);
  • 好吧,很有趣;我想知道与__param(来自@Inject)相比,它是什么样子的。我还想知道它是否可能是Reflect.metadata 扩展的某个方面,因为如果它丢失,Angular 可能会默默地失败;您可以尝试在文件顶部添加console.log('TEST METADATA', Reflect.metadata); 吗?应该是一个函数。
【解决方案3】:

您是否尝试在测试台配置中添加 HttpClient 作为提供程序?

TestBed
  .configureTestingModule({
    imports: [HttpClientTestingModule],
    providers: [GeneralStatsService,
      { provide: HttpClient, useValue: new HttpClient() }
    ]
  })

这是 karma 开发人员之一在有人拥有a similar problem 时提出的建议。当您想test a component with a dependency 时,这也是 Angular 团队的建议。

【讨论】:

  • 感谢您的链接,这似乎是完全相同的问题。提出的解决方案实际上只是一个调试步骤来排除缺乏相关提供者 - 不幸的是,这种情况并非如此(因为它甚至不适用于@Inject(<token>)。但是该链接可能非常有用,谢谢。跨度>
  • 好的,在阅读了问题下的整个对话后,我很确定这是 emitDecoratorMetadata 的问题。需要深入研究一下,因为我的 tsconfig.js 中有这个选项,但运行 karma 时似乎被 webpacker 忽略了。
  • 附言。澄清为什么不需要注入:HttpClientTestingModule 已经提供了带有模拟 HttpClient 服务的HttpClient 令牌。从不建议在测试中提供实际的 HttpClient 实例,因为您不应该在单元测试中发出真正的 http 请求。
【解决方案4】:

问题是您的spec.ts 文件被排除在您的tsconfig.json 中,因此emitDecoratorMetadata 未应用于您的规范吗?

【讨论】:

  • 不幸的是,删除这些排除项并不能解决问题。 :(
【解决方案5】:

我遇到了类似的问题,我通过在 polyfills.js 文件中导入 core-js 来修复它。 但我仍然不知道它为什么会起作用。

import 'core-js';

【讨论】:

    猜你喜欢
    • 2020-12-13
    • 2020-08-04
    • 1970-01-01
    • 1970-01-01
    • 2016-09-15
    • 1970-01-01
    • 2023-04-04
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多