npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2024 – Pkg Stats / Ryan Hefner

@ctng/testing

v5.0.2

Published

The TestHelper helps sharing code between Angular & Jasmine unit tests by providing common test setups and corresponding test contexts for services and directives.

Downloads

2

Readme

TestHelper

The TestHelper helps sharing code between Angular & Jasmine unit tests by providing common test setups and corresponding test contexts for services and directives.

How does it work: The idea of this library is based on Jasmine's userContext which is an empty object that is set as the receiver for each test (and their beforeEach / afterEach), thus it can be used and edit via this during tests and test setups. See this gist for more information.

Note: Since the concept relies on userContext and so on this within unit tests, function(this: ***) {} has to be used within beforeEach(), afterEach() and it(). By using arrow functions (() => {}) the userContext can't be accessed.

How to use

In the following section parts of the unit tests for this library are shown to demonstrate how to use the TestHelper. For a more detailed look, see /test/*.spec.ts.

TestHelper Setup

You can create a new instance of the TestHelper globally by passing one or several common TestMetaData definition(s) - including Angular TestModuleMetadata (imports/providers/declarations/etc).

// src/test/test-helper.ts#L9-L62

/*********************************************************
 * Setup the TestHelper with some common TestMetaData.
 *
 * In this example just a simple test provider.
 *********************************************************/
export const TEST_TOKEN = new InjectionToken<string>('test-token');

const commonMetaData: TestMetaData = {
  providers: [
    {
      provide: TEST_TOKEN,
      useValue: 'test value',
    },
  ],
};

export const baseTestHelper = new TestHelper([commonMetaData]);

/*********************************************************
 * Since several TestMetaData objects can be passed to the TestHelper,
 * any number of TestHelper instances can be created including inheritance.
 *
 * e.g. if there are a lot of tests which need the http client,
 * a http (sub)TestHelper can be created. This helper then provides the
 * base TestMetaData and additional TestMetaData for http client.
 *********************************************************/

/**
 * Since all our fake http unit tests work with HttpTestingController,
 * the httpTestHelper can provide it for all tests.
 *
 * This is the base http test context interface (includes everything the httpTestHelper should provide).
 */
interface HttpTestContext {
  httpMock: HttpTestingController;
}

/**
 * The test contexts which are used in service resp. directive tests.
 */
export interface HttpServiceTestContext<T> extends ServiceTestContext<T>, HttpTestContext {}
export interface HttpDirectiveTestContext<T, H> extends DirectiveTestContext<T, H>, HttpTestContext {}

export const httpTestHelper = new TestHelper([
  // http helper uses the everything from base helper
  ...baseTestHelper.getCommonTestMetaData(),
  {
    imports: [HttpClientTestingModule],
    beforeEach: (testContext: HttpTestContext) => {
      // Callback which is used before each test, here the HttpTestingController is set on the test context
      testContext.httpMock = TestBed.inject(HttpTestingController);
    },
  },
]);

TestHelper functions

A configured test helper instance provides

  • createServiceTestSetup(FooService) for services
  • createDirectiveTestSetup(FooComponent, FooHostComponent) for directives

Additionally you can pass further TestMetaData into the calls which are only provided in the particular describe() context.

Note: If the passed TestMetaData contains a beforeEach() function, this function is called before the tested service/directive is injected. Can be used e.g. to spie on functions the constructor of the tested service is calling. If the service instance is needed, a beforeEach() can be placed within describe() as usual (is called after injection).

Testing Services

Run testHelper.createServiceTestSetup within describe() and provide the service you want to test. The ServiceTestContext<T> interface gives typing and intellisense to your test context.

Without additional config

// src/test/services/services.spec.ts#L13-L29

describe('TestHelper - Services', () => {
  // Create the test setup and context
  baseTestHelper.createServiceTestSetup(FooService);

  it('should create simple service context', function(this: ServiceTestContext<FooService>) {
    // 'Provided' by baseTestHelper
    expect(this.service).toBeDefined();
    expect(this.service.multiply(2, 3)).toBe(6);
  });

  it('should provide the common providers', function(this: ServiceTestContext<FooService>) {
    // 'Provided' by baseTestHelper
    const testToken = TestBed.inject(TEST_TOKEN);
    expect(testToken).toBeDefined();
    expect(testToken).toBe('test value');
  });
});

With additional config

// src/test/services/services.spec.ts#L35-L95

/**
 * For testing purpose, let's imagine we need another spy in our context for his test only,
 * we add it to our test context and set it in beforeEach().
 */
interface AdvFooServiceTestContext extends HttpServiceTestContext<AdvancedFooService> {
  spyTest: jasmine.Spy;
  testString: string;
}

