25 integrationtest - ranhs/soda-test GitHub Wiki

Integration test (sometime called Component Test) is supported by Soda-Test from v2.1.0
It is only supported when using Karma for the tests, and is relevant only of Angular Components.

When testing an Angular component, you can (and should) test the component class, using unit-test. This type of test can validate the integrity of the component class. However unit-test does not validate the binding of the component class to the component HTML. for that you need to use Integration Test. This page explain how to write integration test to an Angular component, using Soda-test.

Testbed

The Angular component integration test is based on an Angular library called testbed. You can lookup the documentation of testbed, and you can use it from Soda-test by importing TestBed from Soda-Test and use it as if it was imported from @angular/core/testing

test.component.ts

import { Component } from '@angular/core'

@Component({
  selector: 'app-test',
  template: '<p>test.component works!</p>'
})
export class TestComponent {
}

test.component.spec.ts

import { describe, it, expect, TR, ComponentFixture, TestBed } from 'soda-test'
import { TestComponent } from './test.component'

@describe('Test Component')
class TestComponentTest {
  component: TestComponent
  fixture: ComponentFixture<TestComponent>

  beforeEach(): void {
    this.fixture = TestBed.createComponent(TestComponent)
    this.component = this.fixture.componentInstance
    this.fixture.detectChanges()
  }

  @it('should create')
  validateCreate(): TR {
    expect(this.component).to.exist
  }
}

However you can get more benefit from soda-test when using the soda-test types

Soda Fixture

The basic type for component integration testing is the ComponentFixture. You should use the SodaFixture type instead that is very similar, but have more benefits. Furthermore you don't have to call TestBed.createComponent method to get the fixture, you can using the @fixture decorator on the member variable to get the SodaFixture object, like when creating stub, spy or other sinons. Also, instead of getting the component instance from the fixture you can use the @component decorator to get the component. make sure to define the variable with the @component decorator right after the variable with the @fixture decorator, so the component instance will be of the component fixture.
@fixture(TestComponent)
fixture: SodaFixture<TestComponent>
@component(TestComponent)
component: TestComponent

In this case you don't need to create the fixture in the beforeEach method.

Alternatively, you can define the fixture and component as parameters to the test method:

validateCreate(
  @fixture(TestComponent) fixture: SodaFixture<TestComponent>, 
  @component(TestComponent) component: TestComponent
  ): TR

test.component.spec.ts

fixture & components as members
import { describe, it, expect, TR, SodaFixture, fixture, component } from 'soda-test'
import { TestComponent } from './test.component'

@describe('Test Component')
class TestComponentTest {

  @fixture(TestComponent)
  fixture: SodaFixture<TestComponent>
  @component(TestComponent)
  component: TestComponent

  @it('should create')
  validateCreate(): TR {
    expect(this.component).to.exist
  }
}

test.component.spec.ts

fixture & components as parameters
import { describe, it, expect, TR, SodaFixture, fixture, component } from 'soda-test'
import { TestComponent } from './test.component'

@describe('Test Component')
class TestComponentTest {
    @it('should create')
    validateCreate(
        @fixture(TestComponent) fixture: SodaFixture<TestComponent>, 
        @component(TestComponent) component: TestComponent
        ): TR {
      expect(component).to.exist
    } 
}

Fixture Options

The @fixture decorator has an optional second argument, the FixtureOptions. It is an object that may have any of the following properties (all properties are optional):
declarations - Array of component class types. If the created component have inner component, you should specify the classes of those components. In this case the inner components are created as part of the creation of the tested component. You can but don't need to specify the tested component class in this property array.
imports - Array of modules that needed to be imported in order to create the inner component / directives. For example, in order to use the ngModel directive, you need to import the FormsModule module.
inputs - Array of strings that are the names of the inputs of the test-component, you need to test. See Testing Component Inputs
outputs - Arrays of strings that are the names of the outpurs of the test-component, you need to test. See Testing Componet Outputs
events - Array of strings that are names of events an inner component may raise. See Simulate Events

addEvents() method

In order to simulate an event that an inner component may raise, soda-test need to known about those events (regardless of the components that may raise them). Some common events (like click and ngModelChanged) Already known to soda-test. To tell soda-test about more (custom) events you need to call the addEvents method (imported from soda-test), and pass its string arguments (as many as you like) that are names of protentional events. It doesn't heart to all any event-name, even if not used in the test. Alternative way to tell soda-test about potential events, it to put their names in the FixtureOptions parameter of the @fixture decorator in the events property.
See Simulate Events section.

