• Stars
    star
    2,068
  • Rank 22,347 (Top 0.5 %)
  • Language
    TypeScript
  • License
    MIT License
  • Created about 7 years ago
  • Updated 2 months ago

Reviews

There are no reviews yet. Be the first to send feedback to the community and the maintainers!

Repository Details

๐ŸฆŠ ๐Ÿš€ A Powerful Tool to Simplify Your Angular Tests

All Contributors spectator MIT commitizen PRs styled with prettier Build Status

A Powerful Tool to Simplify Your Angular Tests

Spectator helps you get rid of all the boilerplate grunt work, leaving you with readable, sleek and streamlined unit tests.

Features

  • โœ… Support for testing Angular components, directives and services
  • โœ… Easy DOM querying
  • โœ… Clean API for triggering keyboard/mouse/touch events
  • โœ… Testing ng-content
  • โœ… Custom Jasmine/Jest Matchers (toHaveClass, toBeDisabled..)
  • โœ… Routing testing support
  • โœ… HTTP testing support
  • โœ… Built-in support for entry components
  • โœ… Built-in support for component providers
  • โœ… Auto-mocking providers
  • โœ… Strongly typed
  • โœ… Jest Support

Table of Contents

Installation

NPM

npm install @ngneat/spectator --save-dev

Yarn

yarn add @ngneat/spectator --dev

Testing Components

Create a component factory by using the createComponentFactory() function, passing the component class that you want to test. The createComponentFactory() returns a function that will create a fresh component in each it block:

import { Spectator, createComponentFactory } from '@ngneat/spectator';
import { ButtonComponent } from './button.component';

describe('ButtonComponent', () => {
  let spectator: Spectator<ButtonComponent>;
  const createComponent = createComponentFactory(ButtonComponent);

  beforeEach(() => spectator = createComponent());

  it('should have a success class by default', () => {
    expect(spectator.query('button')).toHaveClass('success');
  });

  it('should set the class name according to the [className] input', () => {
    spectator.setInput('className', 'danger');
    expect(spectator.query('button')).toHaveClass('danger');
    expect(spectator.query('button')).not.toHaveClass('success');
  });
});

The createComponentFactory function can optionally take the following options which extends the basic Angular Testing Module options:

const createComponent = createComponentFactory({
  component: ButtonComponent,
  imports: [],
  providers: [],
  declarations: [],
  entryComponents: [],
  componentProviders: [], // Override the component's providers
  componentViewProviders: [], // Override the component's view providers
  overrideModules: [], // Override modules
  overrideComponents: [], // Override components in case of testing standalone component
  overrideDirectives: [], // Override directives in case of testing standalone directive
  overridePipes: [], // Override pipes in case of testing standalone pipe
  mocks: [], // Providers that will automatically be mocked
  componentMocks: [], // Component providers that will automatically be mocked
  componentViewProvidersMocks: [], // Component view providers that will be automatically mocked
  detectChanges: false, // Defaults to true
  declareComponent: false, // Defaults to true
  disableAnimations: false, // Defaults to true
  shallow: true, // Defaults to false
});

The createComponent() function optionally takes the following options:

it('should...', () => {
  spectator = createComponent({
    // The component inputs
    props: {
      title: 'Click'
    },
    // Override the component's providers
    // Note that you must declare it once in `createComponentFactory`
    providers: [],
    // Whether to run change detection (defaults to true)
    detectChanges: false
  });

  expect(spectator.query('button')).toHaveText('Click');
});

By providing overrideComponents options in scope of our createComponent() function we can define the way of overriding standalone component and it's dependencies

@Component({
  selector: `app-standalone-with-import`,
  template: `<div id="standalone">Standalone component with import!</div>
  <app-standalone-with-dependency></app-standalone-with-dependency>`,
  imports: [StandaloneComponentWithDependency],
  standalone: true,
})
export class StandaloneWithImportsComponent {}

@Component({
  selector: `app-standalone-with-dependency`,
  template: `<div id="standaloneWithDependency">Standalone component with dependency!</div>`,
  standalone: true,
})
export class StandaloneComponentWithDependency {
  constructor(public query: QueryService) {}
}

@Component({
  selector: `app-standalone-with-dependency`,
  template: `<div id="standaloneWithDependency">Standalone component with override dependency!</div>`,
  standalone: true,
})
export class MockStandaloneComponentWithDependency {
  constructor() {}
}

it('should...', () => {
  const spectator = createHostFactory({
    component: StandaloneWithImportsComponent,
    template: `<div><app-standalone-with-import></app-standalone-with-import></div>`,
    overrideComponents: [
      [
        StandaloneWithImportsComponent,
        {
          remove: { imports: [StandaloneComponentWithDependency] },
          add: { imports: [MockStandaloneComponentWithDependency] },
        },
      ],
    ],
  });

  expect(host.query('#standalone')).toContainText('Standalone component with import!');
  expect(host.query('#standaloneWithDependency')).toContainText('Standalone component with override dependency!');
});

The createComponent() method returns an instance of Spectator which exposes the following API:

  • fixture - The tested component's fixture

  • component - The tested component's instance

  • element - The tested component's native element

  • debugElement - The tested fixture's debug element

  • inject() - Provides a wrapper for TestBed.inject():

const service = spectator.inject(QueryService);

const fromComponentInjector = true;
const service = spectator.inject(QueryService, fromComponentInjector);
  • detectChanges() - Runs detectChanges on the tested element/host:
spectator.detectChanges();
  • detectComponentChanges() - Runs detectChanges on the tested component ( not on the host ). You'll need this method in rare cases when using a host and the tested component is onPush, and you want to force it to run a change detection cycle.
spectator.detectComponentChanges();
  • setInput() - Changes the value of an @Input() of the tested component. Method runs ngOnChanges with SimpleChanges manually if it exists.
