В итоге я решил проблему, поэтому поделюсь с вами своим решением. Сначала начните говорить, что я подхожу к проблеме, используя TDD. Поэтому здесь я сначала опубликую набор тестов, который я использую
import { HttpClient } from '@angular/common/http';
import {
HttpClientTestingModule,
HttpTestingController
} from '@angular/common/http/testing';
import { TestBed, inject } from '@angular/core/testing';
import { bufferCount } from 'rxjs/operators';
import { zip } from 'rxjs';
import { Node } from './node';
const rootHref = '/api/nodes/root';
const rootChildrenHref = '/api/nodes/root/children';
const rootResource = {
_links: {
'node-has-children': { href: rootChildrenHref }
}
};
function expectOneRequest(controller: HttpTestingController, href: string, method: string, body: any) {
// The following `expectOne()` will match the request's URL and METHOD.
// If no requests or multiple requests matched that URL
// `expectOne()` would throw.
const req = controller.expectOne({
method,
url: href
});
// Respond with mock data, causing Observable to resolve.
// Subscribe callback asserts that correct data was returned.
req.flush(body);
}
describe('CoreModule Node', () => {
let httpTestingController: HttpTestingController;
let subject: Node;
beforeEach(() => {
TestBed.configureTestingModule({
imports: [ HttpClientTestingModule ]
});
});
beforeEach(inject([ HttpClient, HttpTestingController ],
(http: HttpClient, testingController: HttpTestingController) => {
subject = new Node(http, rootHref);
httpTestingController = testingController;
}));
afterEach(() => {
// After every test, assert that there are no more pending requests.
httpTestingController.verify();
});
it('should be created', () => {
expect(subject).toBeTruthy();
});
it('#attributes$ is provided', () => {
expect(subject.attributes$).toBeTruthy();
});
it('#attributes$ is observable element', (done: DoneFn) => {
subject.attributes$.subscribe(root => {
expect(root).toBeTruthy();
done();
});
expectOneRequest(httpTestingController, rootHref, 'GET', rootResource);
});
it('#attributes$ observable is cached', (done: DoneFn) => {
zip(subject.attributes$, subject.attributes$).subscribe(([root1, root2]) => {
expect(root1).toBe(root2);
done();
});
expectOneRequest(httpTestingController, rootHref, 'GET', rootResource);
});
it('#update() affect attributes$', (done: DoneFn) => {
// the subscribe at the end of this pipe will trigger a children request
// but the update() call will trigger a resource request that in turn
// will trigger a children request. Therefore bufferCount will produce
// an array of size two.
subject.attributes$.pipe(bufferCount(2)).subscribe(collection => {
expect<number>(collection.length).toEqual(2);
});
subject.update([]).subscribe(root => {
expect(root).toBeTruthy();
done();
});
expectOneRequest(httpTestingController, rootHref, 'GET', rootResource);
expectOneRequest(httpTestingController, rootHref, 'PATCH', {});
expectOneRequest(httpTestingController, rootHref, 'GET', rootResource);
});
it('#update() return observable', (done: DoneFn) => {
subject.update([]).subscribe(root => {
expect(root).toBeTruthy();
done();
});
expectOneRequest(httpTestingController, rootHref, 'PATCH', {});
expectOneRequest(httpTestingController, rootHref, 'GET', rootResource);
});
});
Как вы можете видеть, набор тестов проверяет, что вызовы HTTP-запроса выполняются, только если кто-то подписывается на наблюдаемое, как предполагалось. С этим набором тестов я придумаю следующую реализацию класса Node:
import { HttpClient } from '@angular/common/http';
import { Observable, Subject } from 'rxjs';
import { mergeMap, shareReplay } from 'rxjs/operators';
/** Node
*
* A node is the representation of an artifact on the Sorbotics Platform
* Registry.
*/
export class Node {
/** Attributes */
public attributes$: Observable<any>;
private attributesSubject$: Subject<any> = new Subject();
private initialFetch = false;
constructor(private http: HttpClient, private href: string) {
// the attributes$ observable is a custom implementation that allow us to
// perform the http request on the first subscription
this.attributes$ = new Observable(subscriber => {
/* the Node resource is fetched if this is the first subscription to the
observable */
if (!this.initialFetch) {
this.initialFetch = true;
this.fetchResource()
.subscribe(resource => this.attributesSubject$.next(resource));
}
// connect this subscriber to the subject
this.attributesSubject$
.subscribe(resource => subscriber.next(resource));
});
}
/* Fetch Node resource on the Platform Registry */
private fetchResource: () => Observable<any> =
() => this.http.get(this.href)
/** Update node
*
* This method implement the update of the node attributes. Once the update
* is performed successfully the attributes$ observable will push new values
* to subscribed parties.
*
* @param patches Set of patches that describe the update.
*/
public update(patches: any): Observable<any> {
const req = this.http.patch(this.href, patches)
.pipe(mergeMap(() => this.fetchResource()), shareReplay(1));
req.subscribe(resource => this.attributesSubject$.next(resource));
return req;
}
}
Как вы можете видеть, я учусь на ответе, указанном в комментарии, сделанном @jonrsharpe, но представляю использование настраиваемой обработки подписки в обозримом. Таким образом, я могу отложить HTTP-запрос до первой подписки.