У меня следующий класс:

import { Observable } from 'rxjs/Observable';
import * as Logger from 'js-logger';

import { CookieParser } from './cookie-parser.service';
import { LogLevelConverter } from './loglevel-converter.service';
export class LoggerFactory {
  //Logging levels can be referenced like so: `LoggerFactory.WARN`
  public static readonly DEBUG = Logger.DEBUG;
  public static readonly INFO = Logger.INFO;
  public static readonly WARN = Logger.WARN;
  public static readonly ERROR = Logger.ERROR;
  public static readonly OFF = Logger.OFF;

  private static readonly LOG_LEVEL: string = 'FACTORY_LOG_LEVEL';
  private static initialized = false;

  public static getLogger(name: string): any {
    if (!LoggerFactory.initialized) {
      LoggerFactory.init(name);
    }

    return Logger.get(name);
  }

  private static init(name: string): void {
    //Set default logging level for LoggerFactory
    const DEFAULT_LOG_LEVEL = LoggerFactory.ERROR;
    Logger.setLevel(DEFAULT_LOG_LEVEL);

    let logLevel: string;

    if (window.document.cookie.indexOf(LoggerFactory.LOG_LEVEL) > -1) {
      logLevel = CookieParser.getCookieValue(LoggerFactory.LOG_LEVEL);
    } else if (LoggerFactory.isLocalStorageSupported()) {
      logLevel = localStorage.getItem(LoggerFactory.LOG_LEVEL);
    }

    if (logLevel) {
      Logger.get(name).setLevel(LogLevelConverter.toLogLevel(logLevel));
    } else {
      Logger.get(name).setLevel(DEFAULT_LOG_LEVEL);
    }

    LoggerFactory.initialized = true;
  }

  private static isLocalStorageSupported(): boolean {
    const testKey = 'test',
      storage = window.localStorage;
    try {
      storage.setItem(testKey, '1');
      storage.removeItem(testKey);
      return true;
    } catch (error) {
      return false;
    }
  }
}

/**
 * Used for logging the lifecycles of a component
 * Usage: @NgLifecycleLog()
 * @param  {string}         name for the logger
 * @return {ClassDecorator}
 */
export function NgLifecycleLog(name?: string): ClassDecorator {
  return function(constructor: any): void {
    const LIFECYCLE_HOOKS: Array<string> = ['ngOnInit', 'ngOnChanges', 'ngOnDestroy'];

    //If no name is given, default to using the component's constructor's name
    const NAME: string = name ? name : constructor.name;

    const lifecycleLogger = LoggerFactory.getLogger(NAME);

    console.error("SHOULDN'T BE CALLED"); //gets called

    LIFECYCLE_HOOKS.forEach(hook => {
      const original = constructor.prototype[hook];

      constructor.prototype[hook] = function(...args) {
        lifecycleLogger.info(`${hook}`, ...args);
        original.apply(this, args);
      };
    });
  };
}

У меня также есть следующий модульный тест:

import { LoggerFactory } from './logger-factory.service';

describe('LoggerFactory', () => {
  localStorage.setItem('FACTORY_LOG_LEVEL', 'WARN');
  const LOGGER_NAME = 'unit.testing.name';
  const logger: any = LoggerFactory.getLogger(LOGGER_NAME);

  it('should provide a logger', () => {
    expect(logger).toBeDefined(logger);
  });

  it('should have the correct name', () => {
    expect(logger.context.name).toEqual(LOGGER_NAME);
  });

  it('should allow for localStorage log level overrides', () => {
    expect(logger.getLevel().name).toEqual('WARN');
  });
});

Моя проблема в том, что хотя я никогда не вызываю метод NgLifecycleLog(), он вызывается. Это проблема, потому что я пытаюсь использовать в нем операторы LoggerFactory вместо console. Когда я запускаю модульный тест, вызывается NgLifecycleLog(), что вызывает создание экземпляра LoggerFactory, когда я этого не ожидаю.

Почему это называется? Я пробовал делать fdescribe в тестах, чтобы убедиться, что это единственное, что выполняется, но даже в этом случае это не исправляет.

1
Nxt3 15 Ноя 2017 в 17:57

1 ответ

Лучший ответ

Декораторы классов выполняются один раз, когда определен декорированный класс. Если декорированный класс был включен в тестовый комплект, будет вызвана функция декоратора.

Если функция декоратора вызывает побочные эффекты или задействует другие модули (это может повлиять на модульные тесты), ее следует отрефакторировать.

В этом случае декоратор исправляет методы прототипа класса:

const lifecycleLogger = LoggerFactory.getLogger(NAME);

LIFECYCLE_HOOKS.forEach(hook => {
  const original = constructor.prototype[hook];

  constructor.prototype[hook] = function(...args) {
    lifecycleLogger.info(`${hook}`, ...args);
    original.apply(this, args);
  };
});

lifecycleLogger следует переместить из места, где он вызывается при оформлении класса, в места, где он фактически используется:

LIFECYCLE_HOOKS.forEach(hook => {
  const original = constructor.prototype[hook];

  constructor.prototype[hook] = function(...args) {
    const lifecycleLogger = LoggerFactory.getLogger(NAME);
    lifecycleLogger.info(`${hook}`, ...args);
    original.apply(this, args);
  };
});
1
Estus Flask 15 Ноя 2017 в 15:23