Testa en fs-biblioteksfunktion med Jest/Typescript
Jag försöker testa en biblioteksfunktion som jag har skrivit (den fungerar i min kod) men kan inte få testningen med mocken av fs att fungera. Jag har en serie funktioner för att arbeta med OS inneslutna i funktioner så att olika delar av applikationen kan använda samma anrop.
Jag har försökt att följa denna fråga genom att mocka filsystemet, men det verkar inte fungera för mig.
Ett kort exempel för att visa grunderna i mitt problem är nedan:
import * as fs from 'fs';
export function ReadFileContentsSync(PathAndFileName:string):string {
if (PathAndFileName === undefined || PathAndFileName === null || PathAndFileName.length === 0) {
throw new Error('Need a Path and File');
}
return fs.readFileSync(PathAndFileName).toString();
}
Så nu försöker jag testa den här funktionen med Jest:
import { ReadFileContentsSync } from "./read-file-contents-sync";
const fs = require('fs');
describe('Return Mock data to test the function', () => {
it('should return the test data', () => {
const TestData:string = 'This is sample Test Data';
// Trying to mock the reading of the file to simply use TestData
fs.readFileSync = jest.fn();
fs.readFileSync.mockReturnValue(TestData);
// Does not need to exist due to mock above
const ReadData = ReadFileContentsSync('test-path');
expect(fs.readFileSync).toHaveBeenCalled();
expect(ReadData).toBe(TestData);
});
});
Jag får ett undantag att filen inte finns, men jag skulle ha förväntat mig att själva anropet till fs.readFileSync inte skulle ha kallats, utan att jest.fn() mocken skulle ha använts.
ENOENT: no such file or directory, open 'test-path'
Jag är inte säker på hur man ska göra denna övning/mock.
Svar #1
Eftersom jag nämnde funktionell/OO/och ogillandet av jest-mock, känner jag att jag borde ge lite förklaring här.
Jag är inte emot jest.mock() eller något mockningsbibliotek (som sinon). Jag har använt dem tidigare, och de tjänar definitivt sitt syfte och är ett användbart verktyg. Men jag märker att jag för det mesta inte behöver dem, och det finns vissa kompromisser när man använder dem.
Låt mig först demonstrera tre sätt som koden kan implementeras utan att använda mock.
Det första sättet är funktionellt, och använder ett context som det första argumentet:
// read-file-contents-sync.ts
import fs from 'fs';
export function ReadFileContentsSync({ fs } = { fs }, PathAndFileName: string): string {
if (PathAndFileName === undefined || PathAndFileName === null || PathAndFileName.length === 0) {
throw new Error('Need a Path and File');
}
return fs.readFileSync(PathAndFileName).toString();
}
// read-file-contents-sync.spec.ts
import { ReadFileContentsSync } from "./read-file-contents-sync";
describe('Return Mock data to test the function', () => {
it('should return the test data', () => {
const TestData:Buffer = new Buffer('This is sample Test Data');
// Trying to mock the reading of the file to simply use TestData
const fs = {
readFileSync: () => TestData
}
// Does not need to exist due to mock above
const ReadData = ReadFileContentsSync({ fs }, 'test-path');
expect(ReadData).toBe(TestData.toString());
});
});
Det andra sättet är att använda OO:
// read-file-contents-sync.ts
import fs from 'fs';
export class FileReader {
fs = fs
ReadFileContentsSync(PathAndFileName: string) {
if (PathAndFileName === undefined || PathAndFileName === null || PathAndFileName.length === 0) {
throw new Error('Need a Path and File');
}
return this.fs.readFileSync(PathAndFileName).toString();
}
}
// read-file-contents-sync.spec.ts
import { FileReader } from "./read-file-contents-sync";
describe('Return Mock data to test the function', () => {
it('should return the test data', () => {
const TestData: Buffer = new Buffer('This is sample Test Data');
const subject = new FileReader()
subject.fs = { readFileSync: () => TestData } as any
// Does not need to exist due to mock above
const ReadData = subject.ReadFileContentsSync('test-path');
expect(ReadData).toBe(TestData.toString());
});
});
Det tredje sättet använder en modifierad funktionell stil, vilket kräver TypeScript 3.1 (tekniskt sett kan du göra det innan 3.1, men det är bara lite mer klumpigt och involverar en namespace-hack):
// read-file-contents-sync.ts
import fs from 'fs';
export function ReadFileContentsSync(PathAndFileName: string): string {
if (PathAndFileName === undefined || PathAndFileName === null || PathAndFileName.length === 0) {
throw new Error('Need a Path and File');
}
return ReadFileContentsSync.fs.readFileSync(PathAndFileName).toString();
}
ReadFileContentsSync.fs = fs
// read-file-contents-sync.spec.ts
import { ReadFileContentsSync } from "./read-file-contents-sync";
describe('Return Mock data to test the function', () => {
it('should return the test data', () => {
const TestData: Buffer = new Buffer('This is sample Test Data');
// Trying to mock the reading of the file to simply use TestData
ReadFileContentsSync.fs = {
readFileSync: () => TestData
} as any
// Does not need to exist due to mock above
const ReadData = ReadFileContentsSync('test-path');
expect(ReadData).toBe(TestData.toString());
});
});
De första två sätten ger mer flexibilitet och isolering eftersom varje anrop/instans har sin egen referens till beroendet. Detta innebär att det inte finns något sätt för 'mocken' i ett test att påverka det andra.
Det tredje sättet förhindrar inte att det händer men har fördelen att inte ändra signaturen på den ursprungliga funktionen.
Grunden för allt detta är beroendehantering. Oftast är ett program eller kod svår att underhålla, använda eller testa eftersom det inte ger ett sätt för den anropande kontexten att kontrollera beroendet för dess anropade.
Att förlita sig på ett mockningsbibliotek (särskilt ett mockningssystem så kraftfullt som jest.mock()) kan lätt leda till en vana att ignorera denna viktiga aspekt.
En trevlig artikel som jag skulle rekommendera alla att kolla in är Uncle Bobs Clean Architecture: https://8thlight.com/blog/uncle-bob/2012/08/13/the-clean-architecture.html
Svar #2
Även om unionals kommentar hjälpte till att peka mig i rätt riktning, gjordes importen för fs i min kod som import * as fs from 'fs'. Detta verkade vara problemet. Att ändra importen här till helt enkelt import fs from 'fs' löste problemet.
Därför blir koden:
import fs from 'fs';
export function ReadFileContentsSync(PathAndFileName:string):string {
if (PathAndFileName === undefined || PathAndFileName === null || PathAndFileName.length === 0) {
throw new Error('Need a Path and File');
}
return fs.readFileSync(PathAndFileName).toString();
}
Och testfilen:
jest.mock('fs');
import { ReadFileContentsSync } from "./read-file-contents-sync";
import fs from 'fs';
describe('Return Mock data to test the function', () => {
it('should return the test data', () => {
const TestData:Buffer = new Buffer('This is sample Test Data');
// Trying to mock the reading of the file to simply use TestData
fs.readFileSync = jest.fn();
fs.readFileSync.mockReturnValue(TestData);
// Does not need to exist due to mock above
const ReadData = ReadFileContentsSync('test-path');
expect(fs.readFileSync).toHaveBeenCalled();
expect(ReadData).toBe(TestData.toString());
});
});
