NestJS accessing private class field before testing method with jest - javascript

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.

Related

Mocking es6 class static methods with Jest

I am really having big trouble handling this issue. I have read Jest docs a lot, also other articles, but nothing has helped me yet, even chatGPT.
I am trying to test this class.
export class CookiesManager extends Auth {
static get(key: string) {
return this.cookies.get(key);
}
static set(key: string, value: any, config?: CookieSetOptions) {
this.cookies.set(key, value, config || this.config);
}
static remove(key: string) {
const { hostname } = new URL(`${process.env.WEB_HOST}`);
this.cookies.remove(key, { ...this.config, domain: hostname });
}
}
Here is the Auth class, but in my case, I haven't even reached here or I guess I will not need to handle that part in the scope of this one. Anyway, just for a reference.
import moment from 'moment';
import Cookies, { CookieSetOptions } from 'universal-cookie';
export class Auth {
static cookies = new Cookies();
static config: CookieSetOptions = {
path: '/',
maxAge: 1800,
expires: moment().add(30, 'm').toDate(),
secure: process.env.NODE_ENV !== 'development',
};
static setToken(token: string) {
token && this.cookies.set('auth_token', token, this.config);
}
static getToken() {
return this.cookies.get('auth_token');
}
static clear() {
this.cookies.remove('auth_token', this.config);
}
}
I have written the mock for the CookiesManager class.The module has also other named exports.
jest.mock('core/utils/storage-manager.ts', () => {
const originalClasses = jest.requireActual('core/utils/storage-manager.ts');
class CookiesManagerMock {
static config = {
path: '/',
maxAge: 1800,
}
static set = jest.fn().mockImplementation((key, value, config) => {
console.log('Mock set called');
mockCookies[key] = value;
})
static get = jest.fn().mockImplementation((key) => {
console.log('Mock get called');
return mockCookies[key];
})
static remove = jest.fn().mockImplementation((key, config) => {
console.log('Mock remove called');
delete mockCookies[key];
})
}
return {
...originalClasses,
CookiesManager: CookiesManagerMock,
}
})
This is the test block.
describe('CookiesManager', () => {
afterEach(() => {
jest.restoreAllMocks();
});
test('It should set a cookie', () => {
CookiesManager.set('test_key', 'test_value');
console.log(mockCookies.test_key, 'mockCookies')
expect(CookiesManager.set).toHaveBeenCalledWith('test_key', 'test_value');
});
test('It should get a cookie', () => {
CookiesManager.get.mockReturnValueOnce('test_value');
expect(CookiesManager.get('test_key')).toEqual('test_value');
expect(CookiesManager.get).toHaveBeenCalledWith('test_key');
});
test('It should remove a cookie', () => {
CookiesManager.remove('test_key');
expect(CookiesManager.remove).toHaveBeenCalledWith('test_key');
});
})
UPDATED
Even though the tests are passing, non of the console.log statements are being called in mocked class. And also mockCookies is always empty. I have tried the same with CommonJS modules and the strange thing is that console.log statements are being called.
In Jest docs, it's clearly stated.
mockFn.mockImplementation(fn)
Accepts a function that should be used as the implementation of the
mock. The mock itself will still record all calls that go into and
instances that come from itself – the only difference is that the
implementation will also be executed when the mock is called.
Maybe I am getting something wrong and don't understand the nature of the mocks.
You didn't mock the static methods correctly. The way you are using is trying to mock the instance methods of a class. Here is a solution, create a mock CookiesManager class with mock static methods.
storage-manager.ts:
type CookieSetOptions = any;
export class CookiesManager {
static get(key: string) {}
static set(key: string, value: any, config?: CookieSetOptions) {}
static remove(key: string) {}
}
export const a = () => 'a'
export const b = () => 'b'
storage-manager.test.ts:
import { CookiesManager, a, b } from './storage-manager';
jest.mock('./storage-manager', () => {
const originalModules = jest.requireActual('./storage-manager');
const mockCookies = {};
class CookiesManagerMock {
static set = jest.fn().mockImplementation((key, value, config) => {
console.log('Mock set called');
mockCookies[key] = value;
});
static get = jest.fn().mockImplementation((key) => {
console.log('Mock get called');
return mockCookies[key];
});
static remove = jest.fn().mockImplementation((key, config) => {
console.log('Mock remove called');
delete mockCookies[key];
});
}
return {
...originalModules,
CookiesManager: CookiesManagerMock,
};
});
describe('75323871', () => {
test('It should set a cookie', () => {
const config = { path: '/', maxAge: 1800 };
expect(jest.isMockFunction(CookiesManager.set)).toBeTruthy();
CookiesManager.set('test_key', 'test_value', config);
expect(CookiesManager.set).toHaveBeenCalledWith('test_key', 'test_value', config);
expect(CookiesManager.get('test_key')).toBe('test_value');
// other named exports
expect(a()).toBe('a');
expect(b()).toBe('b');
});
});
jest.config.js:
module.exports = {
preset: 'ts-jest/presets/js-with-ts'
};
Test result:
PASS stackoverflow/75323871/storage-manager.test.ts (8.614 s)
75323871
✓ It should set a cookie (17 ms)
console.log
Mock set called
at Function.<anonymous> (stackoverflow/75323871/storage-manager.test.ts:9:15)
console.log
Mock get called
at Function.<anonymous> (stackoverflow/75323871/storage-manager.test.ts:14:15)
Test Suites: 1 passed, 1 total
Tests: 1 passed, 1 total
Snapshots: 0 total
Time: 9.358 s, estimated 10 s
Note: Maybe jest.spyOn(CookiesManager, 'set').mockImplementation() is a better solution, you only need to mock the specific static method. see https://jestjs.io/docs/es6-class-mocks#static-getter-and-setter-methods
package version:
"jest": "^26.6.3",
"ts-jest": "^26.4.4"

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); /
});
})

