作者都是各自领域经过审查的专家,并撰写他们有经验的主题. 我们所有的内容都经过同行评审,并由同一领域的Toptal专家验证.
michaowkrakiewicz的头像

MichałKrakiewicz

PHP/Laravel老手, michaov(理学士)在云分析巨头Piwik PRO(现在的Matomo)学习敏捷团队合作。. 最近,他专注于Vue.js.

工作经验

10

Share

类型和可测试代码 是否有两种最有效的避免bug的方法,特别是当代码随时间变化时. 我们可以通过利用TypeScript和依赖注入(DI)设计模式,将这两种技术应用到JavaScript开发中, 分别.

在本TypeScript教程中,除了编译之外,我们不会直接介绍TypeScript的基础知识. 相反,我们将简单地演示TypeScript的最佳实践,因为我们将逐步了解如何创建 Discord bot 从头开始,连接测试和DI,并创建样例服务. 我们将使用:

  • Node.js
  • TypeScript
  • Discord.它是Discord API的包装器
  • InversifyJS,依赖注入框架
  • 测试库:Mocha、Chai和ts-mockito
  • 奖励:Mongoose和MongoDB,以便编写集成测试

设置节点.js Project

首先,让我们创建一个名为 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应用程序仪表板中创建新应用程序

为了与Discord API交互,我们需要一个令牌. 要生成这样一个令牌,我们需要在Discord Developer Dashboard中注册一个应用程序. 为了做到这一点,你需要创建一个Discord帐户,然后进入 http://discordapp.com/developers/applications/. 然后,单击 新的应用程序 button:

Discord's "新的应用程序" button.

选择一个名称并单击 Create. Then, click BotAdd Bot,就完成了. 让我们将bot添加到服务器. 但是不要关闭这个页面,我们很快就需要复制一个令牌.

将你的不和机器人添加到你的服务器

为了测试我们的机器人,我们需要一个不和服务器. 您可以使用现有的服务器,也可以创建一个新的服务器. 要做到这一点,复制机器人的 CLIENT_ID-在一般信息选项卡上找到,并将其用作其中的一部分 特别授权 URL:

http://discordapp.com/oauth2/authorize?client_id=&scope=bot

当您在浏览器中点击此URL时, 将出现一个表单,您可以在其中选择应该添加bot的服务器.

标准的不和谐欢迎信息,以响应我们的机器人加入服务器.

将bot添加到服务器后,您应该看到类似于上面的消息.

创建 .env File

我们需要在应用程序中保存令牌. 为了做到这一点,我们要用 dotenv package. 首先,从Discord应用程序仪表板(Bot点击显示令牌):

The "点击显示令牌" link in Discord's Bot section.

现在,创建一个 .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

创建Bot类

现在,让我们终于开始使用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 = {
  机器人:符号(“机器人”),
  客户:符号(“客户”),
  令牌:符号(“令牌”),
};

不使用 SymbolS,这是命名冲突发生时的样子:

错误:找到serviceIdentifier: MessageResponder的不明确匹配
注册绑定:
 MessageResponder
 MessageResponder

在这一点上,它是偶数 more 不方便区分哪一个 MessageResponder 应该使用,特别是如果我们的DI容器变大了. Using SymbolS解决了这个问题, 在两个类具有相同名称的情况下,我们没有提出奇怪的字符串字面值.

在Discord Bot App中使用Container

现在,我们来修改一下 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”的消息:

机器人对包含“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和依赖注入:不只是用于Discord Bot开发

带来面向对象的世界 TypeScript 导入JavaScript是一个很大的增强,无论我们是处理前端还是后端代码. 仅使用类型就可以避免许多错误. 在TypeScript中引入依赖注入,将更多面向对象的最佳实践推向了基于javascript的开发.

Of course, 因为语言的限制, 它永远不会像静态类型语言那样简单和自然. 但有一件事是肯定的:TypeScript, unit tests, 依赖注入允许我们编写更可读的代码, 松耦合, 以及可维护的代码——无论我们开发的是哪种应用.

了解基本知识

  • 为什么要使用依赖注入?

    如果您希望编写更简洁的代码(因为它是可单元测试的),那么您应该使用依赖注入设计模式, 更易于维护, 松耦合. 通过使用依赖注入,您可以获得更简洁的代码,而无需重新发明轮子.

  • 依赖注入的好处是什么?

    通过实现依赖注入, 我们被迫编写单元可测试的代码, 哪一个容易维护. 依赖关系是通过构造函数注入的,可以在单元测试中轻松模拟. 而且,这种模式鼓励我们编写松散耦合的代码.

  • TypeScript的目的是什么?

    TypeScript的主要目的是通过添加类型来使JavaScript代码更清晰、更易读. 它是开发人员的辅助工具,在ide中非常有用. 在底层,TypeScript仍然被转换成纯JavaScript.

  • 什么是Discord机器人?

    Discord bot是一个使用Discord API进行通信的web应用程序.

  • 不和谐机器人能做什么?

    Discord机器人可以回复信息、分配角色、做出回应等等. 普通用户和管理员可以执行的任何Discord操作都有API方法.

  • TypeScript有什么好处?

    TypeScript的主要好处是允许开发人员定义和使用类型. 通过使用类型提示, 转译器(或“源到源编译器”)知道应该将哪种对象传递给给定的方法. 在编译时检测到任何错误或无效调用,从而减少活动服务器上的错误.

聘请Toptal这方面的专家.
Hire Now
michaowkrakiewicz的头像
MichałKrakiewicz

Located in Wrocł啊,波兰

成员自 2017年8月10日

作者简介

PHP/Laravel老手, michaov(理学士)在云分析巨头Piwik PRO(现在的Matomo)学习敏捷团队合作。. 最近,他专注于Vue.js.

Toptal作者都是各自领域经过审查的专家,并撰写他们有经验的主题. 我们所有的内容都经过同行评审,并由同一领域的Toptal专家验证.

工作经验

10

世界级的文章,每周发一次.

订阅意味着同意我们的 隐私政策

世界级的文章,每周发一次.

订阅意味着同意我们的 隐私政策

Toptal开发者

加入总冠军® community.