it('should...', () => {
  spectator.setInput('className', 'danger');

  spectator.setInput({
    className: 'danger'
  });
});
  • output - Returns an Observable @Output() of the tested component:
it('should emit the $event on click', () => {
  let output;
  spectator.output('click').subscribe(result => (output = result));

  spectator.component.onClick({ type: 'click' });
  expect(output).toEqual({ type: 'click' });
});
  • tick(millis?: number) - Run the fakeAsync tick() function and call detectChanges():
it('should work with tick', fakeAsync(() => {
  spectator = createComponent(ZippyComponent);
  spectator.component.update();
  expect(spectator.component.updatedAsync).toBeFalsy();
  spectator.tick(6000);
  expect(spectator.component.updatedAsync).not.toBeFalsy();
}))

Events API

Each one of the events can accept a SpectatorElement which can be one of the following:

type SpectatorElement = string | Element | DebugElement | ElementRef | Window | Document | DOMSelector;

If not provided, the default element will be the host element of the component under test.

  • click() - Triggers a click event:
spectator.click(SpectatorElement);
spectator.click(byText('Element'));
  • blur() - Triggers a blur event:
spectator.blur(SpectatorElement);
spectator.blur(byText('Element'));

Note that if using the jest framework, blur() only works if the element is focused. Details.

  • focus() - Triggers a focus event:
spectator.focus(SpectatorElement);
spectator.focus(byText('Element'));
  • typeInElement() - Simulating the user typing:
spectator.typeInElement(value, SpectatorElement);
spectator.typeInElement(value, byText('Element'));
  • dispatchMouseEvent() - Triggers a mouse event:
spectator.dispatchMouseEvent(SpectatorElement, 'mouseout');
spectator.dispatchMouseEvent(SpectatorElement, 'mouseout'), x, y, event);
spectator.dispatchMouseEvent(byText('Element'), 'mouseout');
spectator.dispatchMouseEvent(byText('Element'), 'mouseout', x, y, event);
  • dispatchKeyboardEvent() - Triggers a keyboard event:
spectator.dispatchKeyboardEvent(SpectatorElement, 'keyup', 'Escape');
spectator.dispatchKeyboardEvent(SpectatorElement, 'keyup', { key: 'Escape', keyCode: 27 })
spectator.dispatchKeyboardEvent(byText('Element'), 'keyup', 'Escape');
spectator.dispatchKeyboardEvent(byText('Element'), 'keyup', { key: 'Escape', keyCode: 27 })
  • dispatchTouchEvent() - Triggers a touch event:
spectator.dispatchTouchEvent(SpectatorElement, type, x, y);
spectator.dispatchTouchEvent(byText('Element'), type, x, y);

Custom Events

You can trigger custom events (@Output() of child components) using the following method:

spectator.triggerEventHandler(MyChildComponent, 'myCustomEvent', 'eventValue');
spectator.triggerEventHandler(MyChildComponent, 'myCustomEvent', 'eventValue', { root: true});

spectator.triggerEventHandler('app-child-component', 'myCustomEvent', 'eventValue');
spectator.triggerEventHandler('app-child-component', 'myCustomEvent', 'eventValue', { root: true});

Event Creators

In case you want to test events independently of any template (e.g. in presenter services) you can fallback on the underlying event creators. They are basically providing the same signature without the preceding element.

const keyboardEvent = createKeyboardEvent('keyup', 'ArrowDown'/*, targetElement */);
const mouseEvent = createMouseEvent('mouseout');
const touchEvent = createTouchEvent('touchmove');
const fakeEvent = createFakeEvent('input');

Keyboard helpers

spectator.keyboard.pressEnter();
spectator.keyboard.pressEscape();
spectator.keyboard.pressTab();
spectator.keyboard.pressBackspace();
spectator.keyboard.pressKey('a');
spectator.keyboard.pressKey('ctrl.a');
spectator.keyboard.pressKey('ctrl.shift.a');

Mouse helpers

spectator.mouse.contextmenu('.selector');
spectator.mouse.dblclick('.selector');

Note that each one of the above methods will also run detectChanges().

Queries

The Spectator API includes convenient methods for querying the DOM as part of a test: query, queryAll, queryLast , queryHost and queryHostAll. All query methods are polymorphic and allow you to query using any of the following techniques.

String Selector

Pass a string selector (in the same style as you would when using jQuery or document.querySelector) to query for elements that match that path in the DOM. This method for querying is equivalent to Angular's By.css predicate. Note that native HTML elements will be returned. For example:

// Returns a single HTMLElement
spectator.query('div > ul.nav li:first-child');
// Returns an array of all matching HTMLElements
spectator.queryAll('div > ul.nav li');

// Query from the document context
spectator.query('div', { root: true });

spectator.query('app-child', { read: ChildServiceService });

Type Selector

Pass a type (such as a component, directive or provider class) to query for instances of that type in the DOM. This is equivalent to Angular's By.directive predicate. You can optionally pass in a second parameter to read a specific injection token from the matching elements' injectors. For example:

// Returns a single instance of MyComponent (if present)
spectator.query(MyComponent);

// Returns the instance of `SomeService` found in the instance of `MyComponent` that exists in the DOM (if present)
spectator.query(MyComponent, { read: SomeService });

spectator.query(MyComponent, { read: ElementRef });
host.queryLast(ChildComponent);
host.queryAll(ChildComponent);

DOM Selector

Spectator allows you to query for elements using selectors inspired by dom-testing-library. The available selectors are:

spectator.query(byPlaceholder('Please enter your email address'));
spectator.query(byValue('By value'));
spectator.query(byTitle('By title'));
spectator.query(byAltText('By alt text'));
spectator.query(byLabel('By label'));
spectator.query(byText('By text'));
spectator.query(byText('By text', {selector: '#some .selector'}));
spectator.query(byTextContent('By text content', {selector: '#some .selector'}));
spectator.query(byRole('checkbox', { checked: true }));

