I'm currently working on an Angular project and I am creating unit testing for a component using Karma + Jasmine, so I have HTML that has a ngIf calling the API Service as:
HTML
<div class="row" *ngIf="apiService.utilsService.userBelongsTo('operations')"></div">
The service in the *ngIf is the service that I want to mock on the spec.ts below
TS
export class CashFlowSalariesComponent implements OnInit, OnChanges {
constructor(
public apiService: ApiService,
) {}
SPECT.TS
describe('CashFlowSalariesComponent', () => {
let fixture: ComponentFixture < CashFlowSalariesComponent > ;
let mockApiService;
let data;
beforeEach(async(() => {
data = [{
id: 1006,
role: "Developer",
...
}]
mockApiService = jasmine.createSpyObj(['userBelongsTo'])
TestBed.configureTestingModule({
schemas: [CUSTOM_ELEMENTS_SCHEMA],
imports: [
RouterTestingModule,
FormsModule,
ReactiveFormsModule,
BrowserModule,
HttpClientTestingModule,
ToastrModule.forRoot({
positionClass: 'toast-bottom-right'
})
],
declarations: [
CashFlowSalariesComponent,
],
providers: [{
provide: ApiService,
useValue: mockApiService
}, UserService, ProfileService, VettingStatusService, ApplicationRoleService,
SeniorityLevelService, PlacementStatusService, EducationLevelService, UtilsService, ShirtSizeService,
CountryService, CityService, PostalCodeService, StateService, ClientSectorService, JobService, ProfileActivityService, ProfileSalaryActivityService, ClientService, RequestTimeOffService, TimeOffTypeService, PulsecheckDetailService, PulsecheckMasterService,
PulsecheckQuestionService, ExpenseService, DepartmentService, ExchangeRateService, SkillCategoriesService, ProfileRoleService,
ToastrService
]
})
fixture = TestBed.createComponent(CashFlowSalariesComponent);
}));
it('should set salaries data correctly', () => {
mockApiService.userBelongsTo.and.returnValue(of('operations'))
debugger;
fixture.detectChanges();
})
As you see, I tried to create mock of api service as: mockApiService = jasmine.createSpyObj(['userBelongsTo']), then use in the it as: mockApiService.userBelongsTo.and.returnValue(of('operations')), but when I debug it throws as unknown as the following picture
and the test return the following error:
Cannot read properties of undefined (reading 'userBelongsTo')
I do not know if this happen because userBelongs to it is inside another service inside apiService:
ApiService
#Injectable()
export class ApiService {
public utilsService: UtilsService;
constructor(private injector: Injector) {
this.utilsService = injector.get(UtilsService);
}
}
Utils.Service:
userBelongsTo(groupName: string) {
return this.groups.split(',').reduce((c, g) => c || g.toUpperCase() == groupName.toUpperCase(), false);
}
How can I make this work? Regards
Dependency injections should be private, why does the template HTML needs to handle the service call? Instead of delegating the service call to the template, make a proper function in the component, as a result, your template will be cleaner
<div class="row" *ngIf="getBelongsTo()"></div">
constructor(private apiService: ApiService) {}
getBelongsTo(): boolean {
// I guess userBelongsTo returns either a string or undefined,
// so using !! creates a valid boolean
// if userBelongsTo returns a valid string, returns TRUE
// if returns undefined/null returns FALSE
return !!this.apiService.utilsService.userBelongsTo('operations');
}
For testing, you need to provide the mock/fake value from userBelongsTo before starting to test. Additionally, the way you are mocking the service is wrong, it could be like the following:
const mockApiService = jasmine.createSpyObj<ApiService>('ApiService', ['userBelongsTo']);
let data;
beforeEach(() => {
data = [{ id: 1006, role: "Developer", ...}];
mockApiService.userBelongsTo.and.returnValue(of('operations'));
}
beforeEach(async() => {
TestBed.configureTestingModule({
declarations: [...],
provides: [{provide: ApiService, useValue: mockApiService}]
})
})
I do not know if this happen because userBelongs to it is inside another service inside apiService
When unit testing, you don't care how any dependecy is implemented when is injected in the component, you are mocking all dependencies so it doesn't matter.
One thing to notice, since you provided how ApiService is implemented, keep in mind that somewhere, in some module, ApiService needs to be added in the providers array since it isn't provided as root, e.x: #Injectable({ provideIn: 'root' })
I have my TestBed configuration with the following provider:
{
provide: ActivatedRoute,
useValue: {
queryParams: of({fooObservable: true})
}
},
how can I write a jasmine spy that accesses this query parameter and tests whether it's true or false? I have done the following but I can't access it in order to test it.
fooSpy = spyOn(this.ActivatedRoute.queryParams, 'queryParams');
This is tricky. Here is how I did it:
const spyParamMap = jasmine.createSpyObj({get: null});
const mockActivatedRoute = { paramMap: of(spyParamMap) };
Then in providers array: { provide: ActivatedRoute, useValue: mockActivatedRoute }
And finally in tests:
spyParamMap.get.and.returnValue('tFilename');
fixture.detectChanges();
In my ngOnInit() I have the following snippet:
this.route.paramMap.subscribe(params => {
this.dlFilename = params.get('download');
if (this.dlFilename) {
// logic for if a filename is passed
}
so I can pass in a result to params.get() and test my logic inside the if.
I hope this helps.
Is it possible to spyOn helper class? In the below code, StatefulPatternService.init() is calling a WebSocketHelper.
I would like to spyOn WebSocketHelper and mock the subscribeFn
export class WebSocketHelper{
private url: string;
constructor(url){
this.url = url;
init();
}
init(){
// init websocket & other login
}
}
#Injectable({
providedIn: 'root'
})
export class StatefulPatternService {
constructor(){}
private callback(_data){ }
init(){
let wsHelper = new WebSocketHelper('/subscribe/topic'); // <-- How to spyOn???
wsHelper.subscribeFn = this.callback;
// ...
}
}
If spyOn won't be possible, then how it can be re-written so that this test can be covered?
Your challenge will be getting a hold of 'wsHelper' in order to spy on it. One thought: can you refactor to make wsHelper a class-scope variable instead? Then you could spyOn it when you get the service in the test suite, for example, something like:
service = TestBed.get(StatefulPatternService);
let spy = spyOn(service.wsHelper, 'subscribeFn');
Update
From the comments to my answer it looks like what you are really trying to do is verify that the constructor was called with the proper url. Since you are saving that in a class variable, there should be no need to spy on the constructor, but rather just test the value of the saved variable. As I mentioned in the comments, to do this you will need two things: to make wsHelper a class level variable, and to add a method on the WebSocketHelper class that returns the value of the private variable 'url' so you can test it. I've set up a stackblitz to demonstrate what I'm talking about here: STACKBLITZ Here is a snippet from that stackblitz:
describe('MyService', () => {
let myService: StatefulPatternService;
beforeEach(() => {
TestBed.configureTestingModule({
imports: [/* any imports needed */],
providers: [ StatefulPatternService ]
});
myService = TestBed.get(StatefulPatternService);
});
it('should be createable', () => {
expect(myService).toBeTruthy();
});
it('should construct wsHelper properly', () => {
myService.init();
expect(myService.wsHelper.get_url()).toEqual('/subscribe/topic');
})
});
I have a constructor that uses this:
constructor(
public core: CoreComponent,
) {
//
}
But when i try to test these components with a testcase like this:
describe('CoreSubMenuComponent', () => {
let component: CoreSubMenuComponent;
let fixture: ComponentFixture<CoreSubMenuComponent>;
beforeEach(async(() => {
TestBed.configureTestingModule({
imports: [
TestingModule.forRoot(),
]
}).compileComponents();
}));
beforeEach(() => {
fixture = TestBed.createComponent(CoreSubMenuComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});
I get a null injector error:
Error: StaticInjectorError(DynamicTestModule)[CoreSubMenuComponent -> CoreComponent]:
StaticInjectorError(Platform: core)[CoreSubMenuComponent -> CoreComponent]:
NullInjectorError: No provider for CoreComponent!
It is imported into the testingModule so that isn't the issue. I also tried adding it to the providers array. but that just moves it to the angular renderer which can't be added to the providers array. I also added an #injectable() tag before the #component tag in the CoreComponent but it doesn't change my tests failing.
I tried getting it with #host() but it throws an error. I also tried to get it with the Injector but that also throws an error. These are 4 components that use the CoreComponent as a var to get some info from them but they make the tests fail. All my other tests are pass just fine with over 80 tests. Just these 4 won't work because they try to get properties that are defined in the CoreComponent.
Does someone know how i could reference this component with an instance so that my tests will pass?
To fix this error use #host en #optional decorators. this tells the compiler to look for it but not require if it isn't provided:
constructor(
#Host() #Optional() public core: CoreComponent,
) {
//
}
I want to know how to mock ActivatedRoute url.
I am getting current url by using ActivatedRoute
this.activatedRoute.url.subscribe(url => {
this.isEdit = false;
if (url[0].path === 'updatecoc') {
this.isEdit = true;
}
});
So I want to mock url in ActivatedRoute
I have tried this
let fakeActivatedRoute = new MockActivatedRoute();// MockActivatedRoute I have implemented this class from MockActivatedRoute class
fakeActivatedRoute.parent = new MockActivatedRoute();
let urlSegment: UrlSegment[] = [];
urlSegment.push({ path: "updatecoc", parameters: {} });
fakeActivatedRoute.url = Observable.of(urlSegment);
TestBed.configureTestingModule({ { provide: ActivatedRoute, useValue: fakeActivatedRoute }})
But I am getting error:
unable to get property 'subscribe' of undefined or null reference
I don't know where I am missed. How can I resolve this issue?
I have a better solution for you :
import { RouterTestingModule } from '#angular/router/testing';
TestBed.configureTestingModule({
imports: [RouterTestingModule],
// ...
})
.compileComponents();
This will mock your whole routing module, now you can inject your dummy mock into your providers and spy on the functions like so
providers: [{provide: ActivatedRoute, useValue: {}}]
And when you test a function calling, let's say, myMock (I know it's not in it, it's for the example) :
const mock = TestBed.get(ActivatedRoute);
spyOn(mock, 'myMock').and.returnValue(/* what you want */);
// ...
expect(mock.myMock).toHaveBeenCalledWith(/* params here */);
EDIT I quickly looked at what url is made of, here is your mock :
mock.url = jasmine
.createSpy()
.and
.returnValue(new BehaviorSubject({
path: 'yout-mocked-path',
parameters: {/* your mocked parameters */}
})));