Вот обычный старый компонент Angular:

@Component({
  selector: 'app-dog',
  templateUrl: './dog.component.html',
  styleUrls: ['./dog.component.css']
})
class DogComponent {
  constructor(private logger) {}
  speak() {
    this.logger.info('bark');
  }
}

Самый распространенный способ (то есть самый простой способ после запуска ng generate) протестировать такой компонент — использовать TestBed

describe('DogComponent' () => {
  let component: BannerComponent;
  let fixture: ComponentFixture<BannerComponent>;
  let mockLogger;
  let logger;
  beforeEach(async(() => {
    // using jest here, but any testing library will do
    mockLogger = { info: jest.fn() };
    TestBed.configureTestingModule({
      declarations: [ DogComponent ],
      providers: [
        { provide: Logger, useValue: mockLogger }
      ]
    }).compileComponents();
  }));
  beforeEach(() => {
    fixture = TestBed.createComponent(BannerComponent);
    component = fixture.componentInstance;
    fixture.detectChanges();
    logger = TestBed.get(Logger);
  });
  
  describe('speak', () => {
    component.speak();
    expect(logger.info).toHaveBeenCalledWith('bark');
  });
});

К сожалению, легко впасть в настройку TestBed перед каждой спецификацией компонента, даже если это бесполезно. Для большого компонента с множеством импортов, объявлений и провайдеров это может означать сотни миллисекунд для каждой спецификации. В моей работе эти спецификации часто занимали до 300 мс каждая. Они складываются и могут разрушить циклы обратной связи с разработчиками.

Основы TypeScript в помощь

Вот пример теста, выполнение которого занимает около 1 миллисекунды:

const component = {
  logger: {
    info: jest.fn()
  }
};
const speak = DogComponent.prototype.speak.bind(component);
speak();
expect(component.logger.info).toHaveBeenCalledWith('bark');

Давайте сломаем это

component — это объект, очень похожий на настоящий DogComponent. У него есть logger, где info замаскирован, но нет метода speak.

const component = {
  logger: {
    info: jest.fn()
  }
};

На самом деле, просто достаточно this контекста для метода speak. Единственный раз, когда speak ссылается на this, это когда он вызывает this.logger.info('bark') . Так что это все, что нам нужно предоставить.

const speak = DogComponent.prototype.speak.bind(component);

Мы делаем функцию speak. Это то же самое, что и метод speak для DogComponent, за исключением того, что мы вызываем bind, поэтому каждый this в методе заменяется на component.

Понимание методов TypeScript

Методы и классы в TypeScript/ES6 могут не соответствовать вашим ожиданиям.

class DogComponent {
  constructor(private logger) {}
  speak() {
    this.logger.info('bark');
  }
}

Действительно ли синтаксический сахар для

function DogComponent(logger) {
  this.logger = logger;
}
DogComponent.prototype.speak = function() {
  this.logger.info('bark');
}

Когда мы устанавливаем атрибут прототипа speak, каждый экземпляр DogComponent будет автоматически связывать этот метод с самим собой.

И обратите внимание, как мы взаимодействуем с этим в нашем тесте:

// in built source
DogComponent.prototype.speak = function() {
  this.logger.info('bark');
}
// in test (call bind so that 'this' references 'component')
const speak = DogComponent.prototype.speak.bind(component);

Альтернативно

Еще один способ сделать такие же быстрые тесты — new объединить ваши компоненты.

const logger = { info: mockFn() };
const dog = new Dog(logger);
dog.speak();
expect(logger.info).toHaveBeenCalledWith('bark');

Хорошая практика

Если вы решили протестировать метод, извлекая его из прототипа, вы тестируете этот метод изолированно. Это хорошая вещь! Это позволяет очень легко проверить, как этот метод взаимодействует с объектом, к которому он привязан. Но это не замена интеграционному тестированию и DOM-тестированию. Обязательно проверьте поведение, которое вы хотите — для этого могут потребоваться интегрированные тесты, требующие дополнительной настройки. Логика шаблона также может легко потребовать тестирования DOM.

Не сбрасывайте со счетов TestBed

Хотя TestBed является источником многих излишне медленных тестов, это все же отличный инструмент. Если вы тестируете только методы компонента, вы не можете сказать, что передается дочерним компонентам, вы не можете проверить, что происходит в DOM, и вы упускаете удобный TestBed API.