ngx-prober
Library for improving Unit Tests of Angular 2+ projects - tested up to Angular 7. It provides the following functionality:
- Cleaner and simpler API for testing Angular projects.
- Easy mocking of TypeScript classes.
- Simpler injection to Component and Directive scopes.
- Dynamic mocking of injected provider classes.
Using ngx-prober results in much simpler and more readable unit tests. It removes a lot of boilerplate code and lets you concentrate on real test scenario.
Installation
npm install ngx-prober --save-dev
Basic example
describe'MySampleComponent',;
And bit more complex one...
describe'MySampleComponent',;
API overview
Main API
probeComponent(componentType: Type<C>, componentModule: Type<any>, config: ComponentProbeConfig): ComponentProbe<C>
- sets up a fixture for testing Component.probeHttpService(serviceType: Type<S>, config: HttpServiceProbeConfig): HttpServiceProbe<S>
- sets up a fixture for testing service which uses HttpClient.mock(mockedType: Type<T>): T
- creates dynamic mock object for given Type.asSpy(functionRef: Function): jasmine.Spy
- casts a function reference to Jasmine Spy. Fails if given parameter is not a Jasmine Spy.ActivatedRouteStub
- simple mock for ActivatedRoute class. Code is taken from Angular documentation: https://angular.io/guide/testing.
ComponentProbeConfig attributes
providers
- service providers required by component under test. Similar toproviders
ofTestBed.configureTestingModule
, butprovider
is extended with some new optional attributes:mock
- when set to true, creates dynamic mock object instead of using real class. Defaults to false.component
- injects the provider to given component scope, instead of module scope.directive
- injects the provider to given directive scope, instead of module scope.
fixtureInit
- code that is run before every test case, after the component creation but before running the test case.modules
- additional modules which are imported to test fixture. Passed toimports
ofTestBed.configureTestingModule
. There's no need to importBrowserAnimationsModule
orNoopAnimationsModule
, the latter one is added automatically.declarations
- additional components needed by our test. Passed todeclarations
ofTestBed.configureTestingModule
. There's no need to declare component under test, it's added automatically.detectChangesOnInit
- runs change detection after creating test fixture. Defaults totrue
.includeNoopAnimationModule
- automatically importsNoopAnimationsModule
. Defaults totrue
.mockedComponents
- component classes which should be replaced with stubs, instead of using real implementation. Experimental functionality.
ComponentProbe attributes
testBed
- Angular TestBed.fixture
- Angular test component fixture.component
- instance of component under test.nativeElement
- HTML element for component under test.debugElement
- debug element for component under test.get(type): T
- retrieves service from root Angular scope (similar toTestBed.get
).getFromChildComponent(type, childComponentType): T
- retrieves service from component scope.getFromDirective(type, directiveType): T
- retrieves service from directive scope.detectChanges()
- runs change detection.queryByCss(selector): DebugElement
- returns first element matching given css selectorqueryAllByCss(selector): DebugElement[]
- returns all elements matching given css selector
HttpServiceProbeConfig attributes
providers
- service providers required by the service under test.fixtureInit
- code that is run before every test case, before the tested service instance is created.modules
- additional modules which are imported to test fixture.HttpClientTestingModule
is included automatically, no need to add it here.autoVerifyAfterEach
- decides ifHttpTestingController.verify()
method should be called automatically after every test case. Defaults to true.
HttpServiceProbe attributes
testBed
- Angular TestBed.service
- instance of service under test.httpController
-HttpTestingController
instanceget(type): T
- retrieves service from root Angular scope (similar toTestBed.get
).expect(...): TestRequest
- base function for defining expected HTTP call, and replying to it with given content. Flexible but verbose, consider using other functions instead.expectSuccess(...): TestRequest
- base function for defining expected HTTP call, and replying with success. Consider using dedicated functions for HTTP methods, before using this one.expectGet(...): TestRequest
- function for defining expected GET call, and replying with success.expectPost(...): TestRequest
- similar to aboveexpectPut(...): TestRequest
- similar to aboveexpectDelete(...): TestRequest
- similar to aboveexpectError(...): TestRequest
- base function for defining expected HTTP call, and replying with error. Consider using dedicated functions for HTTP methods, before using this one.expectGetError(...): TestRequest
- function for defining expected GET call, and replying with error.expectPostError(...): TestRequest
- similar to aboveexpectPutError(...): TestRequest
- similar to aboveexpectDeleteError(...): TestRequest
- similar to above
Detailed examples
Basics test setup
describe'MySampleComponent',;
MySampleModule
is the module that owns MySampleComponent
.
Test fixture will include declarations
, imports
and providers
of MySampleModule
, so we don't need to duplicate them in test code.
We can later mock the things we don't need.
Including additional modules and components in test setup
describe'MySampleComponent',;
Binding to component @Input and @Output fields
describe'MySampleComponent',;
Verifying generated HTML content
describe'MySampleComponent',;
Handling providers
describe'MySampleComponent',;
Remarks:
- Angular always clones the object passed in
useValue
, original instance is not used. - If you need that object later, don't use the original one from
useValue
. You need to fetch the cloned one, usingprobe.get(...)
(as described below).
Handling component-scoped and directive-scoped providers
describe'MySampleComponent',;
Remarks:
- You can use
mock
,useClass
oruseValue
, as in previous example. - Just add
component
ordirective
attribute, to put the service in Component or Directive scope, instead of Module scope.
Retrieving service instances (or serivce mock instances)
describe'MySampleComponent',;
Remarks:
probe.get(...)
only retrieves module-scoped services, and top-level component-scoped services (i.e. services in MySampleModule and MySampleComponent scope).- Next section describes how to fetch directive-scoped services and nested child-scoped services.
Retrieving component-scoped service instances from child components, and directive-scoped service instances
describe'MySampleComponent',;
Remarks:
- Service only exists, if given component/directive also exists on the page.
- So
getFrom...
will fail if component/directive was not rendered for some reason (e.g. by false *ngIf condition, which removed some content).
Creating Mocks outside of ComponentProbe
it'should create mock',
Remarks:
- Created mock is identical to the one created with
{provide: ..., mock: true}
. - Mock can be created this way only inside
it
orbeforeEach
functions.mock
call will fail when used outside of these functions. - So
mock
is not suitable forproviders
section. This code will fail:
{provide: SomeService, useValue: mock(SomeService)}
Use this sytax for creating mocks inproviders
section:
{provide: SomeService, mock: true}
Instrumenting Mock functions
describe'MySampleComponent',;
Remarks:
- Function spies are implemented with Jasmine, so we can use full Jasmine API after calling
asSpy
.
Instrumenting real object's funcions
describe'MySampleComponent',;
Remarks:
- This is done with regular Jasmine API.
Mocking child components
describe'Component mocks',;
Remarks:
- It would be better, if services used by component would be automatically mocked, without specifying it in
providers
. Just I don't know how to implement it.
Simulating UI interactions
describe'MySampleComponent',;
Testing components which use Router
describe'MySampleComponent',;
Testing components which use URL parameters
describe'MySampleComponent',;
Testing MatDialogs components
describe'MyDialogComponent',;
Testing components which use MatDialogs
describe'MySampleComponent',;
Testing services which use HttpClient
describe'MyService',;
Simulating HTTP errors
describe'MyService',;
Verifying whether given url was NOT called
describe'MyService',;
Calling real HTTP backend from test case
describe'MyService',;
Remarks:
- It's enough to add HttpClientModule in HttpServiceProbeConfig.modules field.
- You cannot use any HttpServiceProbe.expect... functions. They will fail, as HttpTestingController is not defined when using HttpClientModule instead of HttpClientTestingModule.
Testing services which don't use HttpClient
describe'MySampleService',;
Remarks:
- Services should be tested with plain tests, without using Angular TestBed. Testing them within Angular container is just unnecessary complication.
mock
still can be used for mocking service's dependencies.- Or we can use more comprehensive mocking frameworks instead, like ts-mockito or typemoq.
Limitations
- Mocking mechanism does not support interface types. Only class types can be mocked.
- Jasmine functionality
mock.someFuntction.and.callThrough
does not work for mocks generated by class type. Original object is not created when generating mock, so there's no instance which could handlecallThrough
behavior.
Why to import module which owns the component, into the test fixture?
// Second parameter of 'probeComponent' is the type of module which owns the tested component.;
Here's the promised explanation :)
The module declaration contains dependencies of tested component. Usually we need some of these dependencies in test fixture as well.
The most popular solution is to declare these dependencies twice:
- in declaration of module which owns the component (for production use)
- in test fixture setup (for unit tests)
I don't like duplication, so I'm using another approach:
- import a module which owns the component into the test fixture
- mock the module dependencies which you don't need
From my experience, it makes the code for test initialization much shorter and easier to maintain.
That's why probeComponent
function encourages you to pass the component's module as second parameter.
If you don't like it, just pass undefined
and do the duplication :)