Skip to main content

通过依赖注入解决间接输入

如果我们的程序强依赖于第三方库/其他模块等,那测试的时候,也必须使用 stub 替换掉强依赖的逻辑。

那如果没有强依赖的话,那就不需要 stub 了。

依赖倒置就可以消除强依赖。

依赖倒置原则

高层模块不应该依赖低层模块。

比如在函数 a 里调用了函数 b,那 a 就是高层模块,b 就是低层模块。

a ---> b

依赖倒置希望在中间增加一个接口,

a ----> 接口
^
|
|
b

a 依赖接口,而 b 去实现接口。

这样就消除了高层模块对于低层模块的强依赖。

这样我们对 b 进行修改替换,也不会影响到 a

这个接口又叫程序接缝

程序接缝是代码中的一个分界线,它允许我们将一个组件和其他组件隔离开来。ab 就被程序接缝给隔离开了。

通过创建接缝,我们可以轻松地替换一个组件的实现,而不影响其他代码。

这有助于将组件之间的耦合度降至最低,使得代码更加模块化。

一般有两种场景下的强依赖需要通过依赖倒置来消除,一种是函数,一种是 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,可以通过

  1. 构造函数
  2. 属性
  3. 方法

三种方式来注入依赖。

如果通过构造函数传参,则说明这个参数是必须传入的。

非必须传入的参数可以通过属性或方法来传入。

构造函数

该方式就是将依赖通过构造函数传入进来。

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