15
votes

With Angular 4 it is possible to test the Component's template, for example checking if clicking a button triggers the expected method and stuff like that.

But how can templates be included in the test coverage? By default they are not (using Angular CLI tests with Karma+Jasmine+Istanbul).

3
If you are using TestBed by angular testing then components are also included in the testing and you can test them individually...Its more of an integration test instead of creating your components with new keyword.Karan Garg
it doesn't measure the test of my ngIfs and ngFors, does it?ganqqwerty

3 Answers

1
votes

Did you by any chance mean that you want to test the actual representation of the templates? Then you have to completely switch to jest instead of karma/jasmine combination. With jest you can generate template snapshots in your tests which are pushed alongside the component and checked on CI tests.

For example let's say you have a main.component which shows a loader while a SessionService is not ready and then the router-outlet for route content when it's done. A jest snapshot would look somewhat like this:

exports[`MainComponent renders: default state 1`] = `
<main
  isReady$={[Function BehaviorSubject]}
  sessionService={[Function Object]}
>
  <loader-line
    ng-reflect-active="true"
  />
</main>
`;

exports[`MainComponent session storage is ready renders 1`] = `
<main
  isReady$={[Function BehaviorSubject]}
  sessionService={[Function Object]}
>
  <main>
    <router-outlet />
  </main>
</main>
`;

And the test code looks like that:

describe('MainComponent', () => {
  let fixture: ComponentFixture<MainComponent>;

  const isReady$ = new BehaviorSubject(false);
  const mockSessionStorage = Mock.all<SessionService>();

  beforeEach(async(() => {
    TestBed.configureTestingModule({
      declarations: [MainComponent, MockComponents(LoaderLineComponent)],
      imports: [RouterTestingModule],
      providers: [mockWith(SessionService, mockSessionStorage)],
    })
      .compileComponents();
  }));

  afterAll(() => {
    isReady$.complete();
  });

  beforeEach(() => {
    Mock.extend(mockSessionStorage).with({isReady$});
    fixture = TestBed.createComponent(MainComponent);
    detectChanges(fixture);
  });

  it(`creates instance`, () => expect(fixture.componentInstance).toBeTruthy());

  it(`renders`, () => expect(fixture).toMatchSnapshot(`default state`));

  describe(`session storage is ready`, () => {
    beforeEach(() => {
      isReady$.next(true);
      detectChanges(fixture);
    });

    it(`renders`, () => expect(fixture).toMatchSnapshot());
  });
});

That's it, no more querying of <loader-line> or <router-outlet> in the spec file code, just look at the snapshot and you're done.

Note: I'm also using ng-mocks, ts-mockery and some own util functions, but the main thing for you to look for are the expect(fixture).toMatchSnapshot() lines which are jest native.

0
votes

In my opinion, you can only test the function that calls when you click, because you don't need to verify that if you click angular will call this function.

But if you want to test it any way you can do it like that

import { TestBed, async, ComponentFixture } from '@angular/core/testing';

describe('', () => {
  let fixture: ComponentFixture<TestComponent>;
  let component: TestComponent;

  beforeEach(async(() => {
    TestBed.configureTestingModule({
      imports: [ ],
      declarations: [ TestComponent ],
      providers: [  ]
    }).compileComponents().then(() => {
      fixture = TestBed.createComponent(TestComponent);
      component = fixture.componentInstance;
    });
  }));
});

it('should click on button', async(() => {
  spyOn(component, 'onEditButtonClick');

  let button = fixture.debugElement.nativeElement.querySelector('button');
  button.click(); 
// here you can test you code or just check that your function have been called like in the example bellow 

  fixture.whenStable().then(() => {
    expect(component.onEditButtonClick).toHaveBeenCalled();
  });
}));
0
votes

In my opinion you should re-think what you want to test and how. You can test if something has being triggered from inside the component (unit tests) like if I call this function, then this property change the value.

export class Page {
  listOfSomething: string[] = [];

