通过依赖注入解决间接输入
如果我们的程序强依赖于第三方库/其他模块等,那测试的时候,也必须使用 stub 替换掉强依赖的逻辑。
那如果没有强依赖的话,那就不需要 stub 了。
依赖倒置就可以消除强依赖。
依赖倒置原则
高层模块不应该依赖低层模块。
比如在函数 a
里调用了函数 b
,那 a
就是高层模块,b
就是低层模块。
a ---> b
依赖倒置希望在中间增加一个接口,
a ----> 接口
^
|
|
b
a
依赖接口,而 b
去实现接口。
这样就消除了高层模块对于低层模块的强依赖。
这样我们对 b
进行修改替换,也不会影响到 a
。
这个接口又叫程序接缝。
程序接缝是代码中的一个分界线,它允许我们将一个组件和其他组件隔离开来。a
和 b
就被程序接缝给隔离开了。
通过创建接缝,我们可以轻松地替换一个组件的实现,而不影响其他代码。
这有助于将组件之间的耦合度降至最低,使得代码更加模块化。
一般有两种场景下的强依赖需要通过依赖倒置来消除,一种是函数,一种是 class,下面就举例逐个说明。
函数
// readAndProcessFile.ts
import { readFileSync } from "fs";
export function readAndProcessFile(filePath: string): string {
const content: string = readFileSync(filePath, {
encoding: "utf-8",
});
return content;
}
上面的代码中,readAndProcessFile
函数强依赖了 readFileSync
。
如果要测试 readAndProcessFile
那就需要 stub readFileSync
。
不想 stub 的话,就可以尝试用依赖注入来消除强依赖。
// readAndProcessFile.ts
export interface FileReader {
read(filePath: string): string;
}
export function readAndProcessFile(
filePath: string,
fileReader: FileReader
): string {
const content: string = fileReader.read(filePath);
return content;
}
// fileReader.ts
import { readAndProcessFile, FileReader } from "./readAndProcessFile";
import { readFileSync } from "fs";
export class TxtFileReader {
read(filePath: string) {
return readFileSync(filePath, {
encoding: "utf-8",
});
}
}
const result = readAndProcessFile("./example.txt", new TxtFileReader());
我们将 readAndProcessFile
函数对 readFileSync
的强依赖消除了,并传入参数 TxtFileReader
对象。
在这个对象里,定义一个 read
方法。在 read
方法里再去使用 readFileSync
。
我们有了 TxtFileReader
这样一个中间层接口后,写测试就不再需要 mock readFileSync
了。
readAndProcessFile ----> TxtFileReader 对象
^
|
|
readFileSync
而只需要在第二个参数,传入一个有 read
方法的对象即可。
read
方法里面可以直接写死返回内容。
强依赖了什么,就通过参数的形式传入什么。
JavaScript 是鸭子类型。
具体的测试代码:
it("read and process file", () => {
class StubFileReader {
read(filePath: string) {
return "nansen";
}
}
const result = readAndProcessFile("./test.txt", new StubFileReader());
expect(result).toBe("nansen");
});
本质上,我们只是调整了代码的结构,从而使测试更好写。
class
// ReadAndProcessFile.ts
import { readFileSync } from "fs";
export class ReadAndProcessFile {
run(filePath: string) {
const content = readFileSync(filePath, {
encoding: "utf-8",
});
return content;
}
}
上面的代码依旧是强依赖于 readFileSync
。
对于 class,可以通过
- 构造函数
- 属性
- 方法
三种方式来注入依赖。
如果通过构造函数传参,则说明这个参数是必须传入的。
非必须传入的参数可以通过属性或方法来传入。
构造函数
该方式就是将依赖通过构造函数传入进来。
// readAndProcessFile.ts
interface FileReader {
read(filePath: string): string;
}
export class ReadAndProcessFile {
private _fileReader: FileReader;
constructor(fileReader: FileReader) {
this._fileReader = fileReader;
}
run(filePath: string) {
const content = this._fileReader.read(filePath);
return content;
}
}
相对应的测试代码:
// readAndProcessFile.spec.ts
import { ReadAndProcessFile, FileReader } from "./readAndProcessFile";
it("构造函数", () => {
class StubFileReader implements FileReader {
read(filePath: string): string {
return "nansen";
}
}
const readAndProcessFile = new ReadAndProcessFile(new StubFileReader());
expect(readAndProcessFile.run("./test.txt")).toBe("nansen");
});
属性
通过 set
将依赖传入进去。
// ReadAndProcessFile.ts
interface FileReader {
read(filePath: string): string;
}
export class ReadAndProcessFile {
private _fileReader: FileReader;
run(filePath: string) {
const content = this._fileReader.read(filePath);
return content;
}
// 如果不希望外部拿到 _fileReader 可以不写 get 方法
// get fileReader() {
// return this._fileReader;
// }
set fileReader(fileReader: FileReader) {
this._fileReader = fileReader;
}
}
相对应的测试代码:
// readAndProcessFile.spec.ts
import { ReadAndProcessFile, FileReader } from "./readAndProcessFile";
it("属性", () => {
class StubFileReader implements FileReader {
read(filePath: string): string {
return "nansen";
}
}
const readAndProcessFile = new ReadAndProcessFile();
readAndProcessFile.fileReader = new StubFileReader();
expect(readAndProcessFile.run("./test.txt")).toBe("nansen");
});
方法
通过方法将依赖传入进去。
// ReadAndProcessFile.ts
interface FileReader {
read(filePath: string): string;
}
export class ReadAndProcessFile {
private _fileReader: FileReader;
run(filePath: string) {
const content = this._fileReader.read(filePath);
return content;
}
setFileReader(fileReader: FileReader) {
this._fileReader = fileReader;
}
}
相对应的测试代码:
// readAndProcessFile.spec.ts
import { ReadAndProcessFile, FileReader } from "./readAndProcessFile";
it("属性", () => {
class StubFileReader implements FileReader {
read(filePath: string): string {
return "nansen";
}
}
const readAndProcessFile = new ReadAndProcessFile();
readAndProcessFile.setFileReader(new StubFileReader());
expect(readAndProcessFile.run("./test.txt")).toBe("nansen");
});