The difference between byText and byTextContent is that the former doesn't match text inside a nested elements.

For example, in this following HTML byText('foobar', {selector: 'div'}) won't match the following div, but byTextContent will:

<div>
  <span>foo</span>
  <span>bar</span>
</div>

Parent Selector

Spectator allows you to query for nested elements within a parent element. This is useful when you have multiple instances of the same component on the page and you want to query for children within a specific one. The parent selector is a string selector that is used to find the parent element. The parent selector is passed as the second parameter to the query methods. For example:

spectator.query(ChildComponent, { parentSelector: '#parent-component-1' });
spectator.queryAll(ChildComponent, { parentSelector: '#parent-component-1' });

Testing Select Elements

Spectator allows you to test <select></select> elements easily, and supports multi select.

Example:

it('should set the correct options on multi select', () => {
  const select = spectator.query('#test-multi-select') as HTMLSelectElement;
  spectator.selectOption(select, ['1', '2']);
  expect(select).toHaveSelectedOptions(['1', '2']);
});

it('should set the correct option on standard select', () => {
  const select = spectator.query('#test-single-select') as HTMLSelectElement;
  spectator.selectOption(select, '1');
  expect(select).toHaveSelectedOptions('1');
});

It also allows you to check if your change event handler is acting correctly for each item selected. You can disable this if you need to pre set choices without dispatching the change event.

API:

spectator.selectOption(selectElement: HTMLSelectElement, options: string | string[] | HTMLOptionElement | HTMLOptionElement[], config: { emitEvents: boolean } = { emitEvents: true });

Example:

it('should dispatch correct number of change events', () => {
  const onChangeSpy = spyOn(spectator.component, 'handleChange');
  const select = spectator.query('#test-onchange-select') as HTMLSelectElement;

  spectator.selectOption(select, ['1', '2'], { emitEvents: true});

  expect(select).toHaveSelectedOptions(['1', '2']);
  expect(onChangeSpy).toHaveBeenCalledTimes(2);
});

it('should not dispatch correct number of change events', () => {
  const onChangeSpy = spyOn(spectator.component, 'handleChange');
  const select = spectator.query('#test-onchange-select') as HTMLSelectElement;

  spectator.selectOption(select, ['1', '2'], { emitEvents: false});

  expect(select).toHaveSelectedOptions(['1', '2']);
  expect(onChangeSpy).not.toHaveBeenCalledTimes(2);
});

You can also pass HTMLOptionElement(s) as arguments to selectOption and the toHaveSelectedOptions matcher. This is particularly useful when you are using [ngValue] binding on the <option>:

it('should set the correct option on single select when passing the element', () => {
  const select = spectator.query('#test-single-select-element') as HTMLSelectElement;

  spectator.selectOption(select, spectator.query(byText('Two')) as HTMLOptionElement);

  expect(select).toHaveSelectedOptions(spectator.query(byText('Two')) as HTMLOptionElement);
});

Mocking Components

If you need to mock components, you can use the ng-mocks library. Instead of using CUSTOM_ELEMENTS_SCHEMA,which might hide some issues and won't help you to set inputs, outputs, etc., ng-mocks will auto mock the inputs, outputs, etc. for you.

Example:

import { createHostFactory } from '@ngneat/spectator';
import { MockComponent } from 'ng-mocks';
import { FooComponent } from './path/to/foo.component';

const createHost = createHostFactory({
  component: YourComponentToTest,
  declarations: [
    MockComponent(FooComponent)
  ]
});

Testing Single Component/Directive Angular Modules

Components (or Directives) that are declared in their own module can be tested by defining the component module in the imports list of the component factory together with the component. For example:

const createComponent = createComponentFactory({
  component: ButtonComponent,
  imports: [ButtonComponentModule],
});

When used like this, however, Spectator internally adds the component ButtonComponent to the declarations of the internally created new module. Hence, you will see the following error:

Type ButtonComponent is part of the declarations of 2 modules [...]

It is possible to tell Spectator not to add the component to the declarations of the internal module and, instead, use the explicitly defined module as is. Simply set the declareComponent property of the factory options to false:

const createComponent = createComponentFactory({
  component: ButtonComponent,
  imports: [ButtonComponentModule],
  declareComponent: false,
});

When using createDirectiveFactory set the declareDirective property of the factory options to false:

const createDirective = createDirectiveFactory({
  component: HighlightComponent,
  imports: [HighlightComponentModule],
  declareDirective: false,
});

Testing with Host

Testing a component with a host component is a more elegant and powerful technique to test your component. It basically gives you the ability to write your tests in the same way that you write your code. Let's see it in action:

import { createHostFactory, SpectatorHost } from '@ngneat/spectator';

describe('ZippyComponent', () => {
  let spectator: SpectatorHost<ZippyComponent>;
  const createHost = createHostFactory(ZippyComponent);

  it('should display the title from host property', () => {
    spectator = createHost(`<zippy [title]="title"></zippy>`, {
      hostProps: {
        title: 'Spectator is Awesome'
      }
    });
    expect(spectator.query('.zippy__title')).toHaveText('Spectator is Awesome');
  });

  it('should display the "Close" word if open', () => {
    spectator = createHost(`<zippy title="Zippy title">Zippy content</zippy>`);

    spectator.click('.zippy__title');

    expect(spectator.query('.arrow')).toHaveText('Close');
    expect(spectator.query('.arrow')).not.toHaveText('Open');
  });
});

The host method returns an instance of SpectatorHost which extends Spectator with the following additional API:

  • hostFixture - The host's fixture
  • hostComponent - The host's component instance
  • hostElement - The host's native element
  • hostDebugElement - The host's fixture debug element
  • setHostInput - Changes the value of an @Input() of the host component
  • queryHost - Read more about querying in Spectator
  • queryHostAll - Read more about querying in Spectator

