Toggle Menu

Angular Unit Test Helper

Helper functions to help write unit tests in Angular using mocks and spies.


angular-unit-test-helper, published on npm, is a unit testing library with a minimal feature set designed to bridge gaps in jasmine’s support of modern JavaScript features.

Excellians Doguhan Uluca and Brendan Sawyer created this library to support Test Driven Development (TDD) in Angular projects. In the spirit of TDD ~99% the project itself is covered by unit tests.

Purpose

TDD is a pillar of Agile Engineering and is fundamental to delivering quality code to production with speed. In unit testing, it is essential to only test a single function at a time. The use of external classes and imports in functions make it impossible to write a unit test without the use of test doubles.

Creating test doubles of all classes in a large project can quickly become very time consuming and difficult to maintain. Ideally, developers should be able to use an auto-mocking library to create mocks of complex objects however, existing libraries don’t play well with Angular’s TestBed and RxJS\BehaviorSubject objects.

angular-unit-test-helper covers this gap.

Install

Add the package to your Angular project with npm:

$ npm i -D angular-unit-test-helper

The example projects and code generator below explain how you can best leverage the library.

Example Projects

Check out my sample projects that leverage angular-unit-test-helper:

Use the ng-tester package to generate robust and efficient unit tests using angular-unit-test-helper.

Usage

$ npm i -D ng-tester
$ npx ng g ng-tester:unit

For more information see https://github.com/bjsawyer/ng-tester/.

Features

Here are the main features of the library.

autoSpyObj(classUnderTest: Function, spyProperties: string[] = [], observableStrategy = ObservablePropertyStrategy.Observable)

An extension of jasmine.createSpyObj with automatic discovery of functions and property getters given a Class, without requiring an instance of an object.

If you’d want to spy on a property without a getter, then you can simply pass in the property name like autoSpyObj(WeatherService, ['currentWeather$']).

Return value of autoSpyObj will be a true mock of the Class with spy-able methods and properties, making it easy to control and modify the return values of external dependencies during testing.

If the property name ends with $ indicating that the property is an Observable, then you can specify an optional ObservablePropertyStrategy to prefer {}new Observable() or new BehaviorSubject(null) default values for your mocked properties.

Usage
const weatherServiceSpy = autoSpyObj(WeatherService)
Alternate Usage
const weatherServiceSpy = autoSpyObj(
  WeatherService,
  ['currentWeather$'],
  ObservablePropertyStrategy.BehaviorSubject
)

autoSpyObj replaces the more verbose and difficult to maintain code, shown below:

jasmine.createSpyObj(WeatherService.name, [
  'getCurrentWeather',
  'getCurrentWeatherByCoords',
  'updateCurrentWeather',
])
addPropertyAsBehaviorSubject(weatherServiceSpy, 'currentWeather$')

addProperty(object: object, propertyName: string, valueToReturn: object)

When creating a mock object, add a property to that object with a property getter, so you can use a jasmine.spyOnProperty.

Usage
  weatherServiceMock = jasmine.createSpyObj('WeatherService', ['getCurrentWeather'])
  addPropertyAsBehaviorSubject(weatherServiceMock, 'currentWeather', null)
  ...
  spyOnProperty(weatherServiceMock, 'currentWeather$').and.returnValue({ temp = 72})

addPropertyAsBehaviorSubject(object: object, propertyName: string)

Convenience method to configure a property as a RxJS\BehaviorSubject, so you can update its value before each test by calling .next on it.

Usage
  weatherServiceMock = jasmine.createSpyObj('WeatherService', ['getCurrentWeather'])
  addPropertyAsBehaviorSubject(weatherServiceMock, 'currentWeather$')
  ...
  weatherServiceMock.currentWeather$.next(fakeWeather)

createComponentMock(className: string, selectorName?: string, template = ”)

Creates a mock class decorated with @Component, if not specified selector is inferred to be app-my-class given MyClassComponent. Provides an option to override empty template.

Usage
TestBed.configureTestingModule({
      declarations: [ ..., createComponentMock('CurrentWeatherComponent')]
      ...
})

Note: Inferred selector in the above example is 'app-current-weather'.

Replaces
@Component({
  selector: 'app-current-weather',
  template: '',
})
class MockCurrentWeatherComponent {}

injectClass<TDependency>(dependency: Type<TDependency> | AbstractType<TDependency>): TDependency

Helper function to inject a dependency, like a service, into the TestBed with a typed return variable.

Usage
beforeEach(() => {
  weatherService = injectClass(WeatherService)
})
Replaces
beforeEach(() => {
  weatherService = TestBed.inject(WeatherService)
})

injectSpy<TDependency>(dependency: Type<TDependency> | AbstractType<TDependency>): jasmine.SpyObj<TDependency>

Similar to injectClass, but more descriptive to read for developers if returning a mocked SpyObj.

Usage
beforeEach(() => {
  weatherServiceMock = injectSpy(WeatherService)
})
Replaces
beforeEach(() => {
  weatherServiceMock = TestBed.inject(WeatherService) as any
})

getAllFunctions(prototype: any, props?: (string | number | symbol)[])

Helper function that returns all functions in a given Class using reflection, so you don’t have to provide an instance of the object.

getAllProperties(prototype: any, props?: (string | number | symbol)[])

Helper function that returns all property getters in a given Class using reflection, so you don’t have to provide an instance of the object.

Contribute

We welcome your issues, questions, ideas, and contributions on GitHub. We’d love for you to check out, star, or fork the project at:

https://github.com/duluca/angular-unit-test-helper

 

Share This

Share on Twitter Share on Facebook Share on LinkedIn Share via email

You Might Also Like

Python

The Risks of Insecure YAML Deserialization in Python

YAML is a python favorite for object serialization, especially around application configuration. So, it is...

Agile

Agile eXamined: The Sprint Goal

Daily standups, acceptance criteria, user stories, retrospectives…am I right Agilists? In the day-to-day, it’s easy...

Artificial Intelligence (AI)

Contextualizing Responsible AI Practices for Fraud Detection

Developing and sharing Responsible Artificial Intelligence (AI) practices are a key element in protecting the...