testing a service call in jasmine - javascript

I am trying to write a unit-test for a function that calls a service. But am running into the error: TypeError: undefined is not a constructor
What I am trying to test is a service call that, on success, sets the value of the variable 'cards'.
I've created the appropriate mock for the service (CardService), which you can see in the spec file below
test.component.spec.ts
class MockCardService extends CardService {
constructor() {
super(null); // null is the http in service's constructor
}
getCardDetails(): any {
return Observable.of([{ 0: 'card1' }, { 1: 'card2' }]);
}
}
describe('MyComponent', () => {
let component: MyComponent;
let mockCardService: MockCardService;
beforeEach(() => {
mockCardService = new MockCardService();
component = new MyComponent(
mockCardService // add the mock service that runs before each test
);
});
// The failing test :(
it('should set the card variable to the value returned by the service', () => {
spyOn(mockCardService, 'getCardDetails').and.callThrough();
// Act
component.ngOnInit();
component.updateCards(); // call the function I am testing
// Assert
expect(component.cards).toConTainText('Card1');
});
And the component file with the function I'm testing:
export class MyComponent implements OnInit {
public cards: CardModel[] = [];
constructor(
private cardService: CardService,
) {
}
ngOnInit() {
this.updateCards(); // call the update card service
}
updateCards(): void {
this.cardService.getCardDetails().subscribe(
(cardsDetails) => {
this.cards = cardsDetails;
},
(err) => {
// todo: error handling
console.log(err);
}
);
}
}
Whenever this test runs I recieve the error:
TypeError: undefined is not a constructor (evaluating 'Observable_1.Observable.of([{ 0: 'card1' }, { 1: 'card2' }])') (line 22)
getCardDetails
updateCards
ngOnInit
And I can't figure out what I'm doing wrong, why 'getCardDetals.subscribe' is undefined. The MockCardService class I provided doesn't appear to be working for some reason.
(note that this.cardService.getCardDetails() is defined, if I log it out in the component itself )
Any help would be very much appreciated.

Author here:
I'm still not sure what is going wrong. But I was able to fix this by changing class MockCardService extends CardService { to just let MockCardService, and using that variable throughout. Good luck to anyone who runs into this!

MockCardService.getCardDetails() should return an Observable, so you can run subscribe in the component.

Related

TypeError: _API.default is not a constructor with Jest tests

I have an API class that I am trying to use in a React app.
// API file
class API {
...
}
export default API;
// Other file
import API from "utils/API";
const api = new API();
And I am getting the error:
TypeError: _API.default is not a constructor
But.. it seems like my default is set?
My Jest setup is like this:
"jest": {
"setupFiles": [
"./jestSetupFile.js"
],
"testEnvironment": "jsdom",
"preset": "jest-expo",
"transformIgnorePatterns": [
"node_modules/(?!((jest-)?react-native|#react-native(-community)?)|expo(nent)?|#expo(nent)?/.*|#expo-google-fonts/.*|react-navigation|#react-navigation/.*|#unimodules/.*|unimodules|sentry-expo|native-base|react-native-svg|react-router-native/.*|#invertase/react-native-apple-authentication/.*)"
]
},
My strong guess is that this is due to a configuration of my babel, webpack or package.json.
What could be causing this?
Note, I want to be clear, this doesn't happen whatsoever in my main application, only in Jest testing
If I change it to a named export/import, I get this:
TypeError: _API.API is not a constructor
Extremely confusing behavior.
As mentioned by others, it would be helpful to see a minimum reproducible example.
However, there is one other possible cause. Are you mocking the API class in your test file at all? This problem can sometimes happen if a class is mistakenly mocked as an "object" as opposed to a function. An object cannot be instantiated with a "new" operator.
For example, say we have a class file utils/API like so:
class API {
someMethod() {
// Does stuff
}
}
export default API;
The following is an "incorrect" way to mock this class and will throw a TypeError... is not a constructor error if the class is instantiated after the mock has been created.
import API from 'utils/API';
jest.mock('utils/API', () => {
// Returns an object
return {
someMethod: () => {}
};
})
// This will throw the error
const api = new API();
The following will mock the class as a function and will accept the new operator and will not throw the error.
import API from 'utils/API';
jest.mock('utils/API', () => {
// Returns a function
return jest.fn().mockImplementation(() => ({
someMethod: () => {}
}));
})
// This will not throw an error anymore
const api = new API();
Trying adding "esModuleInterop": true, in your tsconfig.json. BY default esModuleInterop is set to false or is not set. B setting esModuleInterop to true changes the behavior of the compiler and fixes some ES6 syntax errors.
Refer the documentation here.
This was ultimately due to additional code inside the file that I was exporting the class from.
import { store } from "root/App";
if (typeof store !== "undefined") {
let storeState = store.getState();
let profile = storeState.profile;
}
At the top, outside my class for some functionality I had been working on.
This caused the class default export to fail, but only in Jest, not in my actual application.
You'll need to export it like this :
export default class API
You could try with:
utils/API.js
export default class API {
...
}
test.js
import API from "utils/API";
const api = new API();
I'm adding this because the issue I had presented the same but has a slightly different setup.
I'm not exporting the class with default, i.e.
MyClass.ts
// with default
export default MyClass {
public myMethod()
{
return 'result';
}
}
// without default, which i'm doing in some instances.
export MyClass {
public myMethod()
{
return 'result';
}
}
When you don't have the default, the import syntax changes.
In a (jest) test if you follow the convention where you do have export default MyClass(){};
then the following works.
const MOCKED_METHOD_RESULT = 'test-result'
jest.mock("MyClass.ts", () => {
// will work and let you check for constructor calls:
return jest.fn().mockImplementation(function () {
return {
myMethod: () => {
return MOCKED_METHOD_RESULT;
},
};
});
});
However, if you don't have the default and or are trying to mock other classes etc. then you need to do the following.
Note, that the {get MyClass(){}} is the critical part, i believe you can swap out the jest.fn().mockImplementation() in favour of jest.fn(()=>{})
jest.mock("MyClass.ts", () => ({
get MyClass() {
return jest.fn().mockImplementation(function () {
return {
myMethod: () => {
return MOCKED_METHOD_RESULT;
},
};
});
},
}));
So the issue is the way in which you access the contents of the class your mocking. And the get part allows you to properly define class exports.
I resolved this error by using below code.
jest.mock('YOUR_API_PATH', () => ({
__esModule: true,
default: // REPLICATE YOUR API CONSTRUCTOR BEHAVIOUR HERE BY ADDING CLASS
})
If you want to mock complete API class, please check the below snippet.
jest.mock('YOUR_API_PATH', () => ({
__esModule: true,
default: class {
constructor(args) {
this.var1 = args.var1
}
someMethod: jest.fn(() => Promise.resolve())
},
}));

What's an angular lifecycle-hook need when changing sets in componentInstance property by service?

I have a component that i send to MdDialog(Angular Material Dialog in my custom service.ts)
dialogRef = this.dialog.open(component, config);
And when I change a public property of this component by componentInstance like that:
dialogRef.componentInstance.task = task;
Angular shows me an error:
Error: ExpressionChangedAfterItHasBeenCheckedError: Expression has changed after it was checked. Previous value: 'undefined'. Current value: 'dialog'. It seems like the view has been created after its parent and its children have been dirty checked. Has it been created in a change detection hook ?
Full code of open-modal.service.ts
#Injectable()
export class TasksPopupService {
constructor(
private dialog: MdDialog,
private router: Router,
private tasksService: TasksService
) { }
public open(component: any, id?: string) {
if (id) {
this.tasksService.find(id)
.subscribe(task => {
this.bindDialog(component, task);
});
} else {
this.bindDialog(component, new Task());
}
}
bindDialog(component, task: Task) {
let dialogRef;
let config = new MdDialogConfig();
config.height = '80%';
config.width = '70%';
dialogRef = this.dialog.open(component, config);
dialogRef.componentInstance.task = task;
dialogRef.afterClosed().subscribe(res => {
this.router.navigate([{ outlets: { popup: null } }], { replaceUrl: true });
});
return dialogRef;
}
}
But an error occured only if id is undefined (in ELSE block) I think it's because of this.tasksService.find return Observable (async), and block ELSE is not async. But I'm not sure.
I has some confuse becouse error eccured in MdContainer of Angular Material.
If i get data from server it's need some time, but when i pass a new object it's occur fast and change detection is not finished if i understend right.
Also, it's not parent/child component and lifecycle hooks maybe not works as we expect.
I found solution, but it's not right. Just fast solution.
if (id) {
this.tasksService.find(id)
.subscribe(task => {
this.bindDialog(component, task);
});
} else {
Observable.of(new Task()).delay(300).subscribe(task => {
this.bindDialog(component, task);
});
}
I use delay for change detection has finished and error will not throw.

Angular2 component's "this" is undefined when executing callback function

I have a component which calls a service to fetch data from a RESTful endpoint. This service needs to be given a callback function to execute after fetching said data.
The issue is when I try use the callback function to append the data to the existing data in a component's variable, I get a EXCEPTION: TypeError: Cannot read property 'messages' of undefined. Why is this undefined?
TypeScript version: Version 1.8.10
Controller code:
import {Component} from '#angular/core'
import {ApiService} from '...'
#Component({
...
})
export class MainComponent {
private messages: Array<any>;
constructor(private apiService: ApiService){}
getMessages(){
this.apiService.getMessages(gotMessages);
}
gotMessages(messagesFromApi){
messagesFromApi.forEach((m) => {
this.messages.push(m) // EXCEPTION: TypeError: Cannot read property 'messages' of undefined
})
}
}
Use the Function.prototype.bind function:
getMessages() {
this.apiService.getMessages(this.gotMessages.bind(this));
}
What happens here is that you pass the gotMessages as a callback, when that is being executed the scope is different and so the this is not what you expected.
The bind function returns a new function that is bound to the this you defined.
You can, of course, use an arrow function there as well:
getMessages() {
this.apiService.getMessages(messages => this.gotMessages(messages));
}
I prefer the bind syntax, but it's up to you.
A third option so to bind the method to begin with:
export class MainComponent {
getMessages = () => {
...
}
}
Or
export class MainComponent {
...
constructor(private apiService: ApiService) {
this.getMessages = this.getMessages.bind(this);
}
getMessages(){
this.apiService.getMessages(gotMessages);
}
}
Or you can do it like this
gotMessages(messagesFromApi){
let that = this // somebody uses self
messagesFromApi.forEach((m) => {
that.messages.push(m) // or self.messages.push(m) - if you used self
})
}
Because you're just passing the function reference in getMessages you don't have the right this context.
You can easily fix that by using a lambda which automatically binds the right this context for the use inside that anonymous function:
getMessages(){
this.apiService.getMessages((data) => this.gotMessages(data));
}
I have same issue, resolved by using () => { } instead function()
Please define function
gotMessages = (messagesFromApi) => {
messagesFromApi.forEach((m) => {
this.messages.push(m)
})
}

Angular2 'this' is undefined

I have a code that looks like this:
export class CRListComponent extends ListComponent<CR> implements OnInit {
constructor(
private router: Router,
private crService: CRService) {
super();
}
ngOnInit():any {
this.getCount(new Object(), this.crService.getCount);
}
The ListComponent code is this
#Component({})
export abstract class ListComponent<T extends Listable> {
protected getCount(event: any, countFunction: Function){
let filters = this.parseFilters(event.filters);
countFunction(filters)
.subscribe(
count => {
this.totalItems = count;
},
error => console.log(error)
);
}
And the appropriate service code fragment from CRService is this:
getCount(filters) {
var queryParams = JSON.stringify(
{
c : 'true',
q : filters
}
);
return this.createQuery(queryParams)
.map(res => res.json())
.catch(this.handleError);
}
Now when my ngOnInit() runs, I get an error:
angular2.dev.js:23925 EXCEPTION: TypeError: Cannot read property
'createQuery' of undefined in [null]
ORIGINAL EXCEPTION:
TypeError: Cannot read property 'createQuery' of undefined
So basically, the this in the return this.createQuery(queryParams) statement will be null. Does anybody have an idea how is this possible?
The problem is located here:
gOnInit():any {
this.getCount(new Object(), this.crService.getCount); // <----
}
Since you reference a function outside an object. You could use the bind method on it:
this.getCount(new Object(), this.crService.getCount.bind(this.crService));
or wrap it into an arrow function:
this.getCount(new Object(), (filters) => {
return this.crService.getCount(filters));
});
The second approach would be the preferred one since it allows to keep types. See this page for more details:
https://basarat.gitbooks.io/typescript/content/docs/tips/bind.html
To fix this error I yanked all the innards out of my function causing the error and threw it in another function then the error went away.
example:
I had this function with some code in it
this.globalListenFunc = renderer.listenGlobal('document', 'click', (event) => {
// bunch of code to evaluate click event
// this is the code that had the "this" undefined error
});
I pulled the code out and put it in an external public function, here's the finished code:
this.globalListenFunc = renderer.listenGlobal('document', 'click', (event) => {
this.evaluateClick(event);
});
evaluateClick(evt: MouseEvent){
// all the code I yanked out from above
}

AngularJs 2: How to debug a service call? (es6 syntax)

THE SITUATION:
I am just learning Angular2. I wanted to debug the call to a service.
The service has been properly called, as I can see in the view.
I also want to log to content of variable that hold the result, but it is not working as i would except.
THE SERVICE:
export class ListService
{
getList()
{
return Promise.resolve(LIST);
}
}
THE CALL (from the list component):
export class ListComponent implements OnInit
{
list: Item[];
constructor(
private _router: Router,
private _listService: ListService
) { }
getList()
{
this._listService.getList().then(list => this.list = list);
}
ngOnInit()
{
this.getList();
}
}
LOG ATTEMPT 1:
list is the variable containing the list of items. In the view it properly show the content. So i would expect to log it and see its content.
getList()
{
this._listService.getList().then(list => this.list = list);
console.log(list)
}
But when i get this error instead:
EXCEPTION: ReferenceError: list is not defined in [null]
LOG ATTEMPT 2:
Trying to get the correct syntax:
getList()
{
this._listService.getList().then(list => this.list = list);
console.log(this.list)
}
There are no errors now. But in the console it shows undefined.
But the service has already been called. So it should contain the content
THE QUESTION:
Considering that i am using Angular2 - Ecmascript 2016
What is the proper syntax to log the call of a service?
Thank you!
In fact this.list is set within the callback registered in the then method. So you should use something like that:
getList()
{
this._listService.getList().then(list => {
this.list = list;
console.log(list);
console.log(this.list);
});
}
If you put the console.log(this.list); right the promise, the result won't be probably there so this.list probably doesn't set...

Categories

Resources