SodaDebugElement

Each element (the test-component, inner components or event native html elements) has a SodaDebugElement object that represent them.
To get the SodaDebugElement of the tested-component use the debugElement property of the SodaFixture
SodaDebugElement is an extension of the DebugElement type defined in TestBed. Like TestBed DebugElement, it can be used to get information about the element, like properties/attributes/styles/classes and child elements. Note that the TestBed DebugElement also gives a way to query the HTML for more elements and also to "trigger and event-handler". Those actions are also possible using SodaDebugElement type, but with a little different API.

Finding inner Elements

You can get a SodaDebugElement object of an inner element you can use the query methods of the tested-component SodaDebugElement

query.by.css

This is the most useful way to get inner elements.
finding the element that has the "content" class:
const deContent = this.fixture.debugElement.query.by.css('.content')

Or you can use the short form of this method: queryByCss() that is defined on SodaFixture:

const deContent = this.fixture.queryByCss('.content')

find the element that has a property "id" that equals "a1":

const a1 = this.fixture.queryByCss('[id=a1]')

find the element that its name is "mycomponent":

const mc = this.fixture.queryByCss('mycomponent')

For example the following test validate the text of the only paragraph of the TextComponent given above:

@describe('Test Component')
class TestComponentTest {

  @fixture(TestComponent)
  fixture: SodaFixture<TestComponent>

  @it('have paragraph with expected text')
  validateParagraph(): TR {
    const p = this.fixture.queryByCss('p')
    expect(p.text).to.equal('test.component works!')
  }
}

queryAll.by.css

Same as query.by.css, but instead of returning the first element that matches the requested css, (or null if not found), it returns an array of SodaDebugElement(s) that matches the requested css.

Gets all elements with 'content' class:
const des = this.fixture.debugElement.queryAll.by.css('.content')

queryAllNodes.by.directive

Returns array of DebugNode(s) that have the given directive

Gets all nodes of elements that have the HighlightDirective directive
let nodes = this.fixture.debugElement.queryAllNodes.by.directive(HighlightDirective)

queryAllNodes.by.all

Return array of all the DebugNode(s).
nodes = this.fixture.debugElement.queryAllNodes.by.all()

detectChanges

Call the SodaFixture.detectChanges() after changing values in the component and before validating the value was rendered in the HTML
@it('should update text')
updateText(): TR {
  const p = this.fixture.queryByCss('p')
  this.component.title = "new text"
  this.fixture.detectChanges()
  expect(p.text).to.equal('new text')
}

Element Text

To get the text of element you should check the innerText of the nativeNode of it.
const p = this.fixture.queryByCss('p')
expect(p.nativeNode.innerText).to.equal('test.component works!')

Or you can just use the text property on the DebugElement to get the element text

expect(p.text).to.equal('test.component works!')

The text property can also be used when the ngModel directive is used

@Component({
  selector: 'app-test',
  template: '<input type="text" [(ngModel)]="title">'
})
export class TestComponent {
  title = 'test.component works!'
}
@it('should update text')
updateText(): TR {
  const tb = this.fixture.queryByCss('input')
  expect(tb.text).to.equal('test.component works!')
  this.component.title = "new text"
  this.fixture.detectChanges()
  expect(tb.text).to.equal('new text')
}