Custom Host Component

Sometimes it's helpful to pass your own host implementation. We can pass a custom host component to the createHostFactory() that will replace the default one:

@Component({ selector: 'custom-host', template: '' })
class CustomHostComponent {
  title = 'Custom HostComponent';
}

describe('With Custom Host Component', function () {
  let spectator: SpectatorHost<ZippyComponent, CustomHostComponent>;
  const createHost = createHostFactory({
    component: ZippyComponent,
    host: CustomHostComponent
  });

  it('should display the host component title', () => {
    spectator = createHost(`<zippy [title]="title"></zippy>`);
    expect(spectator.query('.zippy__title')).toHaveText('Custom HostComponent');
  });
});

Testing with Routing

For components which use routing, there is a special factory available that extends the default one, and provides a stubbed ActivatedRoute so that you can configure additional routing options.

describe('ProductDetailsComponent', () => {
  let spectator: SpectatorRouting<ProductDetailsComponent>;
  const createComponent = createRoutingFactory({
    component: ProductDetailsComponent,
    params: { productId: '3' },
    data: { title: 'Some title' }
  });

  beforeEach(() => spectator = createComponent());

  it('should display route data title', () => {
    expect(spectator.query('.title')).toHaveText('Some title');
  });

  it('should react to route changes', () => {
    spectator.setRouteParam('productId', '5');

     // your test here...
  });
});

Triggering a navigation

The SpectatorRouting API includes convenient methods for updating the current route:

interface SpectatorRouting<C> extends Spectator<C> {
  /**
   * Simulates a route navigation by updating the Params, QueryParams and Data observable streams.
   */
  triggerNavigation(options?: RouteOptions): void;

  /**
   * Updates the route params and triggers a route navigation.
   */
  setRouteParam(name: string, value: string): void;

  /**
   * Updates the route query params and triggers a route navigation.
   */
  setRouteQueryParam(name: string, value: string): void;

  /**
   * Updates the route data and triggers a route navigation.
   */
  setRouteData(name: string, value: any): void;

  /**
   * Updates the route fragment and triggers a route navigation.
   */
  setRouteFragment(fragment: string | null): void;

  /**
   * Updates the route url and triggers a route navigation.
   */
  setRouteUrl(url: UrlSegment[]): void;
}

Integration testing with RouterTestingModule

If you set the stubsEnabled option to false, you can pass a real routing configuration and setup an integration test using the RouterTestingModule from Angular.

Note that this requires promises to resolve. One way to deal with this, is by making your test async:

describe('Routing integration test', () => {
  const createComponent = createRoutingFactory({
    component: MyComponent,
    declarations: [OtherComponent],
    stubsEnabled: false,
    routes: [
      {
        path: '',
        component: MyComponent
      },
      {
        path: 'foo',
        component: OtherComponent
      }
    ]
  });

  it('should navigate away using router link', async () => {
    const spectator = createComponent();

    // wait for promises to resolve...
    await spectator.fixture.whenStable();

    // test the current route by asserting the location
    expect(spectator.inject(Location).path()).toBe('/');

    // click on a router link
    spectator.click('.link-1');

    // don't forget to wait for promises to resolve...
    await spectator.fixture.whenStable();

    // test the new route by asserting the location
    expect(spectator.inject(Location).path()).toBe('/foo');
  });
});

Routing Options

The createRoutesFactory function can take the following options, on top of the default Spectator options:

  • params: initial params to use in ActivatedRoute stub
  • queryParams: initial query params to use in ActivatedRoute stub
  • data: initial data to use in ActivatedRoute stub
  • fragment: initial fragment to use in ActivatedRoute stub
  • url: initial URL segments to use in ActivatedRoute stub
  • root: the value for root for the ActivatedRoute stub
  • parent: the value for parent for the ActivatedRoute stub
  • children: the value for children for the ActivatedRoute stub
  • firstChild: the value for firstChild for the ActivatedRoute stub
  • stubsEnabled (default: true): enables the ActivatedRoute stub, if set to false it uses RouterTestingModule instead
  • routes: if stubsEnabled is set to false, you can pass a Routes configuration for RouterTestingModule

Testing Directives

There is a special test factory for testing directives. Let's say we have the following directive:

@Directive({ selector: '[highlight]' })
export class HighlightDirective {

  @HostBinding('style.background-color') backgroundColor : string;

  @HostListener('mouseover')
  onHover() {
    this.backgroundColor = '#000000';
  }

  @HostListener('mouseout')
  onLeave() {
    this.backgroundColor = '#ffffff';
  }
}

Let's see how we can test directives easily with Spectator:

describe('HighlightDirective', () => {
  let spectator: SpectatorDirective<HighlightDirective>;
  const createDirective = createDirectiveFactory(HighlightDirective);

  beforeEach(() => {
    spectator = createDirective(`<div highlight>Testing Highlight Directive</div>`);
  });

  it('should change the background color', () => {
    spectator.dispatchMouseEvent(spectator.element, 'mouseover');

    expect(spectator.element).toHaveStyle({
      backgroundColor: 'rgba(0,0,0, 0.1)'
    });

    spectator.dispatchMouseEvent(spectator.element, 'mouseout');
    expect(spectator.element).toHaveStyle({
      backgroundColor: '#fff'
    });
  });

  it('should get the instance', () => {
    const instance = spectator.directive;
    expect(instance).toBeDefined();
  });
});

Testing Services

The following example shows how to test a service with Spectator:

import { createServiceFactory, SpectatorService } from '@ngneat/spectator';

import { AuthService } from 'auth.service.ts';

describe('AuthService', () => {
  let spectator: SpectatorService<AuthService>;
  const createService = createServiceFactory(AuthService);

  beforeEach(() => spectator = createService());

  it('should not be logged in', () => {
    expect(spectator.service.isLoggedIn()).toBeFalsy();
  });
});