  addStuff: (item: string) => this.listOfSomething.push(item);
}

Here you can test that the listOfSomething changes over time.

To know if a button do that from the button in your template then you might have this situation

<p *ngFor="let item of listOfSomething">{{ item }}<p>

<button (click)="addStuff('stuff')">add stuff</button>

In this case you want to see that the number of elements on the screen changes if you click the button. Basically you are checking addStuff and listOfSomething indirectly, but still checking them.

--

Over all you need to split your tests into unit tests and e2e tests. Jasmine is more for unit tests. you might find a way but it's not worth the time.

Below you can see the different approach you need to have for a login page (e2e).

import { browser, by, element } from 'protractor';

export const testData = {
  userName: 1231232,
  password: 'pass1234'
};

export class LoginPage {
  //////////////////////////////////////////////////////////////////////////////////////////
  // navigate //////

  navigateTo() {
    return browser.get('/login');
  }

  quickLogin() {
    this.navigateTo();
    this.setMobileNumber(testData.userName);
    this.setPassword(testData.password);
    this.doLogin();
  }

  //////////////////////////////////////////////////////////////////////////////////////////
  // selectors /////

  getLoginButton() {
    return element(by.buttonText('Log in'));
  }

  //////////////////////////////////////////////////////////////////////////////////////////
  // getText ///////

  getSelectedMobileNumberPrefix() {
    return element(by.deepCss('section form .mat-select .value-output')).getText();
  }

  getErrorMessage() {
    return element(by.deepCss('snack-bar-container simple-snack-bar span')).getText();
  }

  //////////////////////////////////////////////////////////////////////////////////////////
  // sendKeys //////

  setMobileNumber(phoneNumber: number) {
    return element(by.css('[formControlName=phoneNumber]')).sendKeys(phoneNumber);
  }

  setPassword(password: string) {
    return element(by.css('[formControlName=password]')).sendKeys(password);
  }

  //////////////////////////////////////////////////////////////////////////////////////////
  // click /////////

  doLogin() {
    return this.getLoginButton().click();
  }
}
import { browser, protractor } from 'protractor';

import { DashboardPage } from '../dashboard/dashboard.po';
import { LoginPage } from './login.po';

const password = {
  correct: 'pass1234',
  wrong: 'xxx',
};

const testData = {
  maleUser: 1231232,
  femaleUser: 1231231,
};

describe('Login Journey', () => {
  const EC = protractor.ExpectedConditions;
  let dashboardPage: DashboardPage;
  let loginPage: LoginPage;

  beforeEach(() => {
    dashboardPage = new DashboardPage();
    loginPage = new LoginPage();
  });

  afterEach(() => {
    browser.executeScript('window.localStorage.clear();');
  });

  it('Checking init page', () => {
    loginPage.navigateTo();

    expect(loginPage.getSelectedMobileNumberPrefix()).toContain('+60');
    expect(loginPage.getLoginButton().isEnabled()).toBeFalsy();

    loginPage.setMobileNumber(testData.maleUser);
    loginPage.setPassword(password.correct);

    expect(loginPage.getLoginButton().isEnabled()).toBeTruthy();
    expect(loginPage.getSelectedMobileNumberPrefix()).toContain('+60');
  });

  it('I should be able to login', () => {
    loginPage.navigateTo();

    loginPage.setMobileNumber(testData.maleUser);
    loginPage.setPassword(password.correct);
    loginPage.getLoginButton().click();

    browser.waitForAngular();

    expect(dashboardPage.getTitle()).toContain('Dashboard');
  });

  it('I should NOT be able to login with incorrect credentials', () => {
    loginPage.navigateTo();

    loginPage.setMobileNumber(testData.maleUser);
    loginPage.setPassword(password.wrong);
    loginPage.getLoginButton().click();

    browser.waitForAngular();

    expect(loginPage.getErrorMessage()).toContain('The email address or the password you inputted is incorrect.');
  });
});