describe('TestHelper - Services - additional config', () => {
  // Additional providers/imports/declarations/beforeEach for this test only
  httpTestHelper.createServiceTestSetup(AdvancedFooService, {
    providers: [{ provide: LocalStorageService, useClass: LocalStorageMock }],
    // This beforeEach is executed BEFORE the tested service was injected
    // -> Helpful for testing constructor functionality, e.g. setting up spies before
    beforeEach: (testContext: AdvFooServiceTestContext) => {
      testContext.testString = 'Value set in test-only beforeEach';
    },
  });

  // This beforeEach is executetd AFTER the tested service
  beforeEach(function(this: AdvFooServiceTestContext) {
    // Add additional properties to the test context for this test only
    this.spyTest = spyOn(this.service, 'getLocalStorageItem').and.callThrough();
  });

  it('should create service context', function(this: AdvFooServiceTestContext) {
    // 'Provided' by baseTestHelper
    expect(this.service).toBeDefined();
  });

  it('should still provide the common providers', function(this: AdvFooServiceTestContext) {
    // 'Provided' by baseTestHelper
    const testToken = TestBed.inject(TEST_TOKEN);
    expect(testToken).toBeDefined();
    expect(testToken).toBe('test value');
  });

  it('should provide mock local storage', function(this: AdvFooServiceTestContext) {
    // LocalStorageMock 'provided' by httpTestHelper.createServiceTestSetup (this test only)
    expect(this.service.getLocalStorageItem()).toBe('mock-item');
  });

  it('should set httpMock in global beforeEach()', function(this: AdvFooServiceTestContext) {
    this.service.getFoo().subscribe();

    // 'Provided' by httpTestHelper (beforeEach callback)
    this.httpMock.expectOne('/testinger');
    expectNothing();
  });

  it('should set spie in local beforeEach()', function(this: AdvFooServiceTestContext) {
    // 'Provided' by local beforeEach (this test only)
    expect(this.spyTest).toBeDefined();
  });

  it('should set test string in beforeEach() provided in service setup ', function(this: AdvFooServiceTestContext) {
    // 'Provided' by local beforeEach (this test only)
    expect(this.testString).toBe('Value set in test-only beforeEach');
  });
});

Testing Directives (components, attribute directives, etc.)

The TestHelper implements the 'Host Component principle' (see Angular docs). That means a directive is tested by creating a test host component which uses the actual directive to test.

Run testHelper.createDirectiveTestSetup and provide the directive to test and the test host component. The DirectiveTestContext<T, H> automatically gives intellisense to your test context.

Without additional config

// src/test/components/components.spec.ts#L15-L57

/**
 * Test host component.
 */
@Component({
  template: `
    <ct-ng-foo [fooValue]="fooValue"></ct-ng-foo>
  `,
})
class FooHostComponent {
  constructor() {}

  public fooValue: string;

  public setFooValue(value: string) {
    this.fooValue = value;
  }
}

describe('TestHelper - Components', () => {
  // Create the test setup and context
  baseTestHelper.createDirectiveTestSetup(FooComponent, FooHostComponent);

  it('should create simple component context', function(this: DirectiveTestContext<FooComponent, FooHostComponent>) {
    // 'Provided' by baseTestHelper
    expect(this.fixture).toBeDefined();
    expect(this.testedDirective).toBeDefined();
    expect(this.testedElement).toBeDefined();
    expect(this.hostComponent).toBeDefined();
    expect(this.hostElement).toBeDefined();

    this.hostComponent.setFooValue('Test Value');
    this.fixture.detectChanges();
    // Input should have been set
    expect(this.testedDirective.formattedFooValue).toEqual('TestValue');
  });

  it('should provide the common providers', function(this: DirectiveTestContext<FooComponent, FooHostComponent>) {
    // 'Provided' by baseTestHelper
    const testToken = TestBed.inject(TEST_TOKEN);
    expect(testToken).toBeDefined();
    expect(testToken).toBe('test value');
  });
});

With additional config

// src/test/components/components.spec.ts#L63-L138

/**
 * Test wrapper component.
 */
@Component({
  template: `
    <ct-ng-adv-foo [fooValue]="fooValue"></ct-ng-adv-foo>
  `,
})
class AdvancedFooWrapperComponent {
  constructor() {}

  public fooValue: string;

  public setFooValue(value: string) {
    this.fooValue = value;
  }
}

/**
 * Just for readability, we actually do not provide additional stuff to the test context here.
 */
interface AdvancedFooTestContext extends HttpDirectiveTestContext<AdvancedFooComponent, AdvancedFooWrapperComponent> {
  testString: string;
}

describe('TestHelper - Components - additional config', () => {
  // Additional providers/imports/declarations for this test only
  httpTestHelper.createDirectiveTestSetup(AdvancedFooComponent, AdvancedFooWrapperComponent, {
    providers: [{ provide: LocalStorageService, useClass: LocalStorageMock }],
    // This beforeEach is executed BEFORE the tested service was injected
    // -> Helpful for testing constructor functionality, e.g. setting up spies before
    beforeEach: (testContext: AdvancedFooTestContext) => {
      testContext.testString = 'Value set in test-only beforeEach';
    },
  });

  it('should create component context', function(this: AdvancedFooTestContext) {
    // 'Provided' by baseTestHelper
    expect(this.fixture).toBeDefined();
    expect(this.testedDirective).toBeDefined();
    expect(this.testedElement).toBeDefined();
    expect(this.hostComponent).toBeDefined();
    expect(this.hostElement).toBeDefined();

    this.hostComponent.setFooValue('Test Value');
    this.fixture.detectChanges();
    // Input should have been set
    expect(this.testedDirective.formattedFooValue).toEqual('TestValue');
  });

  it('should provide the common providers', function(this: AdvancedFooTestContext) {
    // 'Provided' by baseTestHelper
    const testToken = TestBed.inject(TEST_TOKEN);
    expect(testToken).toBeDefined();
    expect(testToken).toBe('test value');
  });

  it('should provide mock local storage', function(this: AdvancedFooTestContext) {
    // LocalStorageMock 'provided' by httpTestHelper.createDirectiveTestSetup (this test only)
    expect(this.testedDirective.getLocalStorageItem()).toBe('mock-item');
  });

  it('should set httpMock in beforeEach()', function(this: AdvancedFooTestContext) {
    // Simulate a button click
    this.testedElement.querySelector('.foo-values').click();

    // 'Provided' by httpTestHelper (beforeEach callback)
    this.httpMock.expectOne('/foovalues');
    expectNothing();
  });

  it('should set test string in beforeEach() provided in directive setup ', function(this: AdvancedFooTestContext) {
    // 'Provided' by local beforeEach provided in directive setup (this test only)
    expect(this.testString).toBe('Value set in test-only beforeEach');
  });
});