Package Exports
- shallow-render
- shallow-render/dist/lib/models/rendering
This package does not declare an exports field, so the exports above have been automatically detected and optimized by JSPM instead. If any package subpath is missing, it is recommended to post an issue to the original package (shallow-render) to support the "exports" field. If that is not possible, create a JSPM override to customize the exports field for this package.
Readme
shallow-render
Angular 5 & 6 testing made easy with shallow rendering and easy mocking.
The problem
Testing in Angular is HARD. TestBed is powerful but it's use in component specs ends with lots of duplication.
Here's a standard TestBed spec for a component that uses a few other components, a directive and a pipe and handles click events:
describe('MyComponent', () => {
beforeEach(async => {
return TestBed.configureTestModule({
imports: [
SomeModuleWithDependencies,
],
declarations: [
TestHostComponent,
MyComponent, // <-- All I want to do is test this!!
// We either must list all our dependencies here
// -- OR --
// Use NO_ERRORS_SCHEMA which allows any HTML to be used
// even if it is invalid!
ButtonComponent,
LinkComponent,
FooDirective,
BarPipe,
],
providers: [
MyService
]
}).compileComponents().then(() => {
let myService = TestBed.get(MyService); // Not type safe
spyOn(myService, 'foo').and.returnValue('mocked foo');
});
});
it('renders a link with the provided label text', () => {
const fixture = TestBed.createComponent(TestHostComponent);
fixture.componentInstance.labelText = 'my text';
fixture.detectChanges();
const link = fixture.debugElement.query(By.css('a'));
expect(a.nativeElement.innerText).toBe('my text');
});
it('sends "foo" to bound click events', () => {
const fixture = TestBed.createComponent(TestHostComponent);
spyOn(fixture.componentInstance, 'handleClick');
fixture.detectChanges();
const myComponentElement = fixture.debugElement.query(By.directive(MyComponent));
myComponentElement.click();
expect(fixture.componentInstance.handleClick).toHaveBeenCalledWith('foo');
});});
@Component({
template: `
<my-component [linkText]="linkText" (click)="handleClick($event)">
</my-component>
`
})
class TestHostComponent {
linkLabel: string;
handleClick() {}
}
Whew!!! That was a lot of boilerplate. Here's just some of the issues:
- Our TestBed module looks very similar if not identical to the
NgModule
I've probably already addedMyComponent
to. Total module duplication. - Since I've duplicated my module in my spec, I'm not actually sure the real module was setup correctly.
- I've used REAL components and services in my spec which means I have not isolated the component I'm interested in testing.
- This also means I have to follow, and provide all the dependencies of those real components to the
TestBed
module.
- This also means I have to follow, and provide all the dependencies of those real components to the
- I had to create a
TestHostComponent
so I could pass bindings into my actual component. - My
TestBed
boilerplate code-length exceeded my actual test code-length.
The Solution
We should mock everything we can except for the component in test and that should be EASY. Our modules already define the environment in which our components live. They should be reused, not rebuilt in our specs.
Here's the same specs using shallow-render
:
describe('MyComponent', () => {
let shallow: Shallow<MyComponent>;
beforeEach(() => {
shallow = new Shallow(MyComponent, MyModule);
});
it('renders a link with the provided label text', async () => {
const {find} = await shallow.render(`<my-component linkText="my text"></my-component>`);
const link = find('a');
expect(link.nativeElement.innerText).toBe('my text');
});
it('sends "foo" to bound click events', async () => {
const {element, bindings} = await shallow.render(
`<my-component (click)="handleClick($event)"></my-component>`,
{ bind: { handleClick: () => {} } }
);
element.click();
expect(bindings.handleClick).toHaveBeenCalledWith('foo');
});
});
Here's the difference:
- Reuses (and verifies)
MyModule
contains your component and all its dependencies. - All components inside
MyModule
are mocked using the awesome ng-mocks library. This is what makes the rendering "shallow". - The tests have much less boilerplate which makes the specs easier to follow.
- The HTML used to render the component is IN THE SPEC and easy to find.
- This means specs now double examples of how to use your component.
API
Shallow
class:
This class is used to setup your test module. It's constructor accepts two arguments:
testComponent
- the component you wish to testtestModule
- the Angular module that thetestComponent
belongs to
Behind the scenes, it breaks down the testModule
into its' bare elements and mocks everything along the way. All your components, directives and pipes are run through ng-mocks
. All your providers are mocked with a simple object {}
.
Service or Injection Token Mocking
You have control over your provider
mocks by using shallow.mock
. For example, let's say your component uses FooService
to get data:
@Injectable()
class FooService {
constructor(private _httpClient: HttpClient) {}
async getFoo() {
return await this._httpClient.get<string>('http://foo.service.com').toPromise();
}
async postFoo() {
return await this._httpClient.post<string>('http://foo.service.com').toPromise();
}
}
Shallow will automatically provide an empty mock for this service when you render your component. If your component calls the getFoo
method, you'll need to provide a stub and return the data your component needs to pass your test. To prevent mistyping, all stubs are type-safe and must match the types on the service you're mocking.
shallow.mock(FooService, {getFoo: () => 'mocked foo get'});
Have multiple services? It's chain-able so you can stack them.
shallow
.mock(FooService, {getFoo: () => 'mocked foo get'})
.mock(BarService, {getBar: () => 'mocked bar get'});
InjectionToken
s work too. Stubs are double-checked against your token's interface to make sure you're using the correct types.
shallow
.mock(FooService, {getFoo: () => 'mocked foo get'})
.mock(BarService, {getBar: () => 'mocked bar get'});
If all your specs need the same mock, you can do this in your beforeEach
block so you only need to do it once. Your individual specs may override the initial mocks if they need to.
let shallow: Shallow<MyComponent>;
beforeEach(() => {
shallow = new Shallow(MyComponent, MyModule)
.mock(FooService, {getFoo: () => 'mocked foo get'})
.mock(BarService, {getBar: () => 'mocked bar get'});
})
it('uses the mock', async () => {
const rendered = await shallow.render();
// ...
});
it('can override previously defined mocks', async () => {
const rendered = await shallow
.mock(FooService, {getFoo: () => 'custom foo'})
.render()
});
Skip mocking with dontMock
Have a service/injection token/component/directive/pipe, etc that you don't want to be mocked? Use dontMock
to bypass the automatic mocking of things in your module (or things imported by your module).
*NOTE: Angular's coreModule
and browserModule
are never mocked by this process.
shallow.dontMock(FooService, FooComponent);
Tells Shallow to use the real FooService
and FooComponent
in your spec.
Skip mocking globally with neverMock
Some components/directives/pipes you may want to always use the real thing. You may choose to "never mock" in your Karma shim them for all specs.
in karma-test-shim (also notice neverMock
is a static method on the class)
Shallow.neverMock(FooService, FooPipe);
Tells Shallow to always use the real FooService
and FooPipe
in all your specs.
Global mocks with alwaysMock
Sometimes you will have things that you're constantly re-mocking for a spec. You can setup global mocks for these things by using alwaysMock
in your Karma shim and shallow will always provide your mock in modules that use the provider you specified. Note that doing alwaysMock
is NOT a mock-once for all your specs solution. Use this feature sparingly, remember your specs should generally be self-contained as far as mock data goes. Using alwaysMock
is just as bad as using global variables. TL;DR; Use sparingly or not-at-all.
in karma-test-shim (also notice alwaysMock
is a static method on the class)
Shallow.alwaysMock(FooService, {
getFoo: () => 'foo get',
postFoo: () => 'foo post',
});
Global providers with alwaysProvide
There are some use cases when your Angular app provides something (usually a configuration) at the top-level of your application. These instance usually follow the forRoot
pattern. For these cases, you may want your specs to have a similar environment setup where the 'root' providers are globally provided to all specs. This can be accomplished by using Shallow.alwaysProvide
.
in karma-test-shim (also notice alwaysProvide
is a static method on the class)
Shallow.alwaysProvide(MyGlobalService);
Now, all specs will receive a REAL MyGlobalService
when requested for injection.
If you use the forRoot
pattern, you may provide your root providers like so:
Shallow.alwaysProvide(MyCoreModule.forRoot().providers);
You may also provide a mocked version by chaining alwaysProvide
with alwaysMock
:
Shallow
.alwaysProvide(MyGlobalService)
.alwaysMock(MyGlobalService, {getSomeValue: () => 'Globally mocked value'});
Now, all specs will receive a MOCKED MyGlobalService
when requested for injection.
Using Pipes with mockPipe
Angular pipes are a little special. They are used to transform data in your templates. By default, Shallow will mock all pipes to have no output. Your specs may want to provide mocks for these transforms to allow validation that a pipe received the correct input data.
shallow.mockPipe(MyPipe, input => `MyPipe: ${input}`);
Tells Shallow to have the MyPipe
always perform the following action on input data. This lets you inspect your templates and controls for your Pipe's side-effects.
Replace a module with a test module
Angular has a pattern in which they provide full-module replacements specifically designed for testing (see: HttpClientTestingModule). These testing modules can be used in your Shallow tests by replacing the original module with the test module.
shallow.replaceModule(HttpClientModule, HttpClientTestingModule);
If you would like to do this globally, you can use alwaysReplaceModule
in your global test setup.
Shallow.alwaysReplaceModule(HttpClientModule, HttpClientTestingModule);
Rendering with render
Once you've completed your setup, the final step is rendering. Render takes two arguments:
html
(optional) - This will be the HTML that exercises your component.renderOptions
(optional)detectChanges
: Defaults to true. Automatically run change detection after render.bind
(optional) - an object that provides your bindings to thehtml
template (see more below).
NOTE: render
returns a promise, async
/await
are your friends
Components come in different flavors, some have inputs, some have outputs, some transclude, some are entry components, etc.
Basic rendering with HTML
The simplest form of rendering uses a basic HTML template with simple inputs:
shallow.render('<my-component name="My Name"></my-component>');
This renders MyComponent
and passes a single name
input of "My Name"
.
HTML templates with complex input bindings
Some components require complex types as their inputs:
shallow.render(
'<my-component [person]="testPerson"></my-component>',
{bind: {testPerson: {firstName: 'Brandon', lastName: 'Domingue'}}}
)
Notice we pass in the bind
render option and hand it a testPerson
object. This will render the component and pass the testPerson
into the component's person
input property.
Entry Components
Entry components in Angular don't have selectors. They can't take inputs. You can render these with shallow too. Just omit the HTML template or pass in the entry component class.
shallow.render(MyComponent); // Must be the shallow testComponent
// -- or --
shallow.render(); // Automatically renders the testComponent
Rendering and Querying
A Rendering
is returned from the shallow.render()
method call.
Property | Description | type or return type |
---|---|---|
instance |
Instance of the rendered TestComponent |
|
element |
The DebugElement of the rendered TestComponent |
|
TestBed |
Easy access to TestBed |
|
fixture |
The TestBed fixture from rendering the component |
|
bindings |
The bindings object used in your render (if any) | |
find(CSS/Directive/Component) |
Finds elements by CSS or Directive/Component | QueryMatch<DebugElement> |
findComponent(Component) |
Finds and returns all matches for a Component | QueryMatch<TComponent> |
findDirective(Directive) |
Finds and returns all matches for a Directive | QueryMatch<TDirective> |
get(Token/Provider) |
Type-safe version of TestBed.get |
TProvider |
destructuring properties off of a rendering
Note that ALL of these methods and properties can be destructured from the rendering which allows for some syntactic flexibility when rendering.
For example:
const rendering = await shallow.render();
const label = rendering.find('label');
is the same as the destructured version
const {find} = await shallow.render();
const label = find('label');
Custom Jasmine Expectations
Shallow includes custom Jasmine matchers to help with query validation.
toHaveFound(count: number)
- Expect a query to have found an exact number of items.
expect(find('h1')).toHaveFound(3);
toHaveFoundOne()
- Expect a query to have found exactly one items.
expect(find('h1')).toHaveFoundOne();
toHaveFoundMoreThan(count: number)
- Expect a query to have found more than x items.
expect(find('h1')).toHaveFoundMoreThan(0); // 1 or more
toHaveFoundLessThan(count: number)
- Expect a query to have found fewer than x items.
expect(find('h1')).toHaveFoundLessThan(3); // 2 or fewer
find
=> QueryMatch<DebugElement>
find(CSSSelector | Directive | Component) => QueryMatch<DebugElement>
Accepts a CSS selector, Component class or Directive class and returns all the resulting DebugElements
wrapped in a QueryMatch
object.
const {find} = await shallow.render('<my-component name="foo"></my-component');
const result = find('my-component'); // Find all elements that match this css selector
// Expect a single result like so:
expect(result.length).toBe(1);
// or
expect(result).toHaveFound(1);
// For single results, you can use it like any flat DebugElement
expect(result.componentInstance.name).toBe('foo');
You may also pass in the Component class of the thing you are querying for:
find(MyComponent); // Finds all instances of MyComponent
findComponent
=> QueryMatch<TComponent>
findComponent(Component) => QueryMatch<TComponent>
findComponent
differs from find
in that it will return the instances of the component, not the DebugElement
returned by find
. The returned instance(s) are wrapped in a QueryMatch
object.
const {findComponent} = await shallow.render('<my-component name="foo"></my-component>');
const result = findComponent(MyComponent);
expect(result.name).is('foo');
findDirective
=> QueryMatch<TDirective>
findDirective(Directive) => QueryMatch<TDirective>
findDirective
is similar to findComponent
except it returns directive instances wrapped in a QueryMatch
object.
const {findDirective} = await shallow.render('<div myDirective="foo"/>');
const result = findDirective(MyDirective);
expect(result.myDirective).is('foo');
get
=> provider instance
get(ProvidedClass | InjectionToken) => QueryMatch<TProvider>
This is a type-safe version of TestBed.get()
.
const {get} = await shallow.render();
const service = get(MyService); // Returns an instance of MyService (or the mock if it's mocked) from the injector
expect(service.getFoo).toHaveBeenCalled();
You may also use get
to pull service instances and add more mocks/spys on them AFTER rendering. (This is usually done before rendering by using shallow.mock()
but sometimes you need to alter mocks after the initial rendering. I recommend a type-safe mocking library like ts-mocks to do your mocking.
const {get, find} = await shallow
.mock(MyService, {getFoo: () => 'FIRST FOO'})
.render();
const service = get(MyService);
new Mock(service).extend({getFoo: () => 'SECOND FOO'}); // <-- Using ts-mocks here to re-mock a method
find('button').triggerEventHandler('click', {});
const responseLabel = find('label');
expect(responseLabel.nativeElement.innerText).toBe('SECOND FOO');
QueryMatch
objects
Queries return a special QueryMatch
object. This object is a mash-up of a single object that may be used when the query yields a single result, or an array of objects for when your query yields multiple results.
This lets us use the same object semantically in our tests.
If you expect a single item in your response just use it like a single item:
const match = find('foo');
expect(match.nativeElement.tagName).toBe('foo');
If your query found multiple items, and you try to use the match as if it were a single item, Shallow will throw an error letting you know multiple items were found when you expected only one.
If you expect multiple items, use Array
methods on the matches:
const matches = find('foo');
matches.forEach(match => {
expect(match.nativeElement.tagName).toBe('foo'));
});
Need more examples?
Check out the examples folder for more specific use cases including:
- Simple component
- Component with bindings
- Component with directive
- Component with a service
- Component with custom providers
- Testing directives
- Using custom Pipe mocks
- Using Injection Tokens
- Multiple components
- Multiple modules
- Using replaceModule to substitute test modules
- Using dontMock to bypass mocking in a spec
- Using alwaysMock to globally mock things
- Using alwaysProvide to globally provide things
- Using neverMock to bypass mocking globally