Subscribing to a mocked variable in a service unit test - javascript

I'm trying to unit test a component that subscribes to a data service variable. I'm trying to mock the service and override the component's private data service variable but I'm not sure how to test the subscription of the mocked variable. Here's my (limited) code:
Service:
export class Service {
constructor(private dataService: DataService) {
this.dataService.newData.subscribe(data => {
//do something
});
}
}
Data Service:
export class DataService {
private source = new BehaviorSubject<any>([]);
newData = this.source.asObservable();
//code to update source
}
unit test for Service:
mockDataService {
private source = new BehaviorSubject<any>([]);
newData = this.source.asObservable();
}
describe('Service', () => {
beforeEach(() => {
TestBed.configureTestingModule({
providers: [
Service,
{provide: DataService, useClass: mockDataService} // is this correct?
]
}).overrideComponent(Service, {
set: {
providers: [
{provide: DataService, useClass: mockDataService}
]
}
}).compileComponents();
});
it('should register subscription', inject([Service, DataService], (service: ServiceA, mock: DataService) => { //should I be injecting the mock class?
expect(before()).toBeTruthy();
mock.newData.next("test"); // what's the correct way to test?
expect(after()).toBeTruthy();
}));
}
Am I overriding correctly? And if so, how do I correctly test that my component does the correct actions when subscribing to a private service's variable?
Thanks!

First, it would be helpful if you could provide the annotations alongside the classes. I am assuming that the Service class is a component, because you reference is when calling TestBed.overrideComponent. In that case the naming is confusing. It should have at least a suffix "Component" (see Angular style guide.)
If Service should actually be a service class, nesting services into another one is probably not a good practice (see docs.)
You are basically asking for two things.
1. Do I need to override the providers property of the module via TestBed.configureTestingModule?
For your example above, this is not necessary. You can easily omit the providers attribute from the object. It will then looks like
TestBed.configureTestingModule({})
There might be some cases where changing the providers is needed - but not in your case.
2. How should I test the service properly?
It seems like you are mixing up integration testing with unit testing. You want to test the service in both ways.
First: Unit test the service (Angular docs)
Second: Integration test – what you seem to be doing here. There is a recommended best practice as of the docs (link):
it('should register subscription', () => {
const fixture = TestBed.createComponent(Service);
dataService = fixture.debugElement.injector.get(DataService);
// do things
});
Regarding the mock.newData.next("test"), it is not really clear what you are trying to achieve here. This method call would probably give you an undefined function test error. Why? You are referring to this.source.asObservable() which returns an Obersvable. This object does not have a next method. You should maybe do some basic tutorials on RxJs.
Hope this helps!
Best,
Benji

Related

Angular: manually instantiate class with dependency injection

I am using Angular 10.0 and I have a problem with --prod compiling.
I need to instantiate classes manually and need to support dependency injection.
The following code works fine during development to instantiate my classes:
public instantiateWithDi(parentInjector: Injector, myClass: any): any {
const reflector = ReflectiveInjector.resolveAndCreate([], parentInjector);
const newInstance = reflector.resolveAndInstantiate(myClass);
return newInstance;
}
When I build my project with --prod (or --optimization=true), then I get the following error at runtime:
ERROR Error: Cannot resolve all parameters for 'e'(?). Make sure that all the parameters are decorated with Inject or have valid type annotations and that 'e' is decorated with Injectable.
Decorating the constructor parameters of the classes with #Inject did not work either. Using injection tokens does not help as well.
The classes are already decorated with #Injectable() and in the "providers" array of their respective angular module.
I know, the ReflectiveInjector is deprecated, but simply using the get method of the injector does not work either, because it seems to cache the classes once created and does not re-instantiate them each time I call my "instantiateWithDi" method.
Example usage
I've created a small demo at stackbliz for this: https://stackblitz.com/edit/angular-plugin-mechanism?file=src/app/plugin-execution.service.ts
Basically the magic happens here (plugin-execution.service.ts):
#Injectable({
providedIn: 'root'
})
export class PluginExecutionService {
public static readonly eventListeners = [];
constructor(private injector: Injector){}
private instantiateWithDi(parentInjector: Injector, myClass: any): any {
const reflector = ReflectiveInjector.resolveAndCreate([], parentInjector);
const newInstance = reflector.resolveAndInstantiate(myClass);
return newInstance;
}
public onApplicationEvent(event: ApplicationEvent){
const injector = Injector.create({
parent: this.injector,
providers: [{
provide: ApplicationEvent,
useValue: event
}]
});
PluginExecutionService
.eventListeners
.forEach(cls => this.instantiateWithDi(injector, cls));
}
}
This allows developers to create a class and push their class into a eventListener array. It gets executed every time, an application event occurs.
See the example "plugin" some.plugin.ts in the stackblitz example.
The real usecase is of course much more complex and involves custom decorators and stuff, but that would be quite an overkill for a demo.
You see the result in the console. The "plugins" work fine as intended. But when i build it using --prod, the app does not work any longer...
Any help is very much appreciated!
Thanks,
Manuel

