Jest expected mock function to have been called - javascript

I am testing the following service:
import { HttpException, HttpStatus, Injectable, Logger } from '#nestjs/common';
import { InjectRepository } from '#nestjs/typeorm';
import { extend } from 'lodash';
import { Repository } from 'typeorm';
import { DriverDTO } from './driver.dto';
import { DriverEntity } from './driver.entity';
#Injectable()
export class DriverService {
private logger = new Logger('DriverService');
constructor(
#InjectRepository(DriverEntity)
private driverRepository: Repository<DriverEntity>,
) { }
async create(clientId: string, data: DriverDTO): Promise<Partial<DriverEntity>> {
let driver = await this.driverRepository.findOne({ where: { clientId, driverId: data.driverId } });
this.logger.log(driver);
if (driver) {
throw new HttpException('Driver already exists', HttpStatus.CONFLICT);
}
driver = this.driverRepository.create(extend({ clientId }, data));
await this.driverRepository.save(driver);
return { driverId: driver.driverId, createdAt: driver.createdAt };
}
}
The service.spec.ts is as follows:
import { Test, TestingModule } from '#nestjs/testing';
import { DriverService } from './driver.service';
import { MockType } from '../mock/mock.type';
import { plainToClass } from 'class-transformer';
import { repositoryMockFactory } from '../mock/repositoryMock.factory';
import { getRepositoryToken } from '#nestjs/typeorm';
import { DriverEntity } from './driver.entity';
import { Repository } from 'typeorm';
import { Pagination } from '../pagination/pagination';
import { HttpException, HttpStatus } from '#nestjs/common';
import { DriverDTO } from './driver.dto';
describe('DriverService', () => {
let service: DriverService;
let driverRepositoryMock: MockType<Repository<DriverEntity>>;
beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
providers: [
DriverService,
{ provide: getRepositoryToken(DriverEntity), useFactory: repositoryMockFactory },
],
}).compile();
service = module.get<DriverService>(DriverService);
driverRepositoryMock = module.get(getRepositoryToken(DriverEntity));
});
it('should be defined', () => {
expect(service).toBeDefined();
expect(driverRepositoryMock).toBeDefined();
});
describe('DriverService.create()', () => {
const driverDto: DriverDTO = {
driverId: 'one',
};
const driver: DriverEntity = plainToClass(DriverEntity, {
id: 1,
clientId: 'one',
driverId: 'one',
createdAt: '2019-02-11T04:11:00.766Z',
updatedAt: '2019-02-11T04:11:00.766Z',
});
it('should create driver if it does not already exist', () => {
const response: Partial<DriverEntity> = plainToClass(DriverEntity, {
driverId: driver.driverId,
createdAt: driver.createdAt,
});
driverRepositoryMock.findOne.mockReturnValue(undefined);
driverRepositoryMock.create.mockReturnValue(driver);
expect(service.create('one', driverDto)).resolves.toEqual(response);
expect(driverRepositoryMock.findOne).toHaveBeenCalledTimes(1);
expect(driverRepositoryMock.findOne).toHaveBeenCalledWith({ where: { clientId: 'one', driverId: driverDto.driverId } });
expect(driverRepositoryMock.create).toHaveBeenCalledTimes(1);
expect(driverRepositoryMock.create).toHaveBeenCalledWith({
clientId: driver.clientId,
driverId: driver.driverId,
});
expect(driverRepositoryMock.save).toHaveBeenCalledWith(driver);
});
});
});
But I keep getting the following error:
● DriverService › DriverService.create() › should create driver if it does not already exist
expect(jest.fn()).toHaveBeenCalledTimes(1)
Expected mock function to have been called one time, but it was called zero times.
237 | expect(driverRepositoryMock.findOne).toHaveBeenCalledTimes(1);
238 | expect(driverRepositoryMock.findOne).toHaveBeenCalledWith({ where: { clientId: 'one', driverId: driverDto.driverId } });
> 239 | expect(driverRepositoryMock.create).toHaveBeenCalledTimes(1);
| ^
240 | expect(driverRepositoryMock.create).toHaveBeenCalledWith({
241 | clientId: driver.clientId,
242 | driverId: driver.driverId,
at Object.it (driver/driver.service.spec.ts:239:43)
Test Suites: 1 failed, 1 total
Tests: 1 failed, 8 passed, 9 total
Snapshots: 0 total
Time: 1.762s, estimated 6s
Ran all test suites matching /src\/driver\/driver.service.spec.ts/i.
I mocked the repository like this:
import { Repository, Entity } from 'typeorm';
import { MockType } from './mock.type';
// #ts-ignore
export const repositoryMockFactory: () => MockType<Repository<any>> = jest.fn(() => ({
findOne: jest.fn(entity => entity),
findAndCount: jest.fn(entity => entity),
create: jest.fn(entity => entity),
save: jest.fn(entity => entity),
}));
I can't seem to find the cause. Any help will be appreciated.

Make your test function async to wait until your service method is completely executed before you check the expectations for your mocks:
it('should create driver if it does not already exist', async () => {
// ^^^^^
// ...
await expect(service.create('one', driverDto)).resolves.toEqual(response);
//^^^^^

Related

Unit Testing in ts-jest with TypeOrm entities

Am trying to write tests, but am getting error. i think i have problem with connection.
"Cannot execute operation on "default" connection because connection is not yet established."
i have tests folder, and in it am having user.spec.ts and testhelper.ts
testhelper.ts
// import { Connection, createConnection } from "typeorm";
import { DataSource, DataSourceOptions } from "typeorm";
import Database from "better-sqlite3";
export class TestHelper {
private static _instance: TestHelper;
private constructor() {}
public static get instance(): TestHelper {
if (!this._instance) this._instance = new TestHelper();
return this._instance;
}
private dbConnect!: DataSource;
private testdb!: any;
async setupTestDB() {
this.testdb = new Database(":memory:", { verbose: console.log });
this.dbConnect = new DataSource({
name: "default",
type: "better-sqlite3",
database: ":memory:",
entities: ["src/entity/**/*.ts"],
synchronize: true,
} as DataSourceOptions);
}
teardownTestDB() {
this.dbConnect.destroy();
this.testdb.close();
}
}
user.spec.ts
import { createUser } from "../src/controllers/user.controller";
//#ts-ignore
import { TestHelper } from "./testhelper";
beforeAll(async () => {
await TestHelper.instance.setupTestDB();
});
afterAll(() => {
TestHelper.instance.teardownTestDB();
});
describe("User Tests", () => {
test("should create user", async () => {
const body = {
firstname: "John",
lastname: "Brut",
email: "john#examle.com",
password: "password123",
};
const res = {};
//#ts-ignore
const user = await createUser(body, res);
//#ts-ignore
expect(user.firstname).toBe("John");
//#ts-ignore
expect(user.lastname).toBe("Brut");
});
});
I'm doing this first time. and am stuck on it very long time... can someone please help me with this... : (
try this way, testhelper.ts
async setupTestDB() {
this.testdb = new Database(":memory:", { verbose: console.log });
this.dbConnect = new DataSource({
name: "default",
type: "better-sqlite3",
database: ":memory:",
entities: ["src/entity/**/*.ts"],
synchronize: true,
} as DataSourceOptions);
await this.dbConnect.initialize()
}

Angular: Testing apis from component

I have started learning how to test angular projects. So far basic unit testing is working fine for me but for the dependency testing especially when API services are injected into the component I am facing issue for providing HttpClient. I have tried different solutions but none is working for me.
Service
// Present in HttpClientService file
getDisposition() {
return this.http.get<{ message: string, data: { dispositionList: Disposition[] } }>(`${this.URL}/disposition/get`);
}
// Present in FileProcessService file
deleteMedia(media: string) {
return this.http.delete<{ message: string }>(`${this.URL}/certificate/delete?certificate=${media}`);
}
add-edit-activity.component.ts
import { HttpEventType } from '#angular/common/http';
import { Component, ElementRef, Inject, OnInit, ViewChild } from '#angular/core';
import { FormBuilder, Validators } from '#angular/forms';
import { MatDialog, MatDialogRef, MAT_DIALOG_DATA } from '#angular/material/dialog';
import { DomSanitizer, SafeUrl } from '#angular/platform-browser';
import { AssignedPerson } from '#model/assigned-person.model';
import { Disposition } from '#model/disposition.model';
import { mimeTypes } from '#model/mime-type';
import { FileProcessService } from '#service/file-process.service';
import { HttpClientService } from '#service/http-client.service';
import { DeleteComponent } from '#shared/delete/delete.component';
import { CustomErrorStateMatcher } from '#validator/error-state-matcher';
import { ToastrService } from 'ngx-toastr';
#Component({
selector: 'app-add-edit-activity',
templateUrl: './add-edit-activity.component.html',
styleUrls: ['./add-edit-activity.component.css']
})
export class AddEditActivityComponent implements OnInit {
constructor(private fb: FormBuilder, private sanitizer: DomSanitizer, private dialogRef: MatDialogRef<AddEditActivityComponent>, #Inject(MAT_DIALOG_DATA) public data: any,
private _http: HttpClientService, private _fileService: FileProcessService, private toastr: ToastrService, private dialog: MatDialog,) { }
re = new RegExp(/^[a-zA-Z-]*/, 'g');
ISOstamp = { T: ' ', Z: '000' };
matcher = new CustomErrorStateMatcher();
dispositionList: Disposition[] = [];
assignedPersonList: AssignedPerson[] = [];
filterAssignedPersonList: AssignedPerson[] = [];
uploaded = false;
uploadProgress = false;
fileURL: SafeUrl;
activityForm = this.fb.group({
gaugeId: [this.data.gaugeId, Validators.maxLength(9)], createTimeStamp: [{ value: new Date().toISOString().replace(/[TZ]/g, m => this.ISOstamp[m]), disabled: true }],
user: [{ value: sessionStorage.getItem('username'), disabled: true }], disposition: ['', [Validators.required, Validators.maxLength(30)]],
assignedPersonName: ['', Validators.maxLength(30)], department: ['', Validators.maxLength(20)],
shift: ['', Validators.maxLength(1)], remark: ['', Validators.maxLength(50)],
calibrationDate: ['', Validators.maxLength(10)], attachment: ['', Validators.maxLength(255)]
});
#ViewChild('file') certificate: ElementRef;
ngOnInit(): void {
this.data.type.match(this.re)[0] === 'Update' && this.setFormValues();
this._http.getDisposition().subscribe(response => this.dispositionList = response.data.dispositionList);
this._http.getAssignedPerson().subscribe(response => this.assignedPersonList = this.filterAssignedPersonList = response.data.assignedToList);
}
get GaugeId() {
return this.activityForm.get('gaugeId');
}
get TimeStamp() {
return this.activityForm.get('createTimeStamp');
}
get Attachment() {
return this.activityForm.get('attachment');
}
get Disposition() {
return this.activityForm.get('disposition');
}
get DispositionValue() {
return this.dispositionList.map(e => e.dispositionType).indexOf(this.Disposition.value) < 0;
}
get AssignedTo() {
return this.activityForm.get('assignedPersonName');
}
get AssignedToValue() {
return this.assignedPersonList.map(e => `${e.firstName} ${e.lastName}`).indexOf(this.Disposition.value) < 0;
}
private async setFormValues() {
this.activityForm.patchValue({ ...this.data });
if (this.data.attachment) {
this.uploadProgress = true;
this.uploaded = true;
await this.fetchUploadedFile(this.data.attachment, mimeTypes[this.data.attachment.split('.')[1]]);
this.activityForm.markAsPristine();
}
}
searchAssignedPerson(event) {
if (event.target.value) {
this.filterAssignedPersonList = [];
for (let person of this.assignedPersonList) {
if (person.firstName.toLowerCase().startsWith(event.target.value.toLowerCase())) {
this.filterAssignedPersonList.push(person);
}
}
} else { this.filterAssignedPersonList = this.assignedPersonList }
}
upload(event) {
const file: File = event.target.files[0];
this.certificate.nativeElement.value = '';
if (file.size > (20 * 1000 * 1000)) { // Checking if File size is above 20MB
this.toastr.error('Size of ' + file.name + ' is above 20MB');
return;
}
const fd = new FormData();
fd.append('certificate', file, file.name);
this.processAttachment(fd);
}
private processAttachment(file: FormData) {
this._fileService.uploadMedia(file).subscribe(event => {
if (event.type === HttpEventType.UploadProgress) { this.uploadProgress = true }
if (event.type === HttpEventType.Response) {
let media = event.body.data.Certificate;
this.fetchUploadedFile(media.fileName, media.fileType);
this.toastr.info(event.body.message);
}
}, error => {
this.toastr.error(error.error.message);
this.uploadProgress = false;
});
}
private async fetchUploadedFile(file: string, mimeType: string) {
try {
let response = await this._fileService.getMedia(file).toPromise();
this.fileURL = this.sanitizer.bypassSecurityTrustUrl(URL.createObjectURL(new Blob([response], { type: mimeType })));
this.uploaded = true;
this.uploadProgress = false;
this.activityForm.patchValue({ attachment: file });
this.Attachment.markAsDirty();
} catch (error) {
this.uploadProgress = false;
this._fileService.processError(error.error)
}
}
deleteFile() {
this.dialog.open(DeleteComponent, {
width: '350px', disableClose: true
}).afterClosed().subscribe(response => {
if (response) {
this._fileService.deleteMedia(this.Attachment.value).subscribe(response => {
this.toastr.info(response.message);
this.activityForm.patchValue({ attachment: '' });
this.Attachment.markAsDirty();
this.uploaded = false;
}, error => this.toastr.error(error.error.message));
}
});
}
async doAction() {
let message = '';
if (this.data.type.match(this.re)[0] === 'Add') {
message = await (await this._http.addActivityLog(this.activityForm.getRawValue()).toPromise()).message;
} else {
message = await (await this._http.updateActivityLog(this.GaugeId.value, this.TimeStamp.value, this.activityForm.getRawValue()).toPromise()).message;
}
this.dialogRef.close(message);
}
}
add-edit-activity.component.spec.ts
import { ComponentFixture, TestBed, tick } from '#angular/core/testing';
import { FormsModule, ReactiveFormsModule } from '#angular/forms';
import { MatDialogRef, MAT_DIALOG_DATA } from '#angular/material/dialog';
import { FileProcessService } from '#service/file-process.service';
import { HttpClientService } from '#service/http-client.service';
import { of } from 'rxjs';
import { delay } from 'rxjs/operators';
import { AddEditActivityComponent } from './add-edit-activity.component';
describe('AddEditActivityComponent', () => {
let component: AddEditActivityComponent;
let fixture: ComponentFixture<AddEditActivityComponent>;
let _http: HttpClientService, _file: FileProcessService;
beforeEach(async () => {
await TestBed.configureTestingModule({
declarations: [AddEditActivityComponent],
imports: [FormsModule, ReactiveFormsModule],
providers: [
{ provide: MatDialogRef, useValue: {} },
{ provide: MAT_DIALOG_DATA, useValue: { type: 'Add Activity Log', gaugeId: 'W-001' } },,
{ provider: HttpClientService, useValue: null },
{ provider: FileProcessService, useValue: null }
]
}).compileComponents();
});
beforeEach(() => {
fixture = TestBed.createComponent(AddEditActivityComponent);
component = fixture.debugElement.componentInstance;
_http = fixture.debugElement.injector.get(HttpClientService);
_file = fixture.debugElement.injector.get(FileProcessService);
fixture.detectChanges();
});
// These 2 tests throwing error.
it('SHOULD mock http service', () => {
let spy = spyOn(_http, 'getDisposition').and.callFake(() => {
return of({
message: '',
data: { dispositionList: [] }
}).pipe(delay(100));
});
component.ngOnInit();
tick(100);
expect(component.dispositionList).toEqual([]);
});
it('SHOULD mock file service', () => {
let spy = spyOn(_file, 'deleteMedia').and.callFake((file: string) => {
return of({ message: '' }).pipe(delay(100));
});
component.deleteFile();
tick(100);
expect(component.uploaded).toBe(false);
});
});
The error that am getting for those 2 tests (I'm providing the error of 1 test case, the same is coming for the 2nd one also):
SHOULD mock http service
AddEditActivityComponent
Error: Invalid provider for the NgModule 'DynamicTestModule' - only instances of Provider and Type are allowed, got: [..., ..., ...,
..., ?undefined?, ..., ...]
at throwInvalidProviderError (node_modules/#angular/core/ivy_ngcc/fesm2015/core.js:240:1)
at providerToFactory (node_modules/#angular/core/ivy_ngcc/fesm2015/core.js:11550:1)
at providerToRecord (node_modules/#angular/core/ivy_ngcc/fesm2015/core.js:11521:1)
at R3Injector.processProvider (node_modules/#angular/core/ivy_ngcc/fesm2015/core.js:11424:1)
Error: : could not find an object to spy upon for getDisposition() Usage: spyOn(, ) Error: :
could not find an object to spy upon for getDisposition() Usage:
spyOn(, )
at
Anyone, please help what's the exact error happening with my test cases? Stuck here for almost 2 days.
You have to avoid using a 'null' value when providing services in the TestBed, that what is causing your problem.
The best way to mock Angular services is to create SpyObj instances and then use them like what is done Angular tests documentation.
Here is how would be your spec file if you were using SpyObj:
import { ComponentFixture, TestBed, tick } from '#angular/core/testing';
import { FormsModule, ReactiveFormsModule } from '#angular/forms';
import { MatDialogRef, MAT_DIALOG_DATA } from '#angular/material/dialog';
import { FileProcessService } from '#service/file-process.service';
import { HttpClientService } from '#service/http-client.service';
import { of } from 'rxjs';
import { delay } from 'rxjs/operators';
import { AddEditActivityComponent } from './add-edit-activity.component';
describe('AddEditActivityComponent', () => {
let component: AddEditActivityComponent;
let fixture: ComponentFixture<AddEditActivityComponent>;
let httpSpy: jasmine.SpyObj<HttpClientService>;
let fileSpy: jasmine.SpyObj<FileProcessService>;
beforeEach(async () => {
await TestBed.configureTestingModule({
declarations: [AddEditActivityComponent],
imports: [FormsModule, ReactiveFormsModule],
providers: [
{ provide: MatDialogRef, useValue: {} },
{ provide: MAT_DIALOG_DATA, useValue: { type: 'Add Activity Log', gaugeId: 'W-001' } },
{
provider: HttpClientService,
useValue: jasmine.createSpyObj('HttpClientService', ['getDisposition', 'getAssignedPerson', 'addActivityLog', 'updateActivityLog'])
},
{
provider: FileProcessService,
useValue: jasmine.createSpyObj('FileProcessService', ['uploadMedia', 'getMedia', 'deleteMedia', 'processError'])
}
]
}).compileComponents();
});
beforeEach(() => {
fixture = TestBed.createComponent(AddEditActivityComponent);
component = fixture.debugElement.componentInstance;
httpSpy = fixture.debugElement.injector.get(HttpClientService) as jasmine.SpyObj<HttpClientService>;
fileSpy = fixture.debugElement.injector.get(FileProcessService) as jasmine.SpyObj<FileProcessService>;
fixture.detectChanges();
});
// These 2 tests throwing error.
it('SHOULD mock http service', () => {
httpSpy.getDisposition.and.callFake(() => {
return of({
message: '',
data: { dispositionList: [] }
}).pipe(delay(100));
});
component.ngOnInit();
tick(100);
expect(component.dispositionList).toEqual([]);
});
it('SHOULD mock file service', () => {
fileSpy.deleteMedia.and.callFake((file: string) => {
return of({ message: '' }).pipe(delay(100));
});
component.deleteFile();
tick(100);
expect(component.uploaded).toBe(false);
});
});

Jest - Create React App - Axios: test get mocked

I have a axiosInstance.js axios instance:
import axios from "axios"
import { REACT_FE_ACCESS_TOKEN } from "../../constants/constant";
const axiosInstance = axios.create({
baseURL: process.env.REACT_APP_BACKEND_URL,
headers: {
"content-type": "application/json"
},
responseType: "json"
});
axiosInstance.interceptors.request.use((config) => {
const token = localStorage.getItem(REACT_FE_ACCESS_TOKEN);
if (token) {
config.headers.Authorization = `Bearer ${token}`;
}
return config;
});
export { axiosInstance };
I call it from a class:
import { axiosInstance as api } from "./axiosInstance";
export default class ApiCrud {
constructor(baseUrl) {
this.baseUrl = baseUrl;
}
fetchItems() {
return api.get(`${this.getBaseUrl()}`).then(result => result.data);
}
getBaseUrl() {
return this.baseUrl;
}
}
I want to do a write test (using Create React App and Jest).
This is the axiosInstance.test.js file, that works:
import { axiosInstance } from "../../../../utils/api/base/axiosInstance";
import { REACT_FE_ACCESS_TOKEN } from "../../../../utils/constants/constant";
const token = "a1.b2.c3";
beforeEach(() => {
localStorage.clear();
});
describe('Test API Instance', () => {
it ('Test request interceptor with token', () => {
localStorage.setItem(REACT_FE_ACCESS_TOKEN, token);
expect(localStorage.getItem(REACT_FE_ACCESS_TOKEN)).toBe(token);
const result = axiosInstance.interceptors.request.handlers[0].fulfilled({ headers: {} });
expect(result.headers).toHaveProperty("Authorization");
});
it ('Test request interceptor without token', () => {
const result = axiosInstance.interceptors.request.handlers[0].fulfilled({ headers: {} });
expect(result.headers).not.toHaveProperty("Authorization");
});
});
This is the apiCrud.test.js
import ApiCrud from "../../../../utils/api/base/ApiCrud";
const mockedGet = {
email: "info#example.com",
}
jest.mock('axios', () => {
return {
create: jest.fn(() => ({
get: jest.fn(() => Promise.resolve({ data: mockedGet })),
interceptors: {
request: { use: jest.fn(), eject: jest.fn() },
response: { use: jest.fn(), eject: jest.fn() }
}
}))
}
})
describe('Test API crud', () => {
it ('Test can get base url', () => {
const apiCrud = new ApiCrud('/fake-url');
expect(apiCrud.getBaseUrl()).toBe('/fake-url');
});
it ('Test can fetch items', () => {
const apiCrud = new ApiCrud('/fake-url');
return apiCrud.fetchItems().then(data => {
expect(data).toBe(mockedGet);
})
});
});
But I get
FAIL src/__tests__/utils/api/base/apiCrud.test.js
● Test API crud › Test can fetch items
TypeError: Cannot read property 'then' of undefined
8 |
9 | fetchItems() {
> 10 | return api.get(`${this.getBaseUrl()}`).then(result => result.data);
| ^
11 | }
12 |
13 | getBaseUrl() {
at ApiCrud.fetchItems (src/utils/api/base/ApiCrud.js:10:12)
at Object.<anonymous> (src/__tests__/utils/api/base/apiCrud.test.js:29:20)
So, I think I'm getting wrong with mocking the get for axios, but... How solve?
You are testing ApiCrud class that depends on the ./axiosInstance module. It's simpler to mock direct dependency ./axiosInstance module than indirect
dependency - axios module.
ApiCrud.js:
import { axiosInstance as api } from './axiosInstance';
export default class ApiCrud {
constructor(baseUrl) {
this.baseUrl = baseUrl;
}
fetchItems() {
return api.get(`${this.getBaseUrl()}`).then((result) => result.data);
}
getBaseUrl() {
return this.baseUrl;
}
}
ApiCrud.test.js:
import ApiCrud from './ApiCrud';
import { axiosInstance } from './axiosInstance';
const mockedGet = {
email: 'info#example.com',
};
jest.mock('./axiosInstance');
describe('Test API crud', () => {
afterEach(() => {
jest.clearAllMocks();
});
afterAll(() => {
jest.resetAllMocks();
});
it('Test can get base url', () => {
const apiCrud = new ApiCrud('/fake-url');
expect(apiCrud.getBaseUrl()).toBe('/fake-url');
});
it('Test can fetch items', () => {
axiosInstance.get.mockResolvedValueOnce({ data: mockedGet });
const apiCrud = new ApiCrud('/fake-url');
return apiCrud.fetchItems().then((data) => {
expect(data).toBe(mockedGet);
});
});
});
test result:
PASS examples/69531160/ApiCrud.test.js (10.676 s)
Test API crud
✓ Test can get base url (3 ms)
✓ Test can fetch items (1 ms)
------------------|---------|----------|---------|---------|-------------------
File | % Stmts | % Branch | % Funcs | % Lines | Uncovered Line #s
------------------|---------|----------|---------|---------|-------------------
All files | 73.33 | 0 | 80 | 71.43 |
ApiCrud.js | 100 | 100 | 100 | 100 |
axiosInstance.js | 55.56 | 0 | 0 | 55.56 | 13-17
------------------|---------|----------|---------|---------|-------------------
Test Suites: 1 passed, 1 total
Tests: 2 passed, 2 total
Snapshots: 0 total
Time: 14.978 s

Mock implementation of firebase auth methods after mocking the firebase module

I want to change the implementation of the methods inside jest.mock so i can check how my app reacts to different edge cases so i did this, However typescript is not letting me mock firebase.auth().currentUser method ... i am showing my code and error below
app.js
import firebase from 'firebase/app'
import 'firebase/auth'
import './Init'
const App = {
getLoggedInUser: () => {
const currentUser = firebase.auth().currentUser
if (currentUser) {
return {
email: firebase.auth().currentUser.email,
userId: firebase.auth().currentUser.uid,
isEmailVerified: firebase.auth().currentUser.emailVerified
}
} else {
return undefined
}
},
isAuthenticated: () => {
return !!((App.getLoggedInUser() && App.getLoggedInUser().isEmailVerified === true))
},
}
export default App
app.spec.ts
import myAuthenticationPlugin from 'authenticationPlugin/App'
import firebase from 'firebase/app'
jest.mock('firebase/app', () => {
const userCredentialMock = {
user: {
sendEmailVerification: jest.fn()
}
}
return {
auth: jest.fn().mockReturnThis(),
currentUser: {
email: 'test',
uid: '123',
emailVerified: true
},
signInWithEmailAndPassword: jest.fn(),
createUserWithEmailAndPassword: jest.fn(() => userCredentialMock),
sendPasswordResetEmail: jest.fn(),
signOut: jest.fn(),
onAuthStateChanged: jest.fn(),
initializeApp: jest.fn()
}
})
describe('Test for isAuthenticated ()', () => {
afterEach(() => {
jest.resetAllMocks()
})
it('The API should return a boolean value telling us, If the user is authenticated to access the resources or not', () => {
expect(myAuthenticationPlugin.isAuthenticated()).toBe(true)
})
firebase.auth().currentUser = jest.fn(() => {
return {
email: 'test',
uid: '123',
emailVerified: false
}
})
it('Check false', () => {
expect(myAuthenticationPlugin.isAuthenticated()).toBe(false)
})
})
Error i get
FAIL tests/unit/App.spec.ts
● Test suite failed to run
TypeScript diagnostics (customize using `[jest-config].globals.ts-jest.diagnostics` option):
tests/unit/App.spec.ts:44:5 - error TS2740: Type 'Mock<{ email: string; uid: string; emailVerified: boolean; }, []>' is missing the following properties from type 'User': delete, emailVerified, getIdTokenResult, getIdToken, and 31 more.
44 firebase.auth().currentUser = jest.fn(() => {
~~~~~~~~~~~~~~~~~~~~~~~~~~~
I am confused now as to how to proceed with testing different edge cases for my App is there someway around this ?
You need to mock firebase.auth method and its return value as the currentUser.
E.g.
app.ts:
import firebase from 'firebase/app';
const App = {
getLoggedInUser: () => {
const currentUser = firebase.auth().currentUser;
if (currentUser) {
return {
email: currentUser.email,
userId: currentUser.uid,
isEmailVerified: currentUser.emailVerified,
};
} else {
return undefined;
}
},
isAuthenticated: () => {
return !!(App.getLoggedInUser() && App.getLoggedInUser()!.isEmailVerified === true);
},
};
export default App;
app.test.ts:
import App from './app';
import firebase from 'firebase/app';
jest.mock('firebase/app', () => {
return {
auth: jest.fn(),
};
});
describe('61408137', () => {
it('should return user', () => {
(firebase.auth as jest.Mocked<any>).mockReturnValueOnce({
currentUser: { email: 'example#gmail.com', uid: 1, emailVerified: true },
});
const actual = App.getLoggedInUser();
expect(actual).toEqual({
email: 'example#gmail.com',
userId: 1,
isEmailVerified: true,
});
});
it('should return undefined', () => {
(firebase.auth as jest.Mocked<any>).mockReturnValueOnce({});
const actual = App.getLoggedInUser();
expect(actual).toBeUndefined();
});
});
unit test results with coverage report:
PASS stackoverflow/61408137/app.test.ts (9.822s)
61408137
✓ should return user (3ms)
✓ should return undefined (1ms)
----------|---------|----------|---------|---------|-------------------
File | % Stmts | % Branch | % Funcs | % Lines | Uncovered Line #s
----------|---------|----------|---------|---------|-------------------
All files | 87.5 | 50 | 50 | 87.5 |
app.ts | 87.5 | 50 | 50 | 87.5 | 17
----------|---------|----------|---------|---------|-------------------
Test Suites: 1 passed, 1 total
Tests: 2 passed, 2 total
Snapshots: 0 total
Time: 11.683s
source code: https://github.com/mrdulin/react-apollo-graphql-starter-kit/tree/master/stackoverflow/61408137

Test NestJS Service against Actual Database

I would like to be able to test my Nest service against an actual database. I understand that most unit tests should use a mock object, but it also, at times, makes sense to test against the database itself.
I have searched through SO and the GH issues for Nest, and am starting to reach the transitive closure of all answers. :-)
I am trying to work from https://github.com/nestjs/nest/issues/363#issuecomment-360105413. Following is my Unit test, which uses a custom provider to pass the repository to my service class.
describe("DepartmentService", () => {
const token = getRepositoryToken(Department);
let service: DepartmentService;
let repo: Repository<Department>;
beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
providers: [
DepartmentService,
{
provide: token,
useClass: Repository
}
]
}).compile();
service = module.get<DepartmentService>(DepartmentService);
repo = module.get(token);
});
Everything compiles properly, TypeScript seems happy. However, when I try to execute create or save on my Repository instance, the underlying Repository appears to be undefined. Here's the stack backtrace:
TypeError: Cannot read property 'create' of undefined
at Repository.Object.<anonymous>.Repository.create (repository/Repository.ts:99:29)
at DepartmentService.<anonymous> (relational/department/department.service.ts:46:53)
at relational/department/department.service.ts:19:71
at Object.<anonymous>.__awaiter (relational/department/department.service.ts:15:12)
at DepartmentService.addDepartment (relational/department/department.service.ts:56:16)
at Object.<anonymous> (relational/department/test/department.service.spec.ts:46:35)
at relational/department/test/department.service.spec.ts:7:71
It appears that the EntityManager instance with the TypeORM Repository class is not being initialized; it is the undefined reference that this backtrace is complaining about.
How do I get the Repository and EntityManager to initialize properly?
thanks,
tom.
To initialize typeorm properly, you should just be able to import the TypeOrmModule in your test:
Test.createTestingModule({
imports: [
TypeOrmModule.forRoot({
type: 'mysql',
// ...
}),
TypeOrmModule.forFeature([Department])
]
I prefer not using #nestjs/testing for the sake of simplicity.
First of all, create a reusable helper:
/* src/utils/testing-helpers/createMemDB.js */
import { createConnection, EntitySchema } from 'typeorm'
type Entity = Function | string | EntitySchema<any>
export async function createMemDB(entities: Entity[]) {
return createConnection({
// name, // let TypeORM manage the connections
type: 'sqlite',
database: ':memory:',
entities,
dropSchema: true,
synchronize: true,
logging: false
})
}
Then, write test:
/* src/user/user.service.spec.ts */
import { Connection, Repository } from 'typeorm'
import { createMemDB } from '../utils/testing-helpers/createMemDB'
import UserService from './user.service'
import User from './user.entity'
describe('User Service', () => {
let db: Connection
let userService: UserService
let userRepository: Repository<User>
beforeAll(async () => {
db = await createMemDB([User])
userRepository = await db.getRepository(User)
userService = new UserService(userRepository) // <--- manually inject
})
afterAll(() => db.close())
it('should create a new user', async () => {
const username = 'HelloWorld'
const password = 'password'
const newUser = await userService.createUser({ username, password })
expect(newUser.id).toBeDefined()
const newUserInDB = await userRepository.findOne(newUser.id)
expect(newUserInDB.username).toBe(username)
})
})
Refer to https://github.com/typeorm/typeorm/issues/1267#issuecomment-483775861
Here's an update to the test that employs Kim Kern's suggestion.
describe("DepartmentService", () => {
let service: DepartmentService;
let repo: Repository<Department>;
let module: TestingModule;
beforeAll(async () => {
module = await Test.createTestingModule({
imports: [
TypeOrmModule.forRoot(),
TypeOrmModule.forFeature([Department])
],
providers: [DepartmentService]
}).compile();
service = module.get<DepartmentService>(DepartmentService);
repo = module.get<Repository<Department>>(getRepositoryToken(Department));
});
afterAll(async () => {
module.close();
});
it("should be defined", () => {
expect(service).toBeDefined();
});
// ...
}
I created a test orm configuration
// ../test/db.ts
import { TypeOrmModuleOptions } from '#nestjs/typeorm';
import { EntitySchema } from 'typeorm';
type Entity = Function | string | EntitySchema<any>;
export const createTestConfiguration = (
entities: Entity[],
): TypeOrmModuleOptions => ({
type: 'sqlite',
database: ':memory:',
entities,
dropSchema: true,
synchronize: true,
logging: false,
});
which I then utilize when setting up the tests
// books.service.test.ts
import { Test, TestingModule } from '#nestjs/testing';
import { HttpModule, HttpService } from '#nestjs/common';
import { TypeOrmModule, getRepositoryToken } from '#nestjs/typeorm';
import { Repository } from 'typeorm';
import { BooksService } from './books.service';
import { Book } from './book.entity';
import { createTestConfiguration } from '../../test/db';
describe('BooksService', () => {
let module: TestingModule;
let service: BooksService;
let httpService: HttpService;
let repository: Repository<Book>;
beforeAll(async () => {
module = await Test.createTestingModule({
imports: [
HttpModule,
TypeOrmModule.forRoot(createTestConfiguration([Book])),
TypeOrmModule.forFeature([Book]),
],
providers: [BooksService],
}).compile();
httpService = module.get<HttpService>(HttpService);
service = module.get<BooksService>(BooksService);
repository = module.get<Repository<Book>>(getRepositoryToken(Book));
});
afterAll(() => {
module.close();
});
it('should be defined', () => {
expect(service).toBeDefined();
});
This allows to query the repository after the tests and ensure that the correct data was inserted.
I usually import AppModule for database connection, and finally after tests are executed I close the connection:
let service: SampleService;
let connection: Connection;
beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
imports: [AppModule, TypeOrmModule.forFeature([SampleEntity])],
providers: [SampleService],
}).compile();
service = module.get<SampleService>(SampleService);
connection = await module.get(getConnectionToken());
});
afterEach(async () => {
await connection.close();
});
I used existing AppModule for testing:
import { INestApplication } from '#nestjs/common';
import { NestFactory } from '#nestjs/core';
import { AppModule } from '../../../app.module';
import { AuthService } from '../auth.service';
describe('AuthService', () => {
let app: INestApplication;
let service: AuthService;
beforeAll(async () => {
app = await NestFactory.create(AppModule);
service = app.get(AuthService);
});
afterAll(async () => {
await app.close();
});
it('AuthService should be defined', () => {
expect(service).toBeDefined();
});
describe('login', () => {
it('should login user', async () => {
const user = await service.login({ email: 'xxxzei#mail.ru', password: '12345678' });
expect(user.id).toBeDefined();
});
});
});
Also to add env variables in your configuration file or AppModule:
import { config as testConfig } from 'dotenv';
if (process.env.NODE_ENV === 'test') {
testConfig({ path: resolve('./.env') });
}

Categories

Resources