The createService() function returns SpectatorService with the following properties:

  • service - Get an instance of the service
  • inject() - A proxy for Angular TestBed.inject()

Additional Options

It's also possible to pass an object with options. For example, when testing a service you often want to mock its dependencies, as we focus on the service being tested.

For example:

@Injectable()
export class AuthService {
  constructor( private dateService: DateService )  {}

  isLoggedIn() {
    if( this.dateService.isExpired('timestamp') ) {
      return false;
    }
    return true;
  }
}

In this case we can mock the DateService dependency.

import { createServiceFactory, SpectatorService } from '@ngneat/spectator';

import { AuthService } from 'auth.service.ts';

describe('AuthService', () => {
  let spectator: SpectatorService<AuthService>;
  const createService = createServiceFactory({
    service: AuthService,
    providers: [],
    entryComponents: [],
    mocks: [DateService]
  });

  beforeEach(() => spectator = createService());

  it('should be logged in', () => {
    const dateService = spectator.inject(DateService);
    dateService.isExpired.and.returnValue(false);

    expect(spectator.service.isLoggedIn()).toBeTruthy();
  });
});

Testing Pipes

The following example shows how to test a pipe with Spectator:

import { SpectatorPipe, createPipeFactory } from '@ngneat/spectator';

import { StatsService } from './stats.service';
import { SumPipe } from './sum.pipe';

describe('SumPipe', () => {
  let spectator: SpectatorPipe<SumPipe>;
  const createPipe = createPipeFactory(SumPipe);

  it('should sum up the given list of numbers (template)', () => {
    spectator = createPipe(`{{ [1, 2, 3] | sum }}`);
    expect(spectator.element).toHaveText('6');
  });

  it('should sum up the given list of numbers (prop)', () => {
    spectator = createPipe(`{{ prop | sum }}`, {
      hostProps: {
        prop: [1, 2, 3]
      }
    });
    expect(spectator.element).toHaveText('6');
  });

  it('should delegate the summation to the service', () => {
    const sum = () => 42;
    const provider = { provide: StatsService, useValue: { sum } };
    spectator = createPipe(`{{ prop | sum }}`, {
      hostProps: {
        prop: [2, 40]
      },
      providers: [provider]
    });
    expect(spectator.element).toHaveText('42');
  });
});

The createPipe() function returns SpectatorPipe with the following properties:

  • hostComponent - Instance of the host component
  • debugElement - The debug element of the fixture around the host component
  • element - The native element of the host component
  • detectChanges() - A proxy for Angular TestBed.fixture.detectChanges()
  • inject() - A proxy for Angular TestBed.inject()

Using Custom Host Component

The following example illustrates how to test a pipe using a custom host component:

import { Component, Input } from '@angular/core';
import { SpectatorPipe, createPipeFactory } from '@ngneat/spectator';

import { AveragePipe } from './average.pipe';
import { StatsService } from './stats.service';

@Component({
  template: `<div>{{ prop | avg }}</div>`
})
class CustomHostComponent {
  @Input() public prop: number[] = [1, 2, 3];
}

describe('AveragePipe', () => {
  let spectator: SpectatorPipe<AveragePipe>;
  const createPipe = createPipeFactory({
    pipe: AveragePipe,
    host: CustomHostComponent
  });

  it('should compute the average of a given list of numbers', () => {
    spectator = createPipe();
    expect(spectator.element).toHaveText('2');
  });

  it('should result to 0 when list of numbers is empty', () => {
    spectator = createPipe({
      hostProps: {
        prop: []
      }
    });
    expect(spectator.element).toHaveText('0');
  });

  it('should delegate the calculation to the service', () => {
    const avg = () => 42;
    const provider = { provide: StatsService, useValue: { avg } };
    spectator = createPipe({
      providers: [provider]
    });
    expect(spectator.element).toHaveText('42');
  });
});

Mocking Providers

For every Spectator factory, we can easily mock any provider.

Every service that we pass to the mocks property will be mocked using the mockProvider() function. The mockProvider() function converts each method into a Jasmine spy. (i.e jasmine.createSpy()).

Here are some of the methods it exposes:

dateService.isExpired.and.callThrough();
dateService.isExpired.and.callFake(() => fake);
dateService.isExpired.and.throwError('Error');
dateService.isExpired.andCallFake(() => fake);

However, if you use Jest as test framework and you want to utilize its mocking mechanism instead, import the mockProvider() from @ngneat/spectator/jest. This will automatically use the jest.fn() function to create a Jest compatible mock instead.

mockProvider() doesn't include properties. In case you need to have properties on your mock you can use 2nd argument:

const createService = createServiceFactory({
  service: AuthService,
  providers: [
    mockProvider(OtherService, {
      name: 'Martin',
      emitter: new Subject(),
      mockedMethod: () => 'mocked'
    })
  ],
});

Mocking OnInit dependencies

If a component relies on a service being mocked in the OnInit lifecycle method, change-detection needs to be disabled until after the services have been injected.

To configure this, change the createComponent method to have the detectChanges option set to false and then manually call detectChanges on the spectator after setting up the injected services.

const createComponent = createComponentFactory({
  component: WeatherDashboardComponent
});

it('should call the weather api on init', () => {
  const spectator = createComponent({
    detectChanges: false
  });
  const weatherService = spectator.inject(WeatherDataApi);
  weatherService.getWeatherData.andReturn(of(mockWeatherData));
  spectator.detectChanges();
  expect(weatherService.getWeatherData).toHaveBeenCalled();
});

Mocking constructor dependencies

If a component relies on a service being mocked in its constructor, you need to create and configure the mock, and to provide the mock when creating the component.

const createComponent = createComponentFactory({
  component: WeatherDashboardComponent
});