In above case (when ngModel is used, use can also use the text property to change the text in the model and validate the binding to the component

@it('should update from model')
updateFromModel(): TR {
  const tb = this.fixture.queryByCss('input')
  expect(tb.text).to.equal('test.component works!')
  console.log(typeof tb.nativeElement.innerText)
  tb.text = "text2"
  expect(this.component.title).to.equal('text2')
  this.fixture.detectChanges()
  console.log(tb.nativeElement)
}

Concater Example

In the following component there are 2 text-boxes and one div. The div always display the concatenated text of the 2 text-boxes:
@Component({
  selector: 'app-concater',
  template: `
    <input id="a1" type="text" [(ngModel)]="text1"><br>
    <input id="a2" type="text" [(ngModel)]="text2"><br>
    <div id="d1">{{text1}}{{text2}}</div>
  `
})
export class ConcaterComponent {
  text1=""
  text2=""
}

The following test, update the text of the 2 text-boxes, and validate the div text, then it updated the text of one of the text-boxes and validate the div text is updated accordantly.

@describe('Test Component')
class TestComponentTest {

  @fixture(ConcaterComponent,{imports: [FormsModule]})
  fixture: SodaFixture<ConcaterComponent>
  @component(ConcaterComponent)
  component: ConcaterComponent

  @it('should update text')
  updateText(): TR {
    const tb1 = this.fixture.queryByCss('[id=a1]')
    const tb2 = this.fixture.queryByCss('[id=a2]')
    const div = this.fixture.queryByCss('[id=d1]')
    tb1.text = "ABC"
    tb2.text = "123"
    this.fixture.detectChanges();
    expect(div.text).to.equal('ABC123')
    tb1.text = "ABCD"
    this.fixture.detectChanges();
    expect(div.text).to.equal('ABCD123')
  }
}

triggerEventHandler

If you want to simulate an event (e.g. a button click) you need on the SodaDebugElement object of the component that raise the event, to get the call a method on the triggerEventHandler. (Note that in the original testbed, the triggerEventHandler is the method itself). The method you call has the same name as the event (e.g. click) and may get the event object as argument. In the case of click you don't have to pass and argument.
button.triggerEventHandler.click()

The following component has a button that advance a count variable (not binded in the example)

@Component({
  selector: 'app-test',
  template: '<button (click)="onButtonClick()">'
})
export class TestComponent {
  count = 0
  onButtonClick() {
    this.count++
  }
}

The following text, validates that the click of the button, advance the counter variable

@it('advance the counter when clicking the button')
clickTheButton(): TR {
  expect(this.component.count).to.equal(0)
  const button = this.fixture.queryByCss('button')
  button.triggerEventHandler.click()
  expect(this.component.count).to.equal(1)
}

Custom Event

Let's say we want to test the following component:
@Component({
  selector: 'app-test',
  template: '<mycomponent (ievent)="onMyComponentEvent($event)">My Component</mycomponent>'
})
export class TestComponent {
  onMyComponentEvent(data: any) {
    // do somthing
  }
}

We want to test the ievent event that the mycomponent component defines. We want to validate that when mycomponent raises the ievent the method onMyComponentEvent is called with the relevant argument.
First we shall create a test code that define the tested-component and a method to write the test in:

@describe('Test Component')
class TestComponentTest {
  @fixture(TestComponent)
  fixture: SodaFixture<TestComponent>
  @component(TestComponent)
  component: TestComponent

  @it('call onMyComponentEvent() method of the event data when ievent is raised from mycomponent')
  eventWasCalled(): TR {
  }
}

Note that the tested-component need to create an inner component with the mycomponent selector, and since we did not specified it, it cannot do this. You can still write the test with this component missing, but you will get an error in the console during the test. To avoid this we shall create a stub component (Since we don't want to use the real component)

@Component({selector: 'mycomponent'})
class MyComponentStub {
}

To make the test-component use the stub-component, we need to specified its class when defining the fixture:

  @fixture(TestComponent, {
    declarations: [MyComponentStub]
  })
  fixture: SodaFixture<TestComponent>

In the test method, we shall first find the mycomponent DebugElement:

    const mycomponent = this.fixture.queryByCss('mycomponent')

Now, we wants to simulate the ievent event. This is done by calling a method of that name on the triggerEventHandler property

    mycomponent.triggerEventHandler.ievent("test-data")

However this code cannot work in TypeScript as written above, since the property triggerEventHandler does not contains ievent method.
to solve that we can define a new interface that shall hold the custom event(s) we need to test. This interface may extends the CommonEvents defined by soda-test, but it doesn't have to.

interface MyEvents extends CommonEvents {
  ievent(data: any): void
}

Now we need to tell soda-test to use this interface instead of the default CommonEvents. There are 2 options to do this:

  1. on the fixture declaration use this interface as a second generic argument (this is the recommended way):
fixture: SodaFixture<TestComponent, MyEvents>
  1. or you can pass it to the method that finding the component to test (all the method can except this as generic argument):
    const mycomponent = this.fixture.queryByCss<MyEvents>('mycomponent')

This shall solve the TypeScript problem so we can call the ievent method, but this method still does not exists in runtime.
To add more methods for raising custom event you can do one of the followings:

  1. call the addEvent methods outside of any class and pass it the name(s) of all custom events
addEvents('ievent')
  1. or you can pass those event names when defining the fixture (this is the recommended way)
  @fixture(TestComponent, {
    declarations: [MyComponentStub],
    events: ['ievent']
  })
  fixture: SodaFixture<TestComponent, MyEvents>

Now we can simulate the event as written above.
How shall we check if the onMyComponentEvent was called. We can test its side effects, if it has ones and ones that we can check, but a better way is to define a spy on this method. This is a method of the TestComponent class, and it is defined on its prototype. In this example we shall define the spy as an argument to the test methods:

eventWasCalled( @spy(TestComponent.prototype, 'onMyComponentEvent') onMyComponentEventSpy: SinonSpy): TR {

Now we can use the spy argument to validate the method was called, with the right argument:

  expect(onMyComponentEventSpy).to.have.been.calledWithExactly('test-data')

This is the full test code:

test.component.spec.ts

import { describe, it, expect, TR, SodaFixture, fixture, component, CommonEvents, spy, SinonSpy } from 'soda-test'
import { TestComponent } from './test.component'
import { Component } from '@angular/core'

interface MyEvents extends CommonEvents {
  ievent(data: any): void
}

@Component({selector: 'mycomponent'})
class MyComponentStub {
}

@describe('Test Component')
class TestComponentTest {

  @fixture(TestComponent, {
    declarations: [MyComponentStub],
    events: ['ievent']
  })
  fixture: SodaFixture<TestComponent, MyEvents>
  @component(TestComponent)
  component: TestComponent

  @it('call onMyComponentEvent() method of the event data when ievent is raised from mycomponent')
  eventWasCalled( @spy(TestComponent.prototype, 'onMyComponentEvent') onMyComponentEventSpy: SinonSpy): TR {
    const mycomponent = this.fixture.queryByCss('mycomponent')
    mycomponent.triggerEventHandler.ievent("test-data")
    expect(onMyComponentEventSpy).to.have.been.calledWithExactly('test-data')
  }
}

Custom Attributes

Let's say we want to test the following component:
@Component({
  selector: 'app-test',
  template: '<mycomponent [ikey]="mycomponentkey">My Component</mycomponent>'
})
export class TestComponent {
    mycomponentkey: string
}

We want to test the ikey attribute of the mycomopnent element. We want to validate that the mycomponentkey member is binded to the ikey attribute of the mycomponent element.
For this test we must have a component for the mycomponent element. We can use the real component, or use a stub component, as in the previous example. We shall use the stub component in this example too.

@Component({selector: 'mycomponent'})
class MyComponentStub {
}

@describe('Test Component')
class TestComponentTest {

  @fixture(TestComponent, {
    declarations: [MyComponentStub]
  })
  fixture: SodaFixture<TestComponent>
  @component(TestComponent)
  component: TestComponent

  @it('binds the mycomponentkey member to the ikey attribute on mycomponent')
  attributeWasUpdated(): TR {
  }
}

We are getting an error: Can't bind to 'ikey' since it isn't a known property of 'mycomponent'
This time we must solve this, or we cannot test the ikey attribute
To solve this we shall define an Input in the stub element:

@Component({selector: 'mycomponent'})
class MyComponentStub {
  @Input()
  ikey: string
}

Now the property is binded, and we can start writing the test.
We shall start by finding the mycomponent DebugElement:

    const mycomponent = this.fixture.queryByCss('mycomponent')

And set a test value to the binded member of the main component object:

    this.component.mycomponentkey = "test-key-value"

We need to call detectChanges so the new value shall be propagated to the stub component

    this.fixture.detectChanges()

to get the value of the ikey attribute, use the attributres property of the mycomponent debug element

    expect(mycomponent.attributes.ikey).to.equal('test-key-value')

Note that we are checking the ikey attribute. This is the name of the member in the inner components (stub component in our case). The easiest way is to give it the same name as property in the HTML. If for some reason it has a different name, you should use the name of the member in the inner component class.
The full test code:

test.compoenent.spec.ts

import { describe, it, expect, TR, SodaFixture, fixture, component } from 'soda-test'
import { TestComponent } from './test.component'
import { Component, Input } from '@angular/core'


@Component({selector: 'mycomponent'})
class MyComponentStub {
  @Input()
  ikey: string
}

@describe('Test Component')
class TestComponentTest {

  @fixture(TestComponent, {
    declarations: [MyComponentStub]
  })
  fixture: SodaFixture<TestComponent>
  @component(TestComponent)
  component: TestComponent

  @it('binds the mycomponentkey member to the ikey attribute on mycomponent')
  attributeWasUpdated(): TR {
    const mycomponent = this.fixture.queryByCss('mycomponent')
    this.component.mycomponentkey = "test-key-value"
    this.fixture.detectChanges()
    expect(mycomponent.attributes.ikey).to.equal('test-key-value')
  }
}

Testing Component Inputs

The tested component may have inputs of its own.
let's look at this exapmle:
@Component({
  selector: 'app-test',
  template: '<div>{{t1}}</div>'
})
export class TestComponent {
    @Input()
    t1: string
}

The component has an input called t1 that is bind to the t1 member of the component instance. This text is also display in the (only) div that this component has.
To test inputs of the tested component we need to specified in the fixture-options that inputs property, with the value of an array of string representing the inputs of the tested component.
Note that in this example the name of the input is the same as the name of the member variable, if the names are different, you need to specify the name of the input, not the name of the member variable.

@fixture(TestComponent, {inputs: ['t1']})
fixture: SodaFixture<TestComponent>

In the test code you can set the value of the input, by using the property inputs on the SodaFixture object, and set the property with the name of the input on the inputs property

this.fixture.inputs.t1 = 'TestText'

After calling detectChanges you can validate for example the value was saved in the member variable:

@it('should bind the t1 input to the t1 member')
validateInput(): TR {
  this.fixture.inputs.t1 = 'TestText'
  this.fixture.detectChanges();
  expect(this.component.t1).to.equal('TestText')
}

Or you can validate that the text of the input (in this example) is displayed in the div

@it('should have the text of the t1 input')
validateInputInView(): TR {
  this.fixture.inputs.t1 = 'Text to Display'
  this.fixture.detectChanges();
  const div = this.fixture.queryByCss('div')
  expect(div.text).to.equal('Text to Display')
}

Testing Component Outputs

The tested component may also have outputs of its own.
let's look at this example:
@Component({
  selector: 'app-test',
  template: `<input type="text" [(ngModel)]="text1"><br>
             <button (click)="onSend()">Send</button>`
})
export class TestComponent {
    @Output()
    t1 = new EventEmitter<string>()

    text1: string

    onSend() {
        this.t1.emit(this.text1)
    }
}

The component has a text-box and a Send button. When clicking on the Send Button the text in the edit box is emitted to the component output t1. Note that the output t1 is bind to the component instance member variable t1.
To test outputs of the tested component we need to specified in the fixture-options that outputs property, with the value of an array of string representing the outputs of the tested component.
Note that in this example the name of the output is the same as the name of the member variable, if the names are different, you need to specify the name of the output, not the name of the member variable.

@fixture(TestComponent, {outputs: ['t1']})
fixture: SodaFixture<TestComponent>

In the test code you can register on the output, by using the property events on the SodaFixture object. This property is an EventEmitter. Call the on method on it, passing it the name of the output and a method that shall be called when the output is emitted.

const t1Calls: string[] = []
this.fixture.events.on('t1', (data: string)=> t1Calls.push(data))

In this example we save the calls on the output to the t1Calls array, to be validated latter on.
To simulate the output emit, you can call emit on the component EventEmitter member variable:

@it('should bind the t1 member event to the t1 output')
validateOutput(): TR {
  const t1Calls: string[] = []
  this.fixture.events.on('t1', (data: string)=> t1Calls.push(data))
  this.component.t1.emit('DumyEventData')
  expect(t1Calls).to.deep.equal(['DumyEventData'])
}

Or, in this example, you can set the text-box value, push the Send button, and then validate that the output event was emitted:

@it('should send the text from the text-box to the output')
validateOutputInView(): TR {
  const t1Calls: string[] = []
  this.fixture.events.on('t1', (data: string)=> t1Calls.push(data))
  const tb = this.fixture.queryByCss('input')
  const button = this.fixture.queryByCss('button')
  tb.text = 'Text-Box Text'
  this.fixture.detectChanges()
  button.triggerEventHandler.click()
  expect(t1Calls).to.deep.equal(['Text-Box Text'])
}

⚠️ **GitHub.com Fallback** ⚠️