How to mock ContentChildren in Angular
When a component or directive with ContentChildren is a dependency of the declaration under test,
we can replace it with a mock and test the content that its caller projects into it.
The first examples use the @ContentChildren decorator. For the contentChildren() signal API,
see Signal content queries.
For tests of the declaration's own collection and updates, see How to test ContentChildren. For a query that returns one child, see How to mock ContentChild.
Let's assume that XdCardComponent collects templates marked with MyTplDirective:
@Component({
selector: 'xd-card-template-ref-by-render',
template: '',
})
class XdCardComponent {
@ContentChildren(MyTplDirective)
public readonly templates?: QueryList<MyTplDirective>;
}
Its caller supplies a header and a footer. The myTpl input identifies each template:
<xd-card-template-ref-by-render>
<ng-template myTpl="header" let-label>
rendered-header-{{ label }}
</ng-template>
<span my-tpl *myTpl="'footer'; let label">
rendered-footer-{{ label }}
</span>
</xd-card-template-ref-by-render>
Passing the module as the second argument to MockBuilder
replaces its declarations with mocks, including XdCardComponent and MyTplDirective:
// Return the result so the test waits for TestBed configuration.
beforeEach(() => MockBuilder(null, TargetModule));
MockComponent and MockDirective
preserve ContentChildren queries and their read and descendants options.
Angular populates a QueryList on the mock while the declaration's real lifecycle logic is mocked.
The query contains direct children by default; descendants: true includes nested projected content.
Without matching children, the collection is empty. ContentChildren has no static option.
Render collected templates
After rendering the host with MockRender, we can find the card with
ngMocks.find and select a template by its input value with
ngMocks.findTemplateRef:
const xdCardEl = ngMocks.find('xd-card-template-ref-by-render');
const tplHeader = ngMocks.findTemplateRef(xdCardEl, ['myTpl', 'header']);
const tplFooter = ngMocks.findTemplateRef(xdCardEl, ['myTpl', 'footer']);
Use ngMocks.render to render each template with a value for $implicit.
We can then check the caller's rendered content:
ngMocks.render(xdCardEl.componentInstance, tplHeader, 'test');
expect(xdCardEl.nativeElement.innerHTML).toContain('rendered-header-test');
ngMocks.render(xdCardEl.componentInstance, tplFooter, 'test');
expect(ngMocks.formatHtml(xdCardEl)).toContain(
'<span my-tpl=""> rendered-footer-test </span>',
);
The footer's *myTpl syntax creates a template around the span.
Select that template with ['myTpl', 'footer']; my-tpl is an attribute of the nested span.
To remove rendered content, pass the same component and template to ngMocks.hide.
Signal content queries
For a dependency that uses contentChildren, provide a signal with
MockInstance and control the collection in the test:
class TargetComponent {
public readonly all = contentChildren(ItemDirective, { descendants: true });
}
The signal example below uses a standalone HostComponent
that imports TargetComponent and projects two ItemDirective instances into it.
Pass the host to MockBuilder to mock both imported declarations:
beforeEach(() => MockBuilder(HostComponent));
Before rendering, supply the collection signal:
// In the suite, reset customizations after each test.
MockInstance.scope();
// In the test, provide an ordinary Angular signal before MockRender.
const all = signal<readonly ItemDirective[]>([]);
MockInstance(TargetComponent, 'all', all);
We can now set the collection to the projected mocks:
MockRender(HostComponent);
const target = ngMocks.findInstance(TargetComponent);
const items = ngMocks.findInstances(ItemDirective);
expect(target.all()).toEqual([]);
all.set(items);
expect(target.all()).toEqual(items);
Signal queries are available from Angular 17.2 and stable from Angular 19.
ng-mocks does not recreate their initializers on a mocked declaration.
The array supplied through MockInstance contains values set by the test;
Angular does not populate this ordinary signal as a content query, including after projected content changes.
Customize each signal property that the consumer reads.
To test Angular's collection updates, keep the declaration real.
Live example
This decorator example uses Angular 18 NgModules and keeps the collection and its header/footer rendering case from TestTemplateRefByRender.
import { CommonModule } from '@angular/common';
import {
Component,
ContentChildren,
Directive,
Input,
NgModule,
QueryList,
TemplateRef,
} from '@angular/core';
import { MockBuilder, MockRender, ngMocks } from 'ng-mocks';
@Directive({
selector: '[myTpl]',
})
class MyTplDirective {
@Input('myTpl') public readonly name: string | null = null;
public constructor(public readonly tpl: TemplateRef<any>) {}
}
@Component({
selector: 'xd-card-template-ref-by-render',
template: '',
})
class XdCardComponent {
@ContentChildren(MyTplDirective)
public readonly templates?: QueryList<MyTplDirective>;
}
@NgModule({
declarations: [MyTplDirective, XdCardComponent],
imports: [CommonModule],
})
class TargetModule {}
describe('TestTemplateRefByRender', () => {
// Mock both the card and the template directive.
beforeEach(() => MockBuilder(null, TargetModule));
beforeEach(() =>
MockRender(`
<xd-card-template-ref-by-render>
<ng-template myTpl="header" let-label>
rendered-header-{{ label }}
</ng-template>
<span my-tpl *myTpl="'footer'; let label">
rendered-footer-{{ label }}
</span>
</xd-card-template-ref-by-render>
`),
);
it('renders templates', () => {
const xdCardEl = ngMocks.find('xd-card-template-ref-by-render');
const tplHeader = ngMocks.findTemplateRef(xdCardEl, ['myTpl', 'header']);
const tplFooter = ngMocks.findTemplateRef(xdCardEl, ['myTpl', 'footer']);
// Render the collected header with its implicit context.
ngMocks.render(xdCardEl.componentInstance, tplHeader, 'test');
expect(xdCardEl.nativeElement.innerHTML).toContain('rendered-header-test');
// Render the footer created by the structural directive's short syntax.
ngMocks.render(xdCardEl.componentInstance, tplFooter, 'test');
expect(ngMocks.formatHtml(xdCardEl)).toContain(
'<span my-tpl=""> rendered-footer-test </span>',
);
});
});
Live example of signal content queries
This example uses Angular 19+ standalone defaults.
import {
Component,
contentChildren,
Directive,
Input,
signal,
} from '@angular/core';
import {
isMockOf,
MockBuilder,
MockInstance,
MockRender,
ngMocks,
} from 'ng-mocks';
@Directive({
selector: '[signalContentItem]',
})
class ItemDirective {
@Input() public signalContentItem = '';
}
@Component({
selector: 'target-signal-content',
template: '<ng-content></ng-content>',
})
class TargetComponent {
public readonly direct = contentChildren(ItemDirective);
public readonly all = contentChildren(ItemDirective, {
descendants: true,
});
}
@Component({
selector: 'host-signal-content',
imports: [ItemDirective, TargetComponent],
template: `
<target-signal-content>
@if (show()) {
<span signalContentItem="first"></span>
}
<div><span signalContentItem="nested"></span></div>
</target-signal-content>
`,
})
class HostComponent {
public readonly show = signal(true);
}
describe('TestContentChild:signals', () => {
MockInstance.scope();
it('customizes contentChildren signals on a mocked owner', async () => {
// Keep the host real and mock its imported declarations.
await MockBuilder(HostComponent);
// Provide each collection signal before creating the mock component.
const direct = signal<readonly ItemDirective[]>([]);
const all = signal<readonly ItemDirective[]>([]);
MockInstance(TargetComponent, 'direct', direct);
MockInstance(TargetComponent, 'all', all);
const fixture = MockRender(HostComponent);
const target = ngMocks.findInstance(TargetComponent);
const items = ngMocks.findInstances(ItemDirective);
// Projection does not populate the supplied signals.
expect(isMockOf(target, TargetComponent)).toBe(true);
expect(target.direct()).toEqual([]);
expect(target.all()).toEqual([]);
// Set the direct and descendant collections independently.
direct.set([items[0]]);
all.set(items);
expect(target.direct()).toEqual([items[0]]);
expect(target.all()).toEqual(items);
// Projection changes do not populate or clear these ordinary signals.
fixture.point.componentInstance.show.set(false);
fixture.detectChanges();
expect(target.direct()).toEqual([items[0]]);
expect(target.all()).toEqual(items);
// Apply the changed collection values explicitly.
direct.set([]);
all.set([items[1]]);
expect(target.direct()).toEqual([]);
expect(target.all()).toEqual([items[1]]);
});
});