作者都是各自领域经过审查的专家,并撰写他们有经验的主题. 我们所有的内容都经过同行评审,并由同一领域的Toptal专家验证.
PHP/Laravel老手, michaov(理学士)在云分析巨头Piwik PRO(现在的Matomo)学习敏捷团队合作。. 最近,他专注于Vue.js.
10
类型和可测试代码 是否有两种最有效的避免bug的方法,特别是当代码随时间变化时. 我们可以通过利用TypeScript和依赖注入(DI)设计模式,将这两种技术应用到JavaScript开发中, 分别.
在本TypeScript教程中,除了编译之外,我们不会直接介绍TypeScript的基础知识. 相反,我们将简单地演示TypeScript的最佳实践,因为我们将逐步了解如何创建 Discord bot 从头开始,连接测试和DI,并创建样例服务. 我们将使用:
首先,让我们创建一个名为 typescript-bot
. 然后,输入它并创建一个新的Node.Js项目运行:
npm init
注意:你也可以使用 yarn
对于这个,我们还是坚持 npm
for brevity.
这将打开一个交互式向导,该向导将设置 package.json
file. 你可以安全地按一下 Enter 对于所有问题(或提供一些信息,如果你想). Then, 让我们安装我们的依赖项和开发依赖项(那些只需要测试的).
NPM I——保存打字错误.@types/node reflect-metadata
NPM I -save-dev chai mocha ts-mockito ts-node @types/chai @types/mocha
然后,替换生成的 "scripts"
section in package.json
with:
"脚本":{
"start": "节点src/索引。.js",
"watch": "tsc -p tsconfig . sh ".json -w",
"test": "mocha -r ts-node/register \"tests/**/*.spec.ts\""
},
双引号 tests/**/*.spec.ts
需要递归地查找文件吗. (注意:语法可能因使用Windows的开发人员而异.)
The start
脚本将用于启动机器人 watch
. script编译TypeScript代码 test
来运行测试.
Now, our package.json
文件应该是这样的:
{
“名称”:“typescript-bot”,
“版本”:“1.0.0",
“描述”:“”,
“主要”:“指数.js",
“依赖”:{
“@types /节点”:“^ 11.9.4",
"discord.js": "^11.4.2",
:“dotenv ^ 6.2.0",
:“inversify ^ 5.0.1",
:“reflect-metadata ^ 0.1.13",
:“打印稿^ 3.3.3"
},
" devDependencies ": {
“@types /茶”:“^ 4.1.7",
“@types /摩卡”:“^ 5.2.6",
"chai": "^4.2.0",
“摩卡”:“^ 5.2.0",
:“ts-mockito ^ 2.3.1",
:“ts-node ^ 8.0.3"
},
"脚本":{
"start": "节点src/索引。.js",
"watch": "tsc -p tsconfig . sh ".json -w",
"test": "mocha -r ts-node/register \"tests/**/*.spec.ts\""
},
“作者”:“”,
“许可证”:“ISC”
}
为了与Discord API交互,我们需要一个令牌. 要生成这样一个令牌,我们需要在Discord Developer Dashboard中注册一个应用程序. 为了做到这一点,你需要创建一个Discord帐户,然后进入 http://discordapp.com/developers/applications/. 然后,单击 新的应用程序 button:
选择一个名称并单击 Create. Then, click Bot → Add Bot,就完成了. 让我们将bot添加到服务器. 但是不要关闭这个页面,我们很快就需要复制一个令牌.
为了测试我们的机器人,我们需要一个不和服务器. 您可以使用现有的服务器,也可以创建一个新的服务器. 要做到这一点,复制机器人的 CLIENT_ID
-在一般信息选项卡上找到,并将其用作其中的一部分 特别授权 URL:
http://discordapp.com/oauth2/authorize?client_id=
当您在浏览器中点击此URL时, 将出现一个表单,您可以在其中选择应该添加bot的服务器.
将bot添加到服务器后,您应该看到类似于上面的消息.
.env
File我们需要在应用程序中保存令牌. 为了做到这一点,我们要用 dotenv
package. 首先,从Discord应用程序仪表板(Bot → 点击显示令牌):
现在,创建一个 .env
文件,然后复制并粘贴标记在这里:
TOKEN=paste.the.token.here
如果您使用Git,那么这个文件应该放在 .gitignore
,这样令牌就不会被泄露. 同时,创建一个 .env.example
文件,这样就知道了 TOKEN
需要定义:
TOKEN=
为了编译TypeScript,你可以使用 NPM运行观察
command. 另外, 如果你使用PHPStorm(或其他IDE), 只需要使用TypeScript插件中的文件监视器,让IDE处理编译. 让我们通过创建一个 src/index.ts
文件的内容:
console.日志(“你好”)
同样,让我们创建一个 tsconfig.json
文件如下所示. InversifyJS需要 experimentalDecorators
, emitDecoratorMetadata
, es6
, and reflect-metadata
:
{
" compilerOptions ": {
“模块”:“commonjs”,
“moduleResolution”:“节点”,
“目标”:“es2016”,
"lib": [
"es6",
"dom"
],
“sourceMap”:没错,
"types": [
//添加节点作为选项
"node",
“reflect-metadata”
],
“typeRoots”:(
//添加@types的路径
“node_modules / @types”
],
“experimentalDecorators”:没错,
“emitDecoratorMetadata”:没错,
“resolveJsonModule”:真的
},
“排除”:(
“node_modules”
]
}
如果文件监视程序工作正常,它应该生成一个 src/index.js
文件,并运行 npm start
应导致:
> node src/index.js
Hello
现在,让我们终于开始使用TypeScript最有用的特性:类型. 继续并创建以下内容 src/bot.ts
file:
从“discord”中导入{Client, Message}.js";
导出类Bot {
public listen(): Promise {
let client = new client ();
client.on('message', (message: Message) => {});
返回客户端.Login ('token应该在这里');
}
}
现在,我们可以看到我们需要什么了:一个令牌! 我们是直接复制粘贴到这里,还是直接从环境中加载这个值?
Neither. Instead, 让我们编写更易于维护的代码, extendable, 通过使用我们选择的依赖注入框架注入令牌来测试代码, InversifyJS.
同时,我们可以看到 Client
依赖是硬编码的. 我们也要注入这个.
A 依赖注入容器 对象是否知道如何实例化其他对象. 通常,我们为每个类定义依赖关系,而DI容器负责解析它们.
InversifyJS建议将依赖项放在 inversify.config.ts
所以让我们继续在这里添加我们的DI容器:
导入“reflect-metadata”;
从“倒置”中导入{Container};
导入{TYPES}./types";
导入{Bot}./bot";
从“discord”中导入{Client}.js";
let container = new container ();
container.bind(TYPES.Bot).to(Bot).inSingletonScope ();
container.bind(TYPES.Client).toConstantValue(新客户());
container.bind(TYPES.Token).toConstantValue(过程.env.TOKEN);
导出默认容器;
Also, InversifyJS文档推荐的 creating a types.ts
文件,并列出我们将要使用的每种类型,以及相关的 Symbol
. 这是非常不方便的,但它可以确保在应用程序增长时不会出现命名冲突. Each Symbol
是唯一标识符, 即使它的描述参数相同(该参数仅用于调试目的).
导出const TYPES = {
机器人:符号(“机器人”),
客户:符号(“客户”),
令牌:符号(“令牌”),
};
不使用 Symbol
S,这是命名冲突发生时的样子:
错误:找到serviceIdentifier: MessageResponder的不明确匹配
注册绑定:
MessageResponder
MessageResponder
在这一点上,它是偶数 more 不方便区分哪一个 MessageResponder
应该使用,特别是如果我们的DI容器变大了. Using Symbol
S解决了这个问题, 在两个类具有相同名称的情况下,我们没有提出奇怪的字符串字面值.
现在,我们来修改一下 Bot
类来使用容器. 我们需要加上 @injectable
and @inject()
注释来完成这个. 这是最新消息 Bot
class:
从“discord”中导入{Client, Message}.js";
从“inversiy”导入{inject, injectable};
导入{TYPES}./types";
导入{MessageResponder}./服务/ message-responder”;
@injectable ()
导出类Bot {
私人客户:client;
私有只读令牌:string;
构造函数(
@ inject(类型.客户)客户:客户;
@ inject(类型.Token) Token:字符串
) {
this.Client = Client;
this.Token = Token;
}
public listen(): Promise < string > {
this.client.on('message', (message: Message) => {
console.日志(“消息收到! 内容:“,留言。.content);
});
return this.client.login(this.token);
}
}
控件中实例化我们的bot index.ts
file:
要求(“dotenv”).config(); // Recommended way of loading dotenv
从“./inversify.config";
导入{TYPES}./types";
导入{Bot}./bot";
集装箱.get(TYPES.Bot);
bot.listen().then(() => {
console.日志(“登录!')
}).catch((error) => {
console.log('Oh no! ', error)
});
现在,启动bot并将其添加到服务器. Then, 如果您在服务器通道中键入消息, 它应该出现在命令行日志中,如下所示:
> node src/index.js
Logged in!
接收到的消息! 内容:测试
Finally, 我们已经准备好了基础:TypeScript类型和bot中的依赖注入容器.
让我们直接进入本文的核心内容:创建一个可测试的代码库. 简而言之,我们的代码应该实现最佳实践(比如 SOLID),不隐藏依赖关系,不使用静态方法.
Also, 它不应该在运行时引入副作用,并且容易被嘲笑.
为了简单起见, 我们的机器人将只做一件事:它将搜索传入的消息, 如果其中一个包含“ping”这个词,我们将使用一个可用的Discord机器人命令来让机器人响应“pong”!给那个用户.
中注入自定义对象 Bot
对象并对它们进行单元测试,我们将创建两个类: PingFinder
and MessageResponder
. 我们将注入 MessageResponder
into the Bot
class, and PingFinder
into MessageResponder
.
Here is the src /服务/ ping-finder.ts
file:
从“inversiy”中导入{injectable};
@injectable ()
导出类PingFinder {
Private regexp = 'ping';
public isPing(stringToSearch: string): boolean {
返回stringToSearch.search(this.regexp) >= 0;
}
}
然后将该类注入 src /服务/ message-responder.ts
file:
从“discord”中导入{Message}.js";
导入{PingFinder}./ ping-finder”;
从“inversiy”导入{inject, injectable};
导入{TYPES}../types";
@injectable ()
导出类MessageResponder {
private pingFinder: pingFinder;
构造函数(
@ inject(类型.PingFinder: PingFinder
) {
this.pingFinder = pingFinder;
}
handle(message: Message): Promise {
if (this.pingFinder.isp(消息.content)) {
返回消息.reply('pong!');
}
回报承诺.reject();
}
}
最后,这里有一个修改 Bot
类,它使用 MessageResponder
class:
从“discord”中导入{Client, Message}.js";
从“inversiy”导入{inject, injectable};
导入{TYPES}./types";
导入{MessageResponder}./服务/ message-responder”;
@injectable ()
导出类Bot {
私人客户:client;
私有只读令牌:string;
private messageResponder: messageResponder;
构造函数(
@ inject(类型.客户)客户:客户;
@ inject(类型.Token) Token:字符串;
@ inject(类型.MessageResponder: MessageResponder) {
this.Client = Client;
this.Token = Token;
this.messageResponder = messageResponder;
}
public listen(): Promise {
this.client.on('message', (message: Message) => {
if (message.author.bot) {
console.忽略bot消息!')
return;
}
console.日志(“消息收到! 内容:“,留言。.content);
this.messageResponder.处理(消息).then(() => {
console.日志(“响应发送!");
}).catch(() => {
console.响应未发送.")
})
});
return this.client.login(this.token);
}
}
在这种状态下,应用程序将无法运行,因为没有定义 MessageResponder
and PingFinder
classes. 我们将以下内容添加到 inversify.config.ts
file:
container.bind(TYPES.MessageResponder).(MessageResponder).inSingletonScope ();
container.bind(TYPES.PingFinder).(PingFinder).inSingletonScope ();
同样,我们将添加类型符号到 types.ts
:
MessageResponder:符号(“MessageResponder”),
PingFinder:符号(“PingFinder”),
现在,在重新启动我们的应用程序后,bot应该响应每个包含“ping”的消息:
下面是它在日志中的样子:
> node src/index.js
Logged in!
接收到的消息! 内容:部分留言
未发送响应.
接收到的消息! 内容:带ping的消息
忽略bot消息!
响应发送!
既然我们已经正确地注入了依赖项,编写单元测试就很容易了. We are going to use Chai and ts-mockito for that; however, 您可以使用许多其他的测试运行程序和模拟库.
ts-mockito中的模拟语法非常冗长,但也很容易理解. 下面是如何设置 MessageResponder
服务并注入 PingFinder
模仿它:
让mockedPingFinderClass = mock(PingFinder);
let mockedPingFinderInstance = instance(mockedPingFinderClass);
let service = new MessageResponder(mockedPingFinderInstance);
现在我们已经设置了模拟,我们可以定义模拟的结果 isPing()
电话应该是可靠的 reply()
calls. 关键是,在单元测试中,我们定义的结果 isPing()
call: true
or false
. 消息内容是什么并不重要,所以在测试中我们只使用 “非空字符串”
.
当(mockedPingFinderClass.isp(“非空字符串”)).thenReturn(真正的);
等待服务.处理(mockedMessageInstance)
验证(mockedMessageClass.reply('pong!')).once();
下面是整个测试套件的样子:
导入“reflect-metadata”;
进口“摩卡”;
从'chai'导入{expect};
导入{PingFinder}../../../ src /服务/ ping-finder”;
导入{MessageResponder}../../../ src /服务/ message-responder”;
从“ts-mock”中导入{instance, mock, verify, when};
从“discord”中导入{Message}.js";
describe('MessageResponder', () => {
让mockedPingFinderClass: PingFinder;
让mockedPingFinderInstance: PingFinder;
让mockedMessageClass:消息;
让mockedMessageInstance:消息;
let service: MessageResponder;
beforeEach(() => {
mockedPingFinderClass = mock(PingFinder);
mockedPingFinderInstance = instance(mockedPingFinderClass);
mockedMessageClass = mock(Message);
mockedMessageInstance = instance(mockedMessageClass);
setMessageContents ();
service = new MessageResponder(mockedPingFinderInstance);
})
it('should reply', async () => {
whenIsPingThenReturn(真正的);
等待服务.处理(mockedMessageInstance);
验证(mockedMessageClass.reply('pong!')).once();
})
it('should not reply', async () => {
whenIsPingThenReturn(假);
等待服务.处理(mockedMessageInstance).then(() => {
//成功的承诺出乎意料,所以我们没有通过考验
expect.失败(“意想不到的承诺”);
}).catch(() => {
//被拒绝的承诺是预期的,所以这里什么都不会发生
});
验证(mockedMessageClass.reply('pong!')).never();
})
setMessageContents() {
mockedMessageInstance.content = "非空字符串";
}
函数whenIsPingThenReturn(result: boolean) {
当(mockedPingFinderClass.isp(“非空字符串”)).thenReturn(结果);
}
});
这些测试 PingFinder
都是微不足道的,因为没有需要模拟的依赖关系. 下面是一个测试用例:
describe('PingFinder', () => {
let服务:PingFinder;
beforeEach(() => {
service = new PingFinder();
})
it('should find "ping" in the string', () => {
期望(服务.isp(“平”)).to.be.true
})
});
除了单元测试,我们还可以编写集成测试. 主要的区别在于这些测试中的依赖关系没有被模拟. 然而,有一些依赖关系不应该被测试,比如外部API连接. 在这种情况下,我们可以创建模拟和 rebind
将它们注入到容器中,以便注入模拟. 下面是一个如何做到这一点的例子:
从“../../inversify.config";
导入{TYPES}../../ src /类型”;
// ...
describe('Bot', () => {
让discordMock: Client;
让discordInstance: Client;
let bot: bot;
beforeEach(() => {
discordMock = mock(Client);
discordInstance = instance(discordMock);
container.rebind(TYPES.Client)
.toConstantValue (discordInstance);
集装箱.get(TYPES.Bot);
});
//测试用例在这里
});
这就结束了我们的不和机器人教程. 恭喜你,你干净利落地构建了它,从一开始就使用了TypeScript和DI! 这个TypeScript依赖注入的例子是一个模式,你可以添加到你的列表中,用于任何项目.
带来面向对象的世界 TypeScript 导入JavaScript是一个很大的增强,无论我们是处理前端还是后端代码. 仅使用类型就可以避免许多错误. 在TypeScript中引入依赖注入,将更多面向对象的最佳实践推向了基于javascript的开发.
Of course, 因为语言的限制, 它永远不会像静态类型语言那样简单和自然. 但有一件事是肯定的:TypeScript, unit tests, 依赖注入允许我们编写更可读的代码, 松耦合, 以及可维护的代码——无论我们开发的是哪种应用.
如果您希望编写更简洁的代码(因为它是可单元测试的),那么您应该使用依赖注入设计模式, 更易于维护, 松耦合. 通过使用依赖注入,您可以获得更简洁的代码,而无需重新发明轮子.
通过实现依赖注入, 我们被迫编写单元可测试的代码, 哪一个容易维护. 依赖关系是通过构造函数注入的,可以在单元测试中轻松模拟. 而且,这种模式鼓励我们编写松散耦合的代码.
TypeScript的主要目的是通过添加类型来使JavaScript代码更清晰、更易读. 它是开发人员的辅助工具,在ide中非常有用. 在底层,TypeScript仍然被转换成纯JavaScript.
Discord bot是一个使用Discord API进行通信的web应用程序.
Discord机器人可以回复信息、分配角色、做出回应等等. 普通用户和管理员可以执行的任何Discord操作都有API方法.
TypeScript的主要好处是允许开发人员定义和使用类型. 通过使用类型提示, 转译器(或“源到源编译器”)知道应该将哪种对象传递给给定的方法. 在编译时检测到任何错误或无效调用,从而减少活动服务器上的错误.
PHP/Laravel老手, michaov(理学士)在云分析巨头Piwik PRO(现在的Matomo)学习敏捷团队合作。. 最近,他专注于Vue.js.
10
世界级的文章,每周发一次.
世界级的文章,每周发一次.