Подтвердить что ты не робот

Angular Injection Dependency - @Injectable() не работает в тесте, а @Inject() работает

Общая проблема

Я только что начал добавлять webpacker с angular 5 в существующее приложение rails. Все прекрасно, за исключением странной проблемы с DI в тесте.

Кажется, мои компоненты angular работают только при создании с помощью браузера, но при тестировании с помощью Jasmine/Karma Injector Dependency не может идентифицировать токены ввода. С псевдокодом:

@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
  })
};

конфигурации/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()

конфигурации/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
}

environment.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;

И только в случае загрузчиков / typescript:

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

Ответ 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);
        
        .....

Ответ 2

Вы пытались добавить HttpClient в качестве поставщика в конфигурацию тестового места?

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

Это было предложение одного из разработчиков кармы, когда у кого-то была аналогичная проблема. Это также то, что команда Angular рекомендует, когда вы хотите проверить компонент с зависимостью.

Ответ 3

Итак, посмотрим на JavaScript, сгенерированный с помощью @Inject, и сгенерированный только с помощью @Component или @Injectable (извлеченный из полного декоратора):

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

Это из самой последней версии Angular 5, но, вероятно, все вернется к 2. Вы можете видеть, что @Inject генерирует явную инъекцию параметров, в то время как в противном случае инъекция зависит только от метаданных. Это, по-видимому, сильно указывает на то, что ваша проблема связана с флагом emitDecoratorMetadata, как вы предположили.

Поскольку emitDecoratorMetadata не включен по умолчанию, возможно, ваш tsconfig.json не может быть включен в сборку. Вы можете явно указать его местоположение с помощью свойства ts-loader configFile:

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

Как отмечается в документе, указание имени файла отличается от относительного пути. Для имени файла ts-node будет проходить дерево папок, пытаясь найти файл, но для относительного пути он будет только пытаться относиться к вашему файлу ввода. Вы также можете указать абсолютный путь (для диагностики может быть полезно просто удалить жесткий путь).

Если это не удается, я также могу прочитать Angular Руководство по Webpack, в котором подробно описывается использование awesome-typescript-loader (да, мне нужно было посмотреть его, прежде чем я подумал, что это реально...) вместо ts-loader. Он также явно определяет путь tsconfig, используя хелпер для генерации абсолютного пути.

Ответ 4

Проблема в том, что ваши файлы spec.ts исключены в вашем tsconfig.json, так что emitDecoratorMetadata не применяется к вашим спецификациям?

Ответ 5

У меня возникла похожая проблема, я исправил ее, импортировав core-js в файл polyfills.js. но я до сих пор не знаю, почему это работает.

import 'core-js';