Summary: Yes, use a combination of the dependency stub and async service Angular doc examples.
Turns out that the TwainService does not actually use Http or other dependencies at all, it just returns a Promise:
getQuote(): Promise<string> {
return new Promise(resolve => {
setTimeout( () => resolve(this.nextQuote()), 500 );
});
}
So because in our cases, the component under test depends on a service that has potentially a multitude of its own dependencies, it really is best to mock that service via the WelcomeComponent/userServiceStub example in the Angular docs (keep reading for the caveat). The stub object will have a property for the real method name, and its value is an unnamed function with its statements.
Now, it's easy to mistakenly implement a method in the stub that returns synchronously, even though the real method returns asynchronously. You don't want to do that though because the component under test likely contains a .then() block to handle the real asynchronous method's response. Therefore, make sure your stub method returns your testing-return-value wrapped in a promise: return Promise.resolve(mySwappableTestingReturnValue).
Finally, because we are indeed testing an asynchronous method (the caveat), the it block should also use one of the three options in the TwainService example: angular's async()/whenStable.then(), angular's fakeAsync()/tick(), or jasmine's done. I also found that promise's regular .then() block appended to fixture.componentInstance.myAsyncMethod() works equally well (and likely js's async/await too).
~~~
As a Spy rookie, I also made another mistake where .and.callThrough() may help you.
For example (humor me, it's short and common): LoginComponent has a submit-btn whose onclick is handled by a logMeIn method who calls this.authService.askAuthServer. askAuthServer uses Http.
The spec that tests if a button click correctly calls logMeIn is synchronous, we don't care about the returned value from logMeIn, we only care that it was called (and UI events are handled synchronously aka on the main thread, if that helps you). The spec that tests the decision logic within logMeIn DOES need the returned value so needs to be tested asynchronously, so say we use whenStable().then().
So, you happily create a spyOn logMeIn in the beforeEach, but the click spec succeeds and the async logic spec fails. In the click spec, you'll notice that askAuthServer doesn't execute, although our spy successfully .toHaveBeenCalled(). What is going on? It's because the spy prevented the execution of logMeIn, even before we chose not to wait around for logMeIn's return. So in the logic spec, although it might not use the logMeInSpy, the spy is still there preventing the execution of logMeIn (and askAuthServer too) even though we've setup the asynchronous parts correctly.
Understanding that, either use .and.callThrough() when creating the spy in beforeEach, to allow the execution of logMeIn during both specs, or move the spy into the click spec where it won't affect the logic spec.