it('should call the weather api in the constructor', () => {
  const weatherService = createSpyObject(WeatherDataApi);
  weatherService.getWeatherData.andReturn(of(mockWeatherData));

  spectator = createComponent({
    providers: [
      { provide: WeatherDataApi, useValue: weatherService }
    ]
  });
  
  expect(weatherService.getWeatherData).toHaveBeenCalled();
});

Jest Support

By default, Spectator uses Jasmine for creating spies. If you are using Jest as test framework instead, you can let Spectator create Jest-compatible spies.

Just import one of the following functions from @ngneat/spectator/jest(instead of @ngneat/spectator), and it will use Jest instead of Jasmine. createComponentFactory(), createHostFactory(), createServiceFactory(), createHttpFactory(), mockProvider().

import { createServiceFactory, SpectatorService } from '@ngneat/spectator/jest';
import { AuthService } from './auth.service';
import { DateService } from './date.service';

describe('AuthService', () => {
  let spectator: SpectatorService<AuthService>;
  const createService = createServiceFactory({
    service: AuthService,
    mocks: [DateService]
  });

  beforeEach(() => spectator = createService());

  it('should not be logged in', () => {
    const dateService = spectator.inject<DateService>(DateService);
    dateService.isExpired.mockReturnValue(true);
    expect(spectator.service.isLoggedIn()).toBeFalsy();
  });

  it('should be logged in', () => {
    const dateService = spectator.inject<DateService>(DateService);
    dateService.isExpired.mockReturnValue(false);
    expect(spectator.service.isLoggedIn()).toBeTruthy();
  });
});

When using the component schematic you can specify the --jest flag to have the Jest imports used. In order to Jest imports the default, update angular.json:

"schematics": {
  "@ngneat/spectator:spectator-component": {
    "jest": true
  }
}

Testing with HTTP

Spectator makes testing data services, which use the Angular HTTP module, a lot easier. For example, let's say that you have service with three methods, one performs a GET, one a POST and one performs concurrent requests:

export class TodosDataService {
  constructor(private httpClient: HttpClient) {}

  getTodos() {
    return this.httpClient.get('api/todos');
  }

  postTodo(id: number) {
    return this.httpClient.post('api/todos', { id });
  }

  collectTodos() {
    return merge(
      this.httpClient.get('/api1/todos'),
      this.httpClient.get('/api2/todos')
    );
  }
}

The test for the above service should look like:

import { createHttpFactory, HttpMethod } from '@ngneat/spectator';
import { TodosDataService } from './todos-data.service';

describe('HttpClient testing', () => {
  let spectator: SpectatorHttp<TodosDataService>;
  const createHttp = createHttpFactory(TodosDataService);

  beforeEach(() => spectator = createHttp());

  it('can test HttpClient.get', () => {
    spectator.service.getTodos().subscribe();
    spectator.expectOne('api/todos', HttpMethod.GET);
  });

  it('can test HttpClient.post', () => {
    spectator.service.postTodo(1).subscribe();

    const req = spectator.expectOne('api/todos', HttpMethod.POST);
    expect(req.request.body['id']).toEqual(1);
  });

  it('can test current http requests', () => {
    spectator.service.getTodos().subscribe();
    const reqs = spectator.expectConcurrent([
        { url: '/api1/todos', method: HttpMethod.GET },
        { URL: '/api2/todos', method: HttpMethod.GET }
    ]);

    spectator.flushAll(reqs, [{}, {}, {}]);
  });
});

We need to create an HTTP factory by using the createHttpFactory() function, passing the service that you want to test. The createHttpFactory() returns a function which can be called to get an instance of SpectatorHttp with the following properties:

  • controller - A proxy for Angular HttpTestingController
  • httpClient - A proxy for Angular HttpClient
  • service - The service instance
  • inject() - A proxy for Angular TestBed.inject()
  • expectOne() - Expect that a single request was made which matches the given URL and it's method, and return its mock request

Global Injections

It's possible to define injections which will be available for each test without the need to re-declare them in each test:

// test.ts
import { defineGlobalsInjections } from '@ngneat/spectator';
import { TranslocoModule } from '@ngneat/transloco';

defineGlobalsInjections({
  imports: [TranslocoModule],
});

Please be aware, that defineGlobalsInjections() must be called before the modules are loaded. In the default Angular test.ts this means before this line:

context.keys().map(context);

Component Providers

By default, the original component providers (e.g. the providers on the @Component) are not touched.

However, in most cases, you want to access the component's providers in your test or replace them with mocks.

For example:

@Component({
  template: '...',
  providers: [FooService]
})
class FooComponent {
  constructor(private fooService: FooService} {}

  // ...
}

Use the componentProviders to replace the FooService provider:

const createComponent = createComponentFactory({
  component: FooComponent,
  componentProviders: [
    {
      provide: FooService,
      useValue: someThingElse
    }
  ]
})

Or mock the service by using componentMocks:

const createComponent = createComponentFactory({
  component: FooComponent,
  componentMocks: [FooService]
});

To access the provider, get it from the component injector using the fromComponentInjector parameter:

spectator.inject(FooService, true)

In the same way you can also override the component view providers by using the componentViewProviders and componentViewProvidersMocks.

The same rules also apply to directives using the directiveProviders and directiveMocks parameters.

Custom Matchers

expect('.zippy__content').not.toExist();
expect('.zippy__content').toHaveLength(3);
expect('.zippy__content').toHaveId('id');
expect(spectator.query('.zippy')).toHaveAttribute('id', 'zippy');
expect(spectator.query('.zippy')).toHaveAttribute({id: 'zippy'});
expect(spectator.query('.checkbox')).toHaveProperty('checked', true);
expect(spectator.query('.img')).toHaveProperty({src: 'assets/myimg.jpg'});
expect(spectator.query('.img')).toContainProperty({src: 'myimg.jpg'});

