Testing Service with Mongoose in NestJS - javascript

I am trying to test my LoggingService in NestJS and while I cannot see anything that is wrong with the test the error I am getting is Error: Cannot spy the save property because it is not a function; undefined given instead
The function being tested (trimmed for brevity):
#Injectable()
export class LoggingService {
constructor(
#InjectModel(LOGGING_AUTH_MODEL) private readonly loggingAuthModel: Model<IOpenApiAuthLogDocument>,
#InjectModel(LOGGING_EVENT_MODEL) private readonly loggingEventModel: Model<IOpenApiEventLogDocument>,
) {
}
async authLogging(req: Request, requestId: unknown, apiKey: string, statusCode: number, internalMsg: string) {
const authLog: IOpenApiAuthLog = {
///
}
await new this.loggingAuthModel(authLog).save();
}
}
This is pretty much my first NestJS test and as best I can tell this is the correct way to test it, considering the error is right at the end it seems about right.
describe('LoggingService', () => {
let service: LoggingService;
let mockLoggingAuthModel: IOpenApiAuthLogDocument;
let request;
beforeEach(async () => {
request = new JestRequest();
const module: TestingModule = await Test.createTestingModule({
providers: [
LoggingService,
{
provide: getModelToken(LOGGING_AUTH_MODEL),
useValue: MockLoggingAuthModel,
},
{
provide: getModelToken(LOGGING_EVENT_MODEL),
useValue: MockLoggingEventModel,
},
],
}).compile();
service = module.get(LoggingService);
mockLoggingAuthModel = module.get(getModelToken(LOGGING_AUTH_MODEL));
});
it('should be defined', () => {
expect(service).toBeDefined();
});
it('authLogging', async () => {
const reqId = 'mock-request-id';
const mockApiKey = 'mock-api-key';
const mockStatusCode = 200;
const mockInternalMessage = 'mock-message';
await service.authLogging(request, reqId, mockApiKey, mockStatusCode, mockInternalMessage);
const authSpy = jest.spyOn(mockLoggingAuthModel, 'save');
expect(authSpy).toBeCalled();
});
});
The mock Model:
class MockLoggingAuthModel {
constructor() {
}
public async save(): Promise<void> {
}
}

After much more googling I managed to find this testing examples Repo: https://github.com/jmcdo29/testing-nestjs which includes samples on Mongo and also suggest that using the this.model(data) complicates testing and one should rather use `this.model.create(data).
After making that change the tests are working as expected.

The issue comes from the fact that you pass a class to the TestingModule while telling it that it's a value.
Use useClass to create the TestingModule:
beforeEach(async () => {
request = new JestRequest();
const module: TestingModule = await Test.createTestingModule({
providers: [
LoggingService,
{
provide: getModelToken(LOGGING_AUTH_MODEL),
// Use useClass
useClass: mockLoggingAuthModel,
},
{
provide: getModelToken(LOGGING_EVENT_MODEL),
// Use useClass
useClass: MockLoggingEventModel,
},
],
}).compile();
service = module.get(LoggingService);
mockLoggingAuthModel = module.get(getModelToken(LOGGING_AUTH_MODEL));
});

Related

How to test code block inside observable subscription for Angular service

