1

I have created a basic template application with Angular 6 and I am trying to get Stryker Mutation testing working on it. On a basic home page:

import { Component } from '@angular/core';

/**
* Home Page Definition
*/
@Component({
    selector: 'app-home',
    templateUrl: 'home.page.html',
    styleUrls: ['home.page.scss']
})
export class HomePage {}

I have a basic test file for this page:

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

import { HomePage } from './home.page';

/**
* Home Page Test File
*/
describe('HomePage', () => {
    let component: HomePage;
    let fixture: ComponentFixture<HomePage>;

    beforeEach(async(() => {
        TestBed.configureTestingModule({
            declarations: [HomePage],
            schemas: [CUSTOM_ELEMENTS_SCHEMA]
        }).compileComponents();
    }));

    beforeEach(() => {
        fixture = TestBed.createComponent(HomePage);
        component = fixture.componentInstance;
        fixture.detectChanges();
    });

    it('should create', () => {
        expect(component).toBeDefined();
        expect(component).toBeTruthy();
    });

    it('should be proper component', () => {
        expect(component).toBeInstanceOf(HomePage);
    });
});

While this passes and tests that the Home page will be created, I still have the Stryker mutation errors.

On the basic home page, the @Component has 3 fields that all generate mutant survivors since they are literal text. I am not sure how to write a test that will kill these mutant survivors.

It does not appear that Stryker has a method to ignore a section of code as an alternate if I cannot write tests to handle the condition.

Steven Scott
  • 10,234
  • 9
  • 69
  • 117

1 Answers1

4

You can test the component metadata annotations of a component instance which is fine as a starting point when doing TDD (Test-Driven Development), but you should quickly be able to replace it with proper tests which verifies actual behavior.

Note that runtime component metadata will be changed with the upcoming Angular Ivy internal rewrite.

A StackBlitz demonstrating this spec

Styles

/* host.page.scss */
:host {
  display: block;

  font-family: Georgia, serif;
}

Template

<!-- host.page.html -->
<p>app-home works!</p>

Test suite

// home.page.spec.ts
import { Component, DebugElement } from '@angular/core';
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
import { By } from '@angular/platform-browser';

import { HomePage } from './home.page';

type Diff<T extends string, U extends string> = ({[P in T]: P } & {[P in U]: never } & { [x: string]: never })[T]
type Omit<T, K extends keyof T> = Pick<T, Diff<keyof T, K>>

type ComponentMetadata = Omit<Component, 'styleUrls' | 'templateUrl'>

@Component({
  template: '<app-home></app-home>'
})
class TestHostComponent {}

/**
* Home Page Test File
*/
describe('HomePage', () => {
  let component: HomePage;
  let debugElement: DebugElement;
  let hostFixture: ComponentFixture<TestHostComponent>;
  let metadata: ComponentMetadata;
  let nativeElement: HTMLElement;

  beforeEach(async(() => {
    TestBed.configureTestingModule({
      declarations: [
        HomePage,
        TestHostComponent,
      ],
    }).compileComponents();
  }));

  beforeEach(() => {
    hostFixture = TestBed.createComponent(TestHostComponent);
    debugElement = hostFixture.debugElement.query(By.css('app-home'));
    component = debugElement.componentInstance;
    nativeElement = debugElement.nativeElement;
    metadata = component['__proto__'].constructor.__annotations__[0];
    hostFixture.detectChanges();
  });

  it('should create', () => {
    expect(component).toBeDefined();
    expect(component).toBeTruthy();
  });

  it('should be proper component', () => {
    expect(component instanceof HomePage).toBe(true, 'it must be a HomePage');
  });

  describe('metadata inspection', () => {
    it('should have proper selector', () => {
      const { selector } = metadata;

      expect(selector).toBe('app-home');
    });

    it('should have template', () => {
      const { template } = metadata;

      expect(template).toContain('app-home works!');
    });

    it('should have styles', () => {
      const { styles: [style] } = metadata;

      expect(style).toContain('display:block');
      expect(style).toContain('font-family:Georgia');
    });
  });

  describe('shallow tests with host component', () => {
    it('should have proper selector', () => {
      expect(nativeElement.tagName).toMatch(/app\-home/i);
    });

    it('should have template', () => {
      expect(nativeElement.innerText).toContain('app-home works!');
    });

    it('should have styles', () => {
      const styles: CSSStyleDeclaration = getComputedStyle(nativeElement);
      expect(styles.display).toBe('block');
      expect(styles.fontFamily.startsWith('Georgia'))
        .toBe(true, 'it should use the expected font family')
    });
  });
});
Lars Gyrup Brink Nielsen
  • 3,939
  • 2
  • 34
  • 35
  • Thanks. That showed me what I need and corrected the one issue. With an empty CSS, I will have to place a basic element in the CSS to get the validator then working, but that should be present in a real application. – Steven Scott Aug 19 '18 at 19:15
  • You are welcome, Steven. You can leave out the stylesheet until you need it. Metadata is probably not the greatest approach to testing styles. You should inspect the native element of the component instead. Come to think of it, you could also check the tag name of the native element instead of the selector from the metadata. With regards to the template, a lot of tests should break if the template URL is spelled wrong since Angular components rely a lot on their template. – Lars Gyrup Brink Nielsen Aug 20 '18 at 05:37
  • 1
    Yes all great points. We actually get this question a lot, also for the possibility to ignore these mutants. The question is: why is the code there, if it is not tested? You should either test it, remove it, or except a lower mutation score. There might be good reasons to ignore some lines for mutations, but this is not one of them OMHO. – nicojs Aug 20 '18 at 06:12
  • @LarsGyrupBrinkNielsen What would the raw element look like in a test? – Steven Scott Aug 20 '18 at 15:20
  • @nicojs Angular needs these component definitions. The majority of them will fail to compile when mutated, but not all of these. My reason for trying to get the higher score of removing all mutations, is that different developers will look at the results, and think their code is OK (it met a 25% threshold), but their code may be of concern. Forcing a 0% would ensure our tests are good, but to get there, you need to ignore some code. Junior developers writing tests may not recognize the different patterns and what can be safely ignored vs what needs more test cases. – Steven Scott Aug 20 '18 at 15:26
  • 1
    Let me get back to you with more test examples, Steven. I think that metadata inspection can be used as a starting point when using TDD, but eventually they should be replaced with real tests. Also, I am not sure that we can rely on `__proto__` in all environments or `__annotations__` keeping its name in future Angular versions. – Lars Gyrup Brink Nielsen Aug 20 '18 at 22:28
  • 1
    I added proper tests to the answer and the StackBlitz, @StevenScott. – Lars Gyrup Brink Nielsen Aug 21 '18 at 06:50
  • @LarsGyrupBrinkNielsen While this does work in your Gist, I get an error as I am using Jest instead of Karma. I believe the scss is not getting processed as I do not see data or a structure in the styles variable. – Steven Scott Aug 21 '18 at 17:11
  • 1
    I think it was because I removed the `async()` call from the first `beforeEach`. I added it back now. – Lars Gyrup Brink Nielsen Aug 21 '18 at 18:51