Using factory to create controller

I was wondering whether I can use a factory to initialize a controller and then add it to a module. Code could look something like this, but this is not working:
const controllerFactory = {
provide: DefinitionController,
useFactory: async (service: DefinitionService) => {
//initialization of controller
return new DefinitionController();
},
inject: [DefinitionService],
};
#Module({
controllers: [controllerFactory],
providers: [DefinitionService],
})
export class DefinitionModule {}
It looks like using factories for controllers is not supported, but I am not sure. There is an example of using factory for providers, but I cannot find anything for controller in documentation or on google.
It's not possible to define your controller with an async factory comparable to custom providers. You cannot add dynamic endpoints/routes unless using the native express/fastify instance:
At the moment there is no way to register a route dynamically except
by using the internal HTTP / Fastify / Express instance
There is an issue where a dynamic routing module is discussed but this will probably not be part of nest very soon:
At the moment both Kamil and I are really busy, so this issue may take
some time - except someone else takes on the task :)
But you can use the OnModuleInit lifecycle event to do static initialization:
#Injectable()
export class DefinitionController implements OnModuleInit {
onModuleInit() {
console.log(`Initialization...`);
}
It will be called once when your app starts and has access to the injected providers in your controller, e.g. your DefinitionService.

Angular 5: Using Service from inside Custom Decorator Function

I'm creating a #Log() Decorator Function for debugging purposes;
I want that Decorator to delegate some of it's logic to a LoggingService that in turn depends on other services from the app...
I've been trying a lot of different things, the simplest/most straightforward way was to cache the Main (or Shared) Module's Injector as a static prop on the module itself (see StackBlitz example linked below), and that works for lazy-loaded modules, but not for eagerly loaded ones...
Non-working poc: https://stackblitz.com/edit/angular-j1bpvx?file=app%2Fdecorator.ts
Is there a way I could mkae use of that Service in there??
Thanks!
Class decorator is executed once on class definition. In order to avoid race condition when calling AppModule.injector.get(LoggingService) it should be moved to the place where AppModule.injector is already defined, i.e. class method.
It should be:
constructor.prototype[hook] = function (args) {
const loggingService = AppModule.injector.get(LoggingService);
loggingService.log({ ... })
...
This also creates tight coupling with AppModule and prevents the units from being reused or tested separately from it. It's recommended to use another object to hold injector property, e.g. assign injector not in main but in child module that is imported into AppModule:
export class InjectorContainerModule {
static injector: Injector;
constructor(injector: Injector) {
InjectorContainerModule.injector = injector;
}
}
Try stackblitz fixed
This will print
LoggingService: HelloComponent - ngOnInit was called
Minor changes - basically using ReflectiveInjector as in angular Injector#example
import { ReflectiveInjector } from '#angular/core';
const injector = ReflectiveInjector.resolveAndCreate([
{provide: 'loggingService', useClass: LoggingService}
]);
const loggingService = injector.get('loggingService');
I am sure you can use useExisting and use LoggingService as provider in your app module.

Angular 2+ Unit testing a service that calls a service that calls an httpclient get

I have found many examples online on how to test a service that makes an API request, but for a service that:
calls another service which makes an HTTP request and passes this
back to the original service
makes an http request based on that
I am struggling to get it to work. Here's an example of my code:
beforeEach((done) => {
TestBed.configureTestingModule({
imports: [
RouterTestingModule,
HttpClientTestingModule,
JwtModule.forRoot({
jwtOptionsProvider: {
provide: JWT_OPTIONS,
useFactory: jwtOptionsFactory,
deps: []
}
}),
],
providers: [
ConfigService,
AuthenticationService,
UserService,
JobService
]
});
let injector = getTestBed();
sut = injector.get(JobService);
httpMock = injector.get(HttpTestingController);
});
describe('Appointment Management', () =>{
it('should set an apointment', () => {
expect(sut).toBeDefined();
const dummyResponse = 'dsfsdf';
sut
.setAppointmentDate('temp', new Date(2017, 12, 31))
.subscribe(result => {
expect(result).toEqual(dummyResponse)
})
const req = httpMock.expectOne('http://localhost:32307/appointments');
expect(req.request.method).toBe('POST');
req.flush(dummyResponse);
});
})
For extra clarity of what is going on here, jobservice is under test. It has dependencies of configService and userService.
user service has dependencies of configService and authentication service.
router and jwt service are dependencies of authentication service and user service.
all services also depend on httpclient.
THE ACTUAL RESPONSE I AM GETTING:
Error: Timeout - Async callback was not invoked within timeout specified by jasmine.DEFAULT_TIMEOUT_INTERVAL.
it shouldn't even be making a real call so I am unsure why it times out. I have tried stubbing out the calls the dependencies make as well and that resulted in the same error eg
const userRequest = httpMock.expectOne('http://localhost:32307/user');
userRequest.flush('temp');
When you unit testing, you test what the service being tested does. You don't test what its dependencies do.
This means, you only have to test if the service is calling the other service, and mock the return.
Since jobService is under test, and calls config & user services (from what you wrote that I understood), you should test your function like so :
US = injector.get(UserService);
CS = injector.get(ConfigService);
it('Testing a function called myFunc in your service ...', () => {
spyOn(US, 'myUserServiceFunctionCalled')
.and.returnValue(Observable.of('The mocked answer returned by this function'))
spyOn(CS, 'myConfigServiceFunctionCalled')
.and.returnValue(Observable.of('The mocked answer returned by this function'))
sut.myFunc(); // .subscribe if it's an Observable, and put your expects in it
expect(US.myUserServiceFunctionCalled).toHaveBeenCalled();
expect(CS.myConfigServiceFunctionCalled).toHaveBeenCalled();
});

Angular 2 Shared Data Service is not working

I have built a shared data service that's designed to hold the users login details which can then be used to display the username on the header, but I cant get it to work.
Here's my (abbreviated) code:
// Shared Service
#Injectable()
export class SharedDataService {
// Observable string source
private dataSource = new Subject<any>();
// Observable string stream
data$ = this.dataSource.asObservable();
// Service message commands
insertData(data: Object) {
this.dataSource.next(data)
}
}
...
// Login component
import { SharedDataService } from 'shared-data.service';
#Component({
providers: [SharedDataService]
})
export class loginComponent {
constructor(private sharedData: SharedDataService) {}
onLoginSubmit() {
// Login stuff
this.authService.login(loginInfo).subscribe(data => {
this.sharedData.insertData({'name':'TEST'});
}
}
}
...
// Header component
import { SharedDataService } from 'shared-data.service';
#Component({
providers: [SharedDataService]
})
export class headerComponent implements OnInit {
greeting: string;
constructor(private sharedData: SharedDataService) {}
ngOnInit() {
this.sharedData.data$.subscribe(data => {
console.log('onInit',data)
this.greeting = data.name
});
}
}
I can add a console log in the service insertData() method which shoes the model being updated, but the OnInit method doesn't reflect the change.
The code I've written is very much inspired by this plunkr which does work, so I am at a loss as to what's wrong.
Before posting here I tried a few other attempts. This one and this one again both work on the demo, but not in my app.
I'm using Angular 2.4.8.
Looking through different tutorials and forum posts all show similar examples of how to get a shared service working, so I guess I am doing something wrong. I'm fairly new to building with Angular 2 coming from an AngularJS background and this is the first thing that has me truly stuck.
Thanks
This seems to be a recurring problem in understanding Angular's dependency injection.
The basic issue is in how you are configuring the providers of your service.
The short version:
Always configure your providers at the NgModule level UNLESS you want a separate instance for a specific component. Only then do you add it to the providers array of the component that you want the separate instance of.
The long version:
Angular's new dependency injection system allows for you to have multiple instances of services if you so which (which is in contrast to AngularJS i.e. Angular 1 which ONLY allowed singletons). If you configure the provider for your service at the NgModule level, you'll get a singleton of your service that is shared by all components/services etc. But, if you configure a component to also have a provider, then that component (and all its subcomponents) will get a different instance of the service that they can all share. This option allows for some powerful options if you so require.
That's the basic model. It, is of course, not quite so simple, but that basic rule of configuring your providers at the NgModule level by default unless you explicitly want a different instance for a specific component will carry you far.
And when you want to dive deeper, check out the official Angular docs
Also note that lazy loading complicates this basic rule as well, so again, check the docs.
EDIT:
So for your specific situation,
#Component({
providers: [SharedDataService] <--- remove this line from both of your components, and add that line to your NgModule configuration instead
})
Add it in #NgModule.providers array of your AppModule:
if you add it in #Component.providers array then you are limiting the scope of SharedDataService instance to that component and its children.
in other words each component has its own injector which means that headerComponentwill make its own instance of SharedDataServiceand loginComponent will make its own instance.
My case is that I forget to configure my imports to add HttpClientModule in #NgModules, it works.

Categories