Testing Service with Mongoose in NestJS

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));
});

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']
},

Angular 2 mock Http get() to return local json file

What is the easiest way to mock the response returned by Http get() in Angular 2?
I have local data.json file in my working directory, and I want get() to return response containing that data as a payload, simulating the rest api.
Documents for configuring the Backend object for Http seemed somewhat obscure and overcomplicated for such a simple task.
You need to override the XhrBackend provider with the MockBackend one. You need then to create another injector to be able to execute a true HTTP request.
Here is a sample:
beforeEachProviders(() => {
return [
HTTP_PROVIDERS,
provide(XHRBackend, { useClass: MockBackend }),
SomeHttpService
];
});
it('Should something', inject([XHRBackend, SomeHttpService], (mockBackend, httpService) => {
mockBackend.connections.subscribe(
(connection: MockConnection) => {
var injector = ReflectiveInjector.resolveAndCreate([
HTTP_PROVIDERS
]);
var http = injector.get(Http);
http.get('data.json').map(res => res.json()).subscribe(data) => {
connection.mockRespond(new Response(
new ResponseOptions({
body: data
})));
});
});
}));
By the way, you need to mock the XHRBackend and provide mocked data in a class with the createDb method. createDb method returns the mocked JSON object. To load that data provide correct URL to http.get, for example, if JSON object is contained in a variable mockedObject, then the URL should be "app\mockedObject".
You can read more details here: https://angular.io/docs/ts/latest/guide/server-communication.html.
You can use the HttpTestingController available via the core TestBed as to me it feels more intuitive (each to their own, of course). Untested snippet:
import { TestBed, async } from '#angular/core/testing';
import { HttpTestingController } from '#angular/common/http/testing';
import { MyApiService } from './my-api.service';
export function main() {
describe('Test set', () => {
let httpMock: HttpTestingController;
beforeEach(() => {
TestBed.configureTestingModule({
imports: [],
providers: [MyApiService]
});
httpMock = TestBed.get(HttpTestingController);
});
it('should call get', async(() => {
const data: any = {mydata: 'test'};
let actualResponse: any = null;
MyApiService.get().subscribe((response: any) => {
actualResponse = response;
});
httpMock.expectOne('localhost:5555/my-path').flush(data);
expect(actualResponse).toEqual(data);
}));
});
}

Categories

Resources