1.2.1 • Published 12 months ago

ng-unit v1.2.1

Weekly downloads
34
License
MIT
Repository
github
Last release
12 months ago

ng-unit · GitHub license npm version Build Status Build Status Coverage Status

The boilerplate reducing test utility for Angular. Supports Angular 6 and greater, and running tests in in Chrome, Firefox, Edge, IE11, and Node (via JSDOM).

What is ng-unit?

ng-unit seeks to simplify unit testing of Angular components by providing automated mocking of child components, streamlined test setup, and easier DOM interaction to drastically the amount of boilerplate code needed.

An example

Suppose we want to mock out the child component used in the below component so we can assert that the component under test binds the correct value to its input.

@Component({
    selector: "parent",
    template: `<child [input]="boundToInput"></child>`,
})
class ComponentUnderTest {
    public boundToInput: string
}

Normally you would have to do something like this:

import {Component, Input, Output} from "@angular/core"
import {TestBed} from "@angular/core/testing"
import {By} from "@angular/platform-browser"

it("sets the child components input", () => {
    @Component({ selector: "child" })
    class MockChildComponent {
        @Input() private input: string
    }
    
    TestBed.configureTestingModule({
        declarations: [ComponentUnderTest, MockChildComponent],
    })
    
    const fixture = TestBed.createComponent(ComponentUnderTest)
    const subject = fixture.componentInstance
    fixture.detectChanges()
    
    subject.boundToInput = "foo"
    fixture.detectChanges()
    
    const component = fixture.debugElement.query(By.css("child")).componentInstance
    expect(component.input).to.equal("foo")
})

With ng-unit this simply becomes:

import {testComponent, detectChanges, component} from "ng-unit"

it("sets the child components input", () => {
    const subject = testComponent(ComponentUnderTest)
        .mock([ChildComponent])
        .begin()
    
    subject.boundToInput = "foo"
    detectChanges()
    
    expect(component(ChildComponent).input).to.equal("foo")
})

Installation

npm install --save-dev ng-unit

Setup

If you are using jasmine for mocking then no setup is needed. ng-unit will automatically use spys when it needs to mock methods. If you don't use jasmine for mocking, then you will need to register a provider for mocks before you begin your tests.

For example to use sinon stubs you would need to do the following before your tests

import {mockProvider} from "ng-unit"

mockProvider(() => sinon.stub())

In an Angular CLI app you would put this in test.ts

ng-units documentation uses sinon stubs and chai assertions in all of it's examples

Guide

Basic Testing

A simple test

ng-unit greatly simplifies setup and mocking for Angular TestBed tests. In the simplest scenario you simply need to pass the component to be tested to testComponent() and invoke .begin() to instantiate your component. You can then use element() to query the DOM for elements.

import {testComponent, element} from 'ng-unit'

 @Component({
    selector: "tested",
    template: `<span id="greeting">Hello World</span>`
})
class SubjectComponent { }

it("has a greeting message", () => {
  testComponent(SubjectComponent).begin()
  expect(element("#greeting")).to.have.text("Hello World")
});

You can also select multiple elements with elements('.selector').

Simulating events

You can simulate DOM events by using trigger().

import {testComponent, element, trigger, detectChanges} from "ng-unit"

@Component({
    selector: "tested",
    template: `<button (click)="clicked = true">Click Me</button>`
})
class SubjectComponent {
    public clicked = false
}

it("fires a click event handler", () => {
  const subject = testComponent(SubjectComponent).begin()

  trigger(element('input'), 'click')
  detectChanges()

  expect(subject.clicked).to.be.true
})

Additionally you can optionally pass an object with properties to be added to the event object.

trigger(element('input'), 'keydown', { charCode: 13 })

Setting inputs element values

Value setter convenience methods for DOM inputs are provided. They automatically fire the appropriate change/input events on the input being set.

setTextInputValue(element("input[type=text]"), "Sasquatch") //Text field now has value "Sasquatch"
setTextAreaValue(element("textarea"), "Sasquatch") //Text area now has value "Sasquatch"
setCheckboxValue(element("input[type=check]"), true) //Checkbox is now checked
setRadioButton(element("input[type=radio]"), true) //Radio button is now selected
setSelectValue(element("select"), "Hancock") //Dropdown list now has the value "Hancock" selected
setSelectIndex(element("select"), 1) //Dropdown list now has the second option selected