// Note that toHaveClass accepts classes only in strict order. If order is irrelevant, disable strict mode manually.
expect('.zippy__content').toHaveClass('class');
expect('.zippy__content').toHaveClass('class-a, class-b');
expect('.zippy__content').not.toHaveClass('class-b, class-a');
expect('.zippy__content').toHaveClass(['class-a', 'class-b']);
expect('.zippy__content').not.toHaveClass(['class-b', 'class-a']);

expect('.zippy__content').toHaveClass('class', { strict: false });
expect('.zippy__content').toHaveClass('class-a, class-b', { strict: false });
expect('.zippy__content').toHaveClass('class-b, class-a', { strict: false });
expect('.zippy__content').toHaveClass(['class-b', 'class-a'], { strict: false });
expect('.zippy__content').toHaveClass(['class-b', 'class-a'], { strict: false });

// Note that toHaveText only looks for the existence of a string, not if the string is exactly the same. If you want to verify that the string is completely the same, use toHaveExactText.
// Note that if you want to verify that the string is completely the same, but trimmed first, use toHaveExactTrimmedText.
// Note that if you pass multiple values, Spectator checks the text of each array element against the index of the element found. 
expect('.zippy__content').toHaveText('Content');
expect('.zippy__content').toHaveText(['Content A', 'Content B']);
expect('.zippy__content').toHaveText((text) => text.includes('..'));
expect('.zippy__content').toContainText('Content');
expect('.zippy__content').toContainText(['Content A', 'Content B']);
expect('.zippy__content').toHaveExactText('Content');
expect('.zippy__content').toHaveExactText(['Content A', 'Content B']);
expect('.zippy__content').toHaveExactTrimmedText('Content');
expect('.zippy__content').toHaveExactTrimmedText(['Content A', 'Content B']);
expect('.zippy__content').toHaveValue('value');
expect('.zippy__content').toContainValue('value');

// Note this looks for multiple elements with the class and checks the value of each array element against the index of the element found
expect('.zippy__content').toHaveValue(['value a', 'value b']);
expect('.zippy__content').toContainValue(['value a', 'value b']);
expect(spectator.element).toHaveStyle({backgroundColor: 'rgba(0, 0, 0, 0.1)'});
expect('.zippy__content').toHaveData({data: 'role', val: 'admin'});
expect('.checkbox').toBeChecked();
expect('.checkbox').toBeIndeterminate();
expect('.button').toBeDisabled();
expect('div').toBeEmpty();
expect('div').toBeHidden();
expect('element').toBeSelected();
// Notice that due to restrictions within Jest (not applying actual layout logic in virtual DOM), certain matchers may result in false positives. For example width and height set to 0
expect('element').toBeVisible();
expect('input').toBeFocused();
expect('div').toBeMatchedBy('.js-something');
expect(spectator.component.object).toBePartial({ aProperty: 'aValue' });
expect('div').toHaveDescendant('.child');
expect('div').toHaveDescendantWithText({selector: '.child', text: 'text'});

Schematics

Generate component, service, and directive with Spectator spec templates with Angular Cli: (when using it as default)

Component

  • Default spec: ng g cs dashrized-name
  • Spec with a host: ng g cs dashrized-name --withHost=true
  • Spec with a custom host: ng g cs dashrized-name --withCustomHost=true

Service:

  • Default spec: ng g ss dashrized-name
  • Spec for testing http data service: ng g ss dashrized-name --isDataService=true

Directive:

ng g ds dashrized-name

Default Schematics Collection

To use spectator as the default collection in your Angular CLI project, add it to your angular.json:

ng config cli.defaultCollection @ngneat/spectator

The spectator schematics extend the default @schematics/angular collection. If you want to set defaults for schematics such as generating components with scss file, you must change the schematics package name from @schematics/angular to @ngneat/spectator in angular.json:

"schematics": {
  "@ngneat/spectator:spectator-component": {
    "style": "scss"
  }
}

Working Spectator & Jest Sample Repo and Karma Comparison

The examples in Karma from Angular docs testing developer guide has been reproduced in Spectator and Jest. (For convenience, this is the local version of the Karma examples.)

The Spectator & Jest version can be accessed here.

Core Team

Netanel Basal
Netanel Basal
Dirk Luijk
Dirk Luijk
Ben Elliott
Ben Elliott

Contributors

Thanks goes to these wonderful people (emoji key):~~~~


I. Sinai

๐Ÿ“– ๐Ÿ‘€ ๐ŸŽจ

Valentin Buryakov

๐Ÿ’ป ๐Ÿค”

Ben Grynhaus

๐Ÿ› ๐Ÿ’ป

Martin Nuc

๐Ÿ’ป

Lars Gyrup Brink Nielsen

๐Ÿ“ฆ โš ๏ธ

Andrew Grekov

๐Ÿ’ป ๐Ÿ”ง

Jeroen Zwartepoorte

๐Ÿ’ป

Oliver Schlegel

๐Ÿ’ป

Rex Ye

๐Ÿ”ง ๐Ÿ’ป

tchmura

๐Ÿ’ป

Yoeri Nijs

๐Ÿ’ป

Anders Skarby

๐Ÿ’ป

Gregor Woiwode

๐Ÿ’ป

Alexander Sheremetev

๐Ÿ› ๐Ÿ’ป

Mike

๐Ÿ’ป

Mehmet Erim

๐Ÿ“–

Brett Eckert

๐Ÿ’ป

Ismail Faizi

๐Ÿ’ป

Maxime

๐Ÿ“–

Jonathan Bonnefoy

๐Ÿ’ป

Colum Ferry

๐Ÿ’ป

Chris Cooper

๐Ÿ’ป

Marc Scheib

๐Ÿ“–

dgsmith2

๐Ÿ’ป

dedwardstech

๐Ÿ’ป ๐Ÿค”

tamasfoldi

๐Ÿ’ป ๐Ÿค”

Paolo Caleffi

๐Ÿ’ป

Toni Villena

๐Ÿ’ป

Itay Oded

๐Ÿ’ป

Guillaume de Jabrun

๐Ÿ’ป

Anand Tiwary

๐Ÿ’ป

Ales Doganoc

๐Ÿ’ป

Zoltan

๐Ÿ’ป

Vitalii Baziuk

๐Ÿ’ป

clementlemarc-certua

๐Ÿ’ป

Yuriy Grunin

๐Ÿ’ป

Andrey Chalkin

๐Ÿ’ป

Steven Harris

๐Ÿ’ป ๐Ÿ“–

Richard Sahrakorpi

๐Ÿ’ป

Dominik Kremer

๐Ÿ’ป

Mehmet Ozan Turhan

๐Ÿ’ป

Vlad Lashko

๐Ÿ’ป

William Tjondrosuharto

๐Ÿ’ป

Chaz Gatian

๐Ÿ’ป

Pavel Korobov

๐Ÿ’ป

Enno Lohmann

๐Ÿ’ป

Pawel Boguslawski

๐Ÿ’ป

Tobias Wittwer

๐Ÿ’ป ๐Ÿ“–

Mateo Tibaquirรก

๐Ÿ’ป

This project follows the all-contributors specification. Contributions of any kind welcome!

More Repositories

1

falso

All the Fake Data for All Your Real Needs ๐Ÿ™‚
TypeScript
3,098
star
2

transloco

๐Ÿš€ ๐Ÿ˜ The internationalization (i18n) library for Angular
TypeScript
1,856
star
3

until-destroy

๐ŸฆŠ RxJS operator that unsubscribe from observables on destroy
TypeScript
1,733
star
4

elf

๐Ÿง™โ€โ™€๏ธ A Reactive Store with Magical Powers
TypeScript
1,527
star
5

content-loader

โšช๏ธ SVG component to create placeholder loading, like Facebook cards loading.
TypeScript
733
star
6

hot-toast

๐Ÿž Smoking hot Notifications for Angular. Lightweight, customizable and beautiful by default.
TypeScript
687
star
7

cashew

๐Ÿฟ A flexible and straightforward library that caches HTTP requests in Angular
TypeScript
677
star
8

reactive-forms

(Angular Reactive) Forms with Benefits ๐Ÿ˜‰
TypeScript
609
star
9

tailwind

๐Ÿ”ฅ A schematic that adds Tailwind CSS to Angular applications
TypeScript
608
star
10

query

๐Ÿš€ Powerful asynchronous state management, server-state utilities and data fetching for Angular Applications
TypeScript
555
star
11

forms-manager

๐Ÿฆ„ The Foundation for Proper Form Management in Angular
TypeScript
517
star
12

error-tailor

๐Ÿฆ„ Making sure your tailor-made error solution is seamless!
TypeScript
478
star
13

helipopper

๐Ÿš A Powerful Tooltip and Popover for Angular Applications
TypeScript
392
star
14

nx-serverless

๐Ÿš€ The Ultimate Monorepo Starter for Node.js Serverless Applications
TypeScript
388
star
15

dialog

๐Ÿ‘ป A simple to use, highly customizable, and powerful modal for Angular Applications
TypeScript
371
star
16

hotkeys

๐Ÿค– A declarative library for handling hotkeys in Angular applications
TypeScript
325
star
17

edit-in-place

A flexible and unopinionated edit in place library for Angular applications
TypeScript
252
star
18

svg-icon

๐Ÿ‘ป A lightweight library that makes it easier to use SVG icons in your Angular Application
TypeScript
251
star
19

inspector

๐Ÿ•ต๏ธ An angular library that lets you inspect and change Angular component properties
TypeScript
218
star
20

dirty-check-forms

๐ŸฌDetect Unsaved Changes in Angular Forms
TypeScript
199
star
21

input-mask

๐ŸŽญ @ngneat/input-mask is an angular library that creates an input mask
TypeScript
199
star
22

lib

๐Ÿค– Lets you focus on the stuff that matters
TypeScript
180
star
23

transloco-keys-manager

๐Ÿฆ„ The Key to a Better Translation Experience
TypeScript
174
star
24

dag

๐Ÿ  An Angular service for managing directed acyclic graphs
TypeScript
153
star
25

bind-query-params

Sync URL Query Params with Angular Form Controls
TypeScript
147
star
26

from-event

๐ŸฆŠ ViewChild and FromEvent โ€” a Match Made in Angular Heaven
TypeScript
137
star
27

overview

๐Ÿค– A collection of tools to make your Angular views more modular, scalable, and maintainable
TypeScript
118
star
28

aim

Angular Inline Module Schematics
TypeScript
97
star
29

cmdk

Fast, composable, unstyled command menu for Angular. Directly inspired from pacocoursey/cmdk
TypeScript
91
star
30

copy-to-clipboard

โœ‚๏ธ Modern copy to clipboard. No Flash.
TypeScript
78
star
31

variabless

JS & CSS - A Match Made in Heaven ๐Ÿ’Ž
HTML
78
star
32

loadoff

๐Ÿคฏ When it comes to loaders, take a load off your mind...
TypeScript
78
star
33

effects

๐Ÿช„ A framework-agnostic RxJS effects implementation
TypeScript
63
star
34

avvvatars

Beautifully crafted unique avatar placeholder for your next angular project.
TypeScript
46
star
35

react-rxjs

๐Ÿ”Œ "Plug and play" for Observables in React Apps!
TypeScript
37
star
36

subscribe

Subscription Handling Directive
TypeScript
34
star
37

elf-ng-router-store

Bindings to connect Angular router to Elf
TypeScript
24
star
38

ng-standalone-nx

TypeScript
24
star
39

lit-file-generator

๐ŸŽ A lit generator for a component, directive, and controller.
JavaScript
19
star
40

storage

TypeScript
18
star
41

material-schematics

TypeScript
3
star
42

svg-icon-demo

TypeScript
1
star