I have an Angular service method that uses another service for making api call. After the call success and inside of the subscription, I call some methods e.g. notifySucess, emit new value...
My question is how can I test the code block inside the subscription e.g. expect notifySucess to have been called. I put a break point inside the subscription, but the code execution does not stop there.
For testing component, I know there is 'fixture.detectChanges()' method to be applied. For service testing, are there any similar mechanism?
resource.service.ts
#Injectable({
providedIn: 'root',
})
export class ResourceService {
constructor(
private http: HttpClient,
) {}
putData(newData: Data): Observable<Data> {
return this.http.put<Data>('/api/put-endpoint', newData);
}
}
store.service.ts
#Injectable({
providedIn: 'root',
})
export class StoreService {
constructor(
private resource: ResourceService,
private notificationService: NotificationService,
)
saveChanges(newData: Data) {
this.resourceServie.putData(newData).subscribe(() => {
this.notificationService.notifySuccess('Save changes successfully');
// do something else
}
}
store.service.spec.ts
describe('StoreService', () => {
let storeService: StoreService;
let resourceService: jasmine.SpyObj<ResourceService>;
let notificationService: jasmine.SpyObj<NotificationService>;
notificationService = jasmine.createSpyObj('NotificationService', ['notifySuccess']);
resourceService = jasmine.createSpyObj('ResourceService', ['putData']);
beforeEach(() => {
TestBed.configureTestingModule({
providers: [
{ provide: ResourceService, useValue: resourceService },
{
provide: NotificationService,
useValue: notificationService,
},
],
});
});
it('should saveChanges', () => {
const newData: Data = {foo: bar};
resourceService.putData.and.returnValue(of(newData));
storeService.putData(newData);
expect(resourceService.putData).toHaveBeenCalledWith(newData); // PASS
expect(notificationService.notifySuccess).toHaveBeenCalledWith(Save changes successfully); // FAIL as the code block inside the subscription does not run
});
})
Your spy need to return some value for triggering subscription.
More advanced information about spy you can read here
You need to use:
const newData: Data = {foo: bar};
resourceService = spyOn(resourceService,'putData').and.returnValue(newData);
instead of:
resourceService = jasmine.createSpyObj('ResourceService', ['putData']);
You also can see the similar post - Angular - unit test for a subscribe function in a component
Try to implement store.service.spec.ts in this way:
describe('StoreService', () => {
let storeService: StoreService;
let resourceService: ResourceService;
let notificationService: NotificationService;
const newData: Data = {foo: bar};
resourceService = spyOn(resourceService,'putData').and.returnValue(newData);
resourceService = spyOn(notificationService,'notifySuccess').and.returnValue('Save changes successfully');
beforeEach(() => {
TestBed.configureTestingModule({
providers: [],
});
resourceService= TestBed.inject(ResourceService);
notificationService= TestBed.inject(NotificationService);
storeService = TestBed.inject(storeService);
});
it('should saveChanges', () => {
storeService.saveChanges(newData);
expect(resourceService.putData).toHaveBeenCalledWith(newData);
expect(notificationService.notifySuccess).toHaveBeenCalledWith(Save changes successfully); /
});
})

Error: Need to call TestBed.initTestEnvironment() first

I'm trying do a test in angular of a service.
This is my part of the code
describe('AddressService', () => {
let service: AddressService;
let injector: TestBed;
let httpTestingController: HttpTestingController;
beforeEach(() => {
TestBed.configureTestingModule({
imports: [HttpClientTestingModule],
providers: [AddressService]
});
injector = getTestBed();
service = injector.inject(AddressService);
httpTestingController = injector.inject(HttpTestingController);
// service = TestBed.inject(AddressService);
});
afterEach(() => {
httpTestingController.verify();
})
httpTestingController = TestBed.inject(HttpTestingController);
it('should be created', () => {
expect(service).toBeTruthy();
});
const dummyAddressListResponse = {
data: [
{direccion: 'address1'}, {Colas: 'queue1'},
{direccion: 'address2'}, {Colas: 'queue2'}
],
};
it('getAddress() should return data', () => {
service.getAddress().subscribe((res) => {
expect(res).toEqual(dummyAddressListResponse);
});
const req = httpTestingController.expectOne(`${environment.URI}/mock-address`);
expect(req.request.method).toBe('GET');
req.flush(dummyAddressListResponse);
})
});
At the moment of run the test ng test --main src/app/services/address/address.service.spec.ts
I'm seeing this error Error: Need to call TestBed.initTestEnvironment() first
I have searched and don't see any solution, Has it happened to someone?
For jest users - just add the following code in setup-jest.js.
Because jest needs to be initialized.
import { TestBed } from "#angular/core/testing";
import { BrowserDynamicTestingModule, platformBrowserDynamicTesting } from "#angular/platform-browser-dynamic/testing";
TestBed.initTestEnvironment(BrowserDynamicTestingModule, platformBrowserDynamicTesting());
the first thing: --main shouldn't be used, it points to an entrypoint, not to a desired test, and should be src/test.ts.
To run a single test use the next command:
ng test --include "app/services/address/address.service.spec.ts"
The test should be a bit different:
describe('AddressService', () => {
let service: AddressService;
let injector: TestBed;
let httpTestingController: HttpTestingController;
beforeEach(async () => {
// let's compile TestBed first.
await TestBed.configureTestingModule({
imports: [HttpClientTestingModule],
providers: [AddressService],
}).compileComponents();
// let's use TestBed.injector.
service = TestBed.inject(AddressService);
httpTestingController = TestBed.inject(HttpTestingController);
});
afterEach(() => {
httpTestingController.verify();
})
it('should be created', () => {
expect(service).toBeTruthy();
});
it('getAddress() should return data', () => {
const dummyAddressListResponse = {
data: [
{direccion: 'address1'}, {Colas: 'queue1'},
{direccion: 'address2'}, {Colas: 'queue2'}
],
};
let actual: any;
service.getAddress().subscribe((res) => actual = res);
const req = httpTestingController.expectOne(`${environment.URI}/mock-address`);
expect(req.request.method).toBe('GET');
req.flush(dummyAddressListResponse);
expect(actual).toEqual(dummyAddressListResponse);
});
});
Is this using ng-packagr (i.e. an angular library)? If so you might want to check that there are no node_modules under the ./project/ folder.
This was throwing me this exact same error. The moment I deleted the node_modules under the project folder it all started to work again.
Source: https://github.com/ngneat/spectator/issues/546
The problem is that it must run everything from test.ts.
So instead of run ng test --main src/app/services/address/address.service.spec.ts command, just change const context = require.context('./', true, /\.spec\.ts$/); to const context = require.context('./', true, /address\.service\.spec\.ts$/); and use ng test command.

NestJS accessing private class field before testing method with jest

Assuming there is the following nest service class with the private field myCache and the public method myFunction:
import * as NodeCache from 'node-cache'
class MyService{
private myCache = new NodeCache();
myFunction() {
let data = this.myCache.get('data');
if(data === undefined){
// get data with an http request and store it in this.myCache with the key 'data'
}
return data;
}
}
I want to test the function myFunction for two different cases.
Fist case: If condition is true. Second Case: If condition is false.
Here is the test class with the two missing tests:
import { Test, TestingModule } from '#nestjs/testing';
import { MyService} from './myService';
describe('MyService', () => {
let service: MyService;
beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
providers: [MyService],
}).compile();
service = module.get<MyService>(MyService);
});
it('should be defined', () => {
expect(service).toBeDefined();
});
describe('myFunction', () => {
it('should return chached data', () => {
// first test
}),
it('should return new mocked data', () => {
// second test
})
})
});
Therefore I guess I have to access or mock the myCache private class field.
Because it is private I can't access it in the test class.
My Question is: What's the best and correct way to achieve this?
If you're just looking to mock it, you can always use as any to tell Typescript to not warn you about accessing private values.
jest.spyOn((service as any).myCache, 'get').mockReturnValueOnce(someValue);
However, that's kind of annoying to have to do over and over again and not really the best practice. What I would do instead is move your cache to be an injectable provider so that it could be swapped out at a moments notice and your MyService no longer has a hard dependency on node-cache. Something like this:
// my.module.ts
#Module({
providers: [
MyService,
{
provide: 'CACHE',
useClass: NodeCache
}
]
})
export class MyModule {}
// my.service.ts
#Injectable()
export class MyService {
constructor(#Inject('CACHE') private readonly myCache: NodeCache) {}
...
And now in your test you can swap out the CACHE token for a mock implementation that can also be retrieved in your beforeEach block, meaning no more any.
describe('MyService', () => {
let service: MyService;
let cache: { get; set; }; // you can change the type here
beforeEach(async () => {
const modRef = await Test.createTestingModule({
providers: [
MyService,
{
provide: 'CACHE',
useValue: { get: jest.fn(), set: jest.fn() }
}
]
}).compile();
service = modRef.get(MyService);
cache = modRef.get<{ get; set; }>('CACHE');
});
});
And now you can call jest.spyOn(cache, 'get') without the use of as any.

Observable "source is deprecated" error on npm lint

I get "source is deprecated: This is an internal implementation detail, do not use." when I run the command npm lint on my code below:
set stream(source: Observable<any>) {
this.source = source;
}
If I take it out, it satisfies the lint, but it breaks my unit tests. Why is this?
If you are testing effects, you need to update the approach. I have changed using the provideMockActions, the action would be an let actions$: Observable;
fdescribe('PizzaEffects', () => {
let actions$: Observable;;
let service: Service;
let effects: PizzaEffects;
const data = givenPizzaData();
beforeEach(() => {
TestBed.configureTestingModule({
imports: [ApolloTestingModule],
providers: [
Service,
PizzaEffects,
Apollo,
// { provide: Actions, useFactory: getActions }, remove
provideMockActions(() => actions$),
]
});
actions$ = TestBed.get(Actions);
service = TestBed.get(Service);
effects = TestBed.get(PizzaEffects);
spyOn(service, 'loadData').and.returnValue(of(data));
});
describe('loadPizza', () => {
it('should return a collection from LoadPizzaSuccess', () => {
const action = new TriggerAction();
const completion = new LoadPizzaSuccess(data);
actions$ = hot('-a', { a: action });
const expected = cold('-b', { b: completion });
expect(effects.getPizzaEffect$).toBeObservable(expected);
});
});
});

Angular 2 JWT Unit Testing

My API calls are authenticated with JWT. I am trying to write code for a service method. All requests has this interceptor:
public interceptBefore(request: InterceptedRequest): InterceptedRequest {
// Do whatever with request: get info or edit it
this.slimLoadingBarService.start();
let currentUser = JSON.parse(localStorage.getItem('currentUser'));
if (currentUser && currentUser.data.token) {
request.options.headers.append('Authorization', 'Bearer ' + currentUser.data.token);
}
return request;
}
Service method that I want to test:
getAll(page: number, pageSize: number, company: string): Observable<any> {
return this.http.get(`${this.conf.apiUrl}/jobs`)
.map((response: Response) => response.json());
}
Started the code for it:
import { MockBackend, MockConnection } from '#angular/http/testing';
import { Http, BaseRequestOptions, Response, ResponseOptions, RequestMethod } from '#angular/http';
import { JobListService } from './job-list.service';
import { inject, TestBed } from '#angular/core/testing/test_bed';
import { JOBLISTMOCK } from '../mocks/job-list.mock';
fdescribe('Service: JobListService', () => {
beforeEach(() => {
TestBed.configureTestingModule({
providers: [
JobListService,
MockBackend,
BaseRequestOptions,
{
provide: Http,
useFactory: (backend: MockBackend, defaultOptions: BaseRequestOptions) => {
return new Http(backend, defaultOptions);
},
deps: [MockBackend, BaseRequestOptions]
},
]
});
});
it('should create a service', inject([JobListService], (service: JobListService) => {
expect(service).toBeTruthy();
}));
describe('getAll', () => {
it('should return jobs', inject([JobListService, MockBackend], (service: JobListService, backend: MockBackend) => {
let response = new ResponseOptions({
body: JSON.stringify(JOBLISTMOCK)
});
const baseResponse = new Response(response);
backend.connections.subscribe(
(c: MockConnection) => c.mockRespond(baseResponse)
);
return service.getAll(1, 10, '18').subscribe(data => {
expect(data).toEqual(JOBLISTMOCK);
});
}));
});
});
Do not know how to test it against the interceptor.
PS: As the tests are now, getting an error:
1) should create a service
JobListService
TypeError: null is not an object (evaluating 'this.platform.injector') in src/test.ts (line 83858)
_createCompilerAndModule#webpack:///~/#angular/core/testing/test_bed.js:254:0 <- src/test.ts:83858:44
2) should return jobs
JobListService getAll
TypeError: null is not an object (evaluating 'this.platform.injector') in src/test.ts (line 83858)
_createCompilerAndModule#webpack:///~/#angular/core/testing/test_bed.js:254:0 <- src/test.ts:83858:44
TypeError: null is not an object (evaluating 'this.platform.injector')
Generally you will get this error if you haven't initialized the test environment correctly. You could solve this problem by doing the following
import {
BrowserDynamicTestingModule, platformBrowserDynamicTesting
} from '#angular/platform-browser-dynamic/testing';
...
beforeAll(() => {
TestBed.initTestEnvironment(
BrowserDynamicTestingModule,
platformBrowserDynamicTesting()
);
});
The thing about this though, is that it should only be called once for the entire test suite execution. So if you have it in every test file, then you need to reset it first in each file
beforeAll(() => {
TestBed.resetTestEnvironment();
TestBed.initTestEnvironment(
BrowserDynamicTestingModule,
platformBrowserDynamicTesting()
);
});
Better than this though, is to not add it in each test file. If you look at the Angular docs for Webpack integration, in the testing section, you will see a file karma-test-shim.js. In this file is the recommended way to initialize the test environment
Error.stackTraceLimit = Infinity;
require('core-js/es6');
require('core-js/es7/reflect');
require('zone.js/dist/zone');
require('zone.js/dist/long-stack-trace-zone');
require('zone.js/dist/proxy');
require('zone.js/dist/sync-test');
require('zone.js/dist/jasmine-patch');
require('zone.js/dist/async-test');
require('zone.js/dist/fake-async-test');
var appContext = require.context('../src', true, /\.spec\.ts/);
appContext.keys().forEach(appContext);
var testing = require('#angular/core/testing');
var browser = require('#angular/platform-browser-dynamic/testing');
testing.TestBed.initTestEnvironment(browser.BrowserDynamicTestingModule,
browser.platformBrowserDynamicTesting());
You can see at the bottom where we make the same initialization call as above. You should add this file to the karma.conf.js file in the files array in the configuration. This is from the linked documentation above
files: [
{pattern: './config/karma-test-shim.js', watched: false}
],
preprocessors: {
'./config/karma-test-shim.js': ['webpack', 'sourcemap']
},

Categories

Resources