These work with any DOM element reference, not just those returned by ng-units selection methods. They can be used in traditional TestBed tests if desired.

Select lists and [ngValue]

Using [ngValue] for select list option bindings complicates setting a value slightly. The bound values can be things other than strings, and consequently the value is not bound to the DOM. This makes determining which option to select more complicated.

If you know the index of the option you want you can use setSelectIndex(element("select"), index) to select the value

If you have access to a list of all the objects that are bound as option values you can use setSelectFromOptions() to select the value:

    const listOptions = [{ message: "Hello"}, { message: "Goodbye"}]
    setSelectFromOptions(element("select"), { message: "Goodbye"}, listOptions)

Setting component inputs

Initial values for component inputs can be set prior to component instantiation (so they are properly present at OnInit time) with the test builder method.setInput().

testComponent(SubjectComponent)
    .setInput("label", "presents")
    .begin()

Once .begin() is called you can change the input value with the setInput() function.

import {testComponent, setInput} from "ng-unit"

testComponent(SubjectComponent)
    .setInput("label", "fizz")
    .begin()

setInput("label", "buzz")

Unlike directly setting input properties on the component under test directly, using setInput will properly trigger lifecycle methods such as ngOnChanges(). Take note that, in order to change an input after after .begin() is called you must have given it an initial value while setting up the test.

Watching component outputs

Component outputs can be watched prior to component instantiation (so values emitted at OnInit time are not missed) with .onOutput().

testComponent(SubjectComponent)
    .onOutput("save", event => persist(event))
    .begin()

Once .begin() is called you can add new output watches with onOutput()

import {testComponent, onOutput} from "ng-unit"

testComponent(SubjectComponent)
    .onOutput("save", event => persist(event))
    .begin()

onOutput("save", event => console.log(event))

Providing providers

Providers for services and other things can be registered with .providers()

testComponent(SubjectComponent)
    .providers([
      { provide: FooService, useValue: mockFooService },
      { provide: BarService, useValue: new BarService() },
    ])
    .begin()

Importing other modules

Other modules that your component under test depends upon can be imported using .import()

testComponent(SubjectComponent)
  .import([FormsModule, ReactiveFormsModule])
  .begin()

Using schemas

Schemas can be registered with .schemas()

testComponent(SubjectComponent)
    .schemas([CUSTOM_ELEMENTS_SCHEMA])
    .begin()

Mocking child components

Child components can be mocked during test setup with .mock(). When mocked a component will have a blank template and require none of it's normal imports, providers, or child components to be registered for the test. This isolates your tests from needing any knowledge of the children beyond what inputs you provide them, what outputs you subscribe to, and any methods you call on the children directly.

import {testComponent, element, detectChanges} from "ng-unit"

@Component({
    selector: "tested",
    template: `
      <child-component [someInput]=""></child-component>
    `
})
class SubjectComponent {
}

it("renders transcluded content", () => {
  testComponent(SubjectComponent)
    .mock([ChildComponent])
    .begin()

  expect(component("#transcluded").someInput).to.equal("")
})

By default uses sinon for mocking functions. If you use Jasmine or another mocking library you can provide a factory for your own mocks using mockProvider().

Interacting with mocked components

Child components can be selected with the component() and components() functions. You can query for children using either CSS selector of the Component type.

import {testComponent, element} from 'ng-unit'

@Component({
    selector: "child",
    template: `<span">{{message}}</span>`
})
class ChildComponent { 
    @Input() public message: string
}

@Component({
    selector: "tested",
    template: `<child class="greeting" [message]="greeting"></child>`
})
class SubjectComponent { 
    private greeting = "Hello World!"
}

it("has a greeting message", () => {
  testComponent(SubjectComponent)
      .mock([ChildComponent])
      .begin()
      
  expect(component(ChildComponent).greeting).to.equal("Hello World!")
  expect(component(".greeting").greeting).to.equal("Hello World!")
})

Mock components have properties that correspond to their real versions to inputs, outputs, and methods.

You can assert that an input was set to a value by selecting the mock and asserting on the input property value.

expect(component(ChildComponent)).greeting.to.equal("Hello World")

You can cause the mock child to emit and output by selecting the component and using the output event emitter that is created on the mock.

component(ChildComponent).someOutput.emit("foo")

You can cause the mock child to emit and output by selecting the component and asserting on the mocked method.

expect(component(ChildComponent).someMethod).to.have.been.calledWith("bar")

Mocked methods can be setup before the component under test is instantiated, so you can set their initial return values.

testComponent(SubjectComponent)
  .mock([FooComponent])
  .setupMock(FooComponent, fooMock => fooMock.getValue.returns("cake"))
  .begin()

Mocked components and transclusion

Mocked components automatically render and transcluded content so you can assert against it.

import {testComponent, element, detectChanges} from "ng-unit"

@Component({
    selector: "tested",
    template: `
      <child-component>
        <span id="transcluded">This is transcluded!</span>
      </child-component>
    `
})
class SubjectComponent {
}

it("renders transcluded content", () => {
  testComponent(SubjectComponent).begin()

  expect(element("#transcluded")).to.have.text("This is transcluded!")
})

Custom mock providers

By default ng-unit uses sinon stubs for mocking functions. You can configure your own mock provider if you prefer to use Jasmine spys or another mocking framework.

import {mockProvider} from "ng-unit"

mockProvider(() => jasmine.createSpy())

Using real child components

If you want your test to utilize a real instances of child components configure them with .use(). This can be useful for doing integration tests that test numerous components. Take note that using a real child component also requires you to register any imports, providers, and child components the component uses just like you were setting up a traditional test bed test.

testComponent(SubjectComponent)
  .use([FooComponent, BarComponent])
  .begin()

Falling back to TestBed functionality

In the event that ng-unit does not allow you to test something in the desired way you can always fall back to TestBed functionality by accessing the component fixture using fixture().

import {testComponent, fixture} from "ng-unit"

it("allows accessing the component fixture", () => {
  testComponent(SubjectComponent).begin()
  
  fixture().autoDetectChanges(true);
})

Usage without test setup

Even if you don't wish to use ng-units test setup, you can still take advantage of it's mocking, component selection, and assignment functionality.

Since setTextInputValue() and the other input setting functions use DOM elements, it allows you to use elements selected using test beds selection methods.

import {setTextInputValue} from "ng-unit"

const input = fixture.debugElement.query(By.css("input")).nativeElement
setTextInputValue(input, "foo")

Real or mocked child components can be selected even when not using testComponent() by utilizing the selectComponent() and selectComponents() functions and providing the test fixture.

import {testComponent, testComponents} from "ng-unit"

const fixture = TestBed.createComponent(ComponentUnderTest)
const singleComponent: MessageComponent = selectComponent(MessageComponent, fixture)
const multipleComponents: Array<MessageComponent>  = selectComponents(MessageComponent, fixture)

Mocking components can be accomplished by using the mockComponent() function.

import {mockComponent} from "ng-unit"

TestBed.configureTestingModule({
    declarations: [ComponentUnderTest, mockComponent(ChildCmponent)],
})

Thanks to

SauceLabs for generously providing our platform for cross browser testing

1.2.1

12 months ago

1.2.0

4 years ago

1.1.1

4 years ago

1.1.0

4 years ago

1.0.0

6 years ago

0.11.0

6 years ago

0.10.0

6 years ago

0.9.0

6 years ago

0.8.12

6 years ago

0.8.11

6 years ago

0.8.10

6 years ago

0.7.10

6 years ago

0.7.9

6 years ago

0.7.8

6 years ago

0.7.7

6 years ago

0.7.6

6 years ago

0.6.6

6 years ago

0.5.6

6 years ago

0.5.5

6 years ago

0.4.5

6 years ago

0.4.4

6 years ago

0.4.3

6 years ago

0.4.2

6 years ago

0.4.0

6 years ago

0.3.0

6 years ago

0.2.0

6 years ago

0.1.0

6 years ago

0.0.2

6 years ago

0.0.1

6 years ago