タイプとテスト可能なコード これは、特にコードが時間の経過とともに変化するときに、バグを回避するための最も効果的な2つの方法です。 TypeScriptと依存性注入(DI)デザインパターンをそれぞれ活用することで、これら2つの手法をJavaScript開発に適用できます。
このTypeScriptチュートリアルでは、コンパイルを除いて、TypeScriptの基本については直接説明しません。代わりに、Discordボットを最初から作成し、テストとDIを接続し、サンプルサービスを作成する方法を説明しながら、TypeScriptのベストプラクティスを簡単に示します。使用します:
まず、typescript-bot
という名前の新しいディレクトリを作成しましょう。次に、それを入力し、次を実行して新しいNode.jsプロジェクトを作成します。
npm init
注:yarn
を使用することもできますそのためですが、npm
に固執しましょう簡潔にするため。
これにより、インタラクティブなウィザードが開き、package.json
が設定されます。ファイル。押すだけで安全に 入る すべての質問に対応します(または、必要に応じて情報を提供します)。次に、依存関係と開発依存関係(テストにのみ必要なもの)をインストールしましょう。
npm i --save typescript discord.js inversify dotenv @types/node reflect-metadata npm i --save-dev chai mocha ts-mockito ts-node @types/chai @types/mocha
次に、生成された'scripts'
を置き換えますpackage.json
のセクションと:
'scripts': { 'start': 'node src/index.js', 'watch': 'tsc -p tsconfig.json -w', 'test': 'mocha -r ts-node/register 'tests/**/*.spec.ts'' },
tests/**/*.spec.ts
を囲む二重引用符ファイルを再帰的に見つけるために必要です。 (注:構文は、Windowsを使用している開発者によって異なる場合があります。)
start
スクリプトはボット、watch
を起動するために使用されますTypeScriptコードをコンパイルするためのスクリプト、およびtest
テストを実行します。
さて、私たちのpackage.json
ファイルは次のようになります。
テレグラムボットとは
{ 'name': 'typescript-bot', 'version': '1.0.0', 'description': '', 'main': 'index.js', 'dependencies': { '@types/node': '^11.9.4', 'discord.js': '^11.4.2', 'dotenv': '^6.2.0', 'inversify': '^5.0.1', 'reflect-metadata': '^0.1.13', 'typescript': '^3.3.3' }, 'devDependencies': { '@types/chai': '^4.1.7', '@types/mocha': '^5.2.6', 'chai': '^4.2.0', 'mocha': '^5.2.0', 'ts-mockito': '^2.3.1', 'ts-node': '^8.0.3' }, 'scripts': { 'start': 'node src/index.js', 'watch': 'tsc -p tsconfig.json -w', 'test': 'mocha -r ts-node/register 'tests/**/*.spec.ts'' }, 'author': '', 'license': 'ISC' }
Discord APIと対話するには、トークンが必要です。このようなトークンを生成するには、Discord DeveloperDashboardにアプリを登録する必要があります。これを行うには、Discordアカウントを作成し、に移動する必要があります https://discordapp.com/developers/applications/ 。次に、をクリックします 新しいアプリ ボタン:
名前を選択してクリック 作成する 。次に、をクリックします ボット → ボットを追加 、これで完了です。ボットをサーバーに追加しましょう。ただし、まだこのページを閉じないでください。すぐにトークンをコピーする必要があります。
ボットをテストするには、Discordサーバーが必要です。既存のサーバーを使用することも、新しいサーバーを作成することもできます。これを行うには、[一般情報]タブにあるボットのCLIENT_ID
をコピーして、この一部として使用します 特別承認 URL:
https://discordapp.com/oauth2/authorize?client_id=&scope=bot
ブラウザでこのURLを押すと、ボットを追加するサーバーを選択できるフォームが表示されます。
ボットをサーバーに追加すると、上記のようなメッセージが表示されます。
.env
の作成ファイルアプリにトークンを保存する方法が必要です。そのために、dotenv
を使用しますパッケージ。まず、Discordアプリケーションダッシュボードからトークンを取得します( ボット → クリックしてトークンを公開 ):
次に、.env
を作成しますファイルを作成し、トークンをコピーしてここに貼り付けます。
TOKEN=paste.the.token.here
Gitを使用する場合は、トークンが危険にさらされないように、このファイルを.gitignore
に配置する必要があります。また、.env.example
を作成しますファイル、そのためTOKEN
定義する必要があります:
TOKEN=
TypeScriptをコンパイルするには、npm run watch
を使用できます。コマンド。または、PHPStorm(または別のIDE)を使用している場合は、TypeScriptプラグインのファイルウォッチャーを使用して、IDEにコンパイルを処理させます。 src/index.ts
を作成してセットアップをテストしましょう内容のファイル:
console.log('Hello')
また、tsconfig.json
を作成しましょう以下のようなファイル。 InversifyJSには、experimentalDecorators
、emitDecoratorMetadata
、es6
、およびreflect-metadata
が必要です。
{ 'compilerOptions': { 'module': 'commonjs', 'moduleResolution': 'node', 'target': 'es2016', 'lib': [ 'es6', 'dom' ], 'sourceMap': true, 'types': [ // add node as an option 'node', 'reflect-metadata' ], 'typeRoots': [ // add path to @types 'node_modules/@types' ], 'experimentalDecorators': true, 'emitDecoratorMetadata': true, 'resolveJsonModule': true }, 'exclude': [ 'node_modules' ] }
ファイルウォッチャーが正しく機能する場合は、src/index.js
を生成する必要があります。ファイル、および実行中npm start
結果は次のようになります。
> node src/index.js Hello
それでは、ついにTypeScriptの最も便利な機能であるタイプの使用を開始しましょう。先に進み、次を作成しますsrc/bot.ts
ファイル:
import {Client, Message} from 'discord.js'; export class Bot { public listen(): Promise { let client = new Client(); client.on('message', (message: Message) => {}); return client.login('token should be here'); } }
これで、ここで必要なもの、つまりトークンを確認できます。ここにコピーして貼り付けるだけですか、それとも環境から直接値をロードしますか?
どちらでもない。代わりに、選択した依存性注入フレームワークであるInversifyJSを使用してトークンを注入することにより、より保守可能、拡張可能、およびテスト可能なコードを記述しましょう。
また、Client
がわかります。依存関係はハードコーディングされています。これも注入します。
に 依存性注入コンテナ 他のオブジェクトをインスタンス化する方法を知っているオブジェクトです。通常、各クラスの依存関係を定義し、DIコンテナがそれらの解決を処理します。
llc s corp c corp
InversifyJSは、依存関係をinversify.config.ts
に配置することをお勧めしますファイルがあるので、先に進んで、そこにDIコンテナを追加しましょう。
import 'reflect-metadata'; import {Container} from 'inversify'; import {TYPES} from './types'; import {Bot} from './bot'; import {Client} from 'discord.js'; let container = new Container(); container.bind(TYPES.Bot).to(Bot).inSingletonScope(); container.bind(TYPES.Client).toConstantValue(new Client()); container.bind(TYPES.Token).toConstantValue(process.env.TOKEN); export default container;
また、 InversifyJSドキュメントは推奨します types.ts
を作成するファイル、および関連するSymbol
とともに使用する各タイプのリスト。これは非常に不便ですが、アプリの成長に伴って名前の衝突が発生しないようにします。各Symbol
説明パラメーターが同じであっても、は一意の識別子です(パラメーターはデバッグのみを目的としています)。
export const TYPES = { Bot: Symbol('Bot'), Client: Symbol('Client'), Token: Symbol('Token'), };
Symbol
sを使用しない場合、名前の衝突が発生したときの外観は次のとおりです。
Error: Ambiguous match found for serviceIdentifier: MessageResponder Registered bindings: MessageResponder MessageResponder
この時点で、それは もっと どのMessageResponder
を分類するのは不便です特にDIコンテナが大きくなる場合は、使用する必要があります。 Symbol
sを使用するとそれが処理され、同じ名前の2つのクラスがある場合に奇妙な文字列リテラルが発生することはありません。
それでは、Bot
を変更しましょうコンテナを使用するクラス。 @injectable
を追加する必要がありますおよび@inject()
それを行うための注釈。これが新しいBot
ですクラス:
import {Client, Message} from 'discord.js'; import {inject, injectable} from 'inversify'; import {TYPES} from './types'; import {MessageResponder} from './services/message-responder'; @injectable() export class Bot { private client: Client; private readonly token: string; constructor( @inject(TYPES.Client) client: Client, @inject(TYPES.Token) token: string ) { this.client = client; this.token = token; } public listen(): Promise { this.client.on('message', (message: Message) => { console.log('Message received! Contents: ', message.content); }); return this.client.login(this.token); } }
最後に、index.ts
でボットをインスタンス化しましょうファイル:
require('dotenv').config(); // Recommended way of loading dotenv import container from './inversify.config'; import {TYPES} from './types'; import {Bot} from './bot'; let bot = container.get(TYPES.Bot); bot.listen().then(() => { console.log('Logged in!') }).catch((error) => { console.log('Oh no! ', error) });
次に、ボットを起動してサーバーに追加します。次に、サーバーチャネルにメッセージを入力すると、次のようにコマンドラインのログに表示されます。
> node src/index.js Logged in! Message received! Contents: Test
最後に、ボット内のTypeScriptタイプと依存性注入コンテナという基盤を設定しました。
この記事の内容の核心である、テスト可能なコードベースの作成に直接進みましょう。つまり、コードはベストプラクティスを実装する必要があります( 固体 )、依存関係を非表示にしない、静的メソッドを使用しない。
また、 実行時に副作用が発生しないようにし、簡単にモックできるようにする必要があります 。
簡単にするために、ボットは1つのことだけを実行します。着信メッセージを検索し、「ping」という単語が含まれている場合は、使用可能なDiscordボットコマンドの1つを使用して、ボットに「pong! 」そのユーザーに。
カスタムオブジェクトをBot
に挿入する方法を示すためオブジェクトとユニットテストを行い、2つのクラスを作成します:PingFinder
およびMessageResponder
。注入しますMessageResponder
Bot
にクラス、およびPingFinder
MessageResponder
に。
コンテンツの空間階層を確立するために使用できるデザインツールに名前を付けます
これがsrc/services/ping-finder.ts
ですファイル:
import {injectable} from 'inversify'; @injectable() export class PingFinder { private regexp = 'ping'; public isPing(stringToSearch: string): boolean { return stringToSearch.search(this.regexp) >= 0; } }
次に、そのクラスをsrc/services/message-responder.ts
に挿入します。ファイル:
import {Message} from 'discord.js'; import {PingFinder} from './ping-finder'; import {inject, injectable} from 'inversify'; import {TYPES} from '../types'; @injectable() export class MessageResponder { private pingFinder: PingFinder; constructor( @inject(TYPES.PingFinder) pingFinder: PingFinder ) { this.pingFinder = pingFinder; } handle(message: Message): Promise { if (this.pingFinder.isPing(message.content)) { return message.reply('pong!'); } return Promise.reject(); } }
最後に、変更されたBot
です。 MessageResponder
を使用するクラスクラス:
import {Client, Message} from 'discord.js'; import {inject, injectable} from 'inversify'; import {TYPES} from './types'; import {MessageResponder} from './services/message-responder'; @injectable() export class Bot { private client: Client; private readonly token: string; private messageResponder: MessageResponder; constructor( @inject(TYPES.Client) client: Client, @inject(TYPES.Token) token: string, @inject(TYPES.MessageResponder) 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.log('Ignoring bot message!') return; } console.log('Message received! Contents: ', message.content); this.messageResponder.handle(message).then(() => { console.log('Response sent!'); }).catch(() => { console.log('Response not sent.') }) }); return this.client.login(this.token); } }
その状態では、MessageResponder
の定義がないため、アプリの実行に失敗します。およびPingFinder
クラス。以下をinversify.config.ts
に追加しましょうファイル:
container.bind(TYPES.MessageResponder).to(MessageResponder).inSingletonScope(); container.bind(TYPES.PingFinder).to(PingFinder).inSingletonScope();
また、タイプシンボルをtypes.ts
に追加します。
MessageResponder: Symbol('MessageResponder'), PingFinder: Symbol('PingFinder'),
これで、アプリを再起動した後、ボットは「ping」を含むすべてのメッセージに応答する必要があります。
そして、これがログでどのように見えるかです:
> node src/index.js Logged in! Message received! Contents: some message Response not sent. Message received! Contents: message with ping Ignoring bot message! Response sent!
依存関係が適切に挿入されたので、単体テストの作成は簡単です。そのためにChaiとts-mockitoを使用します。ただし、使用できるテストランナーやモックライブラリは他にもたくさんあります。
ts-mockitoのモック構文は非常に冗長ですが、理解しやすいものです。 MessageResponder
を設定する方法は次のとおりですサービスと注入PingFinder
それにモック:
let mockedPingFinderClass = mock(PingFinder); let mockedPingFinderInstance = instance(mockedPingFinderClass); let service = new MessageResponder(mockedPingFinderInstance);
モックを設定したので、isPing()
の結果を定義できます。呼び出しは、確認する必要がありますreply()
呼び出します。重要なのは、単体テストでは、isPing()
の結果を定義するということです。電話:true
またはfalse
。メッセージの内容は関係ないため、テストでは'Non-empty string'
を使用します。
when(mockedPingFinderClass.isPing('Non-empty string')).thenReturn(true); await service.handle(mockedMessageInstance) verify(mockedMessageClass.reply('pong!')).once();
テストスイート全体は次のようになります。
import 'reflect-metadata'; import 'mocha'; import {expect} from 'chai'; import {PingFinder} from '../../../src/services/ping-finder'; import {MessageResponder} from '../../../src/services/message-responder'; import {instance, mock, verify, when} from 'ts-mockito'; import {Message} from 'discord.js'; describe('MessageResponder', () => { let mockedPingFinderClass: PingFinder; let mockedPingFinderInstance: PingFinder; let mockedMessageClass: Message; let mockedMessageInstance: Message; 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(true); await service.handle(mockedMessageInstance); verify(mockedMessageClass.reply('pong!')).once(); }) it('should not reply', async () => { whenIsPingThenReturn(false); await service.handle(mockedMessageInstance).then(() => { // Successful promise is unexpected, so we fail the test expect.fail('Unexpected promise'); }).catch(() => { // Rejected promise is expected, so nothing happens here }); verify(mockedMessageClass.reply('pong!')).never(); }) function setMessageContents() { mockedMessageInstance.content = 'Non-empty string'; } function whenIsPingThenReturn(result: boolean) { when(mockedPingFinderClass.isPing('Non-empty string')).thenReturn(result); } });
PingFinder
のテスト嘲笑される依存関係がないため、非常に簡単です。テストケースの例を次に示します。
describe('PingFinder', () => { let service: PingFinder; beforeEach(() => { service = new PingFinder(); }) it('should find 'ping' in the string', () => { expect(service.isPing('ping')).to.be.true }) });
単体テストとは別に、統合テストを作成することもできます。主な違いは、これらのテストの依存関係がモックされていないことです。ただし、外部API接続など、テストしてはならない依存関係がいくつかあります。その場合、モックとrebind
を作成できます。それらをコンテナに追加して、代わりにモックを注入します。これを行う方法の例を次に示します。
Pythonにログインする方法
import container from '../../inversify.config'; import {TYPES} from '../../src/types'; // ... describe('Bot', () => { let discordMock: Client; let discordInstance: Client; let bot: Bot; beforeEach(() => { discordMock = mock(Client); discordInstance = instance(discordMock); container.rebind(TYPES.Client) .toConstantValue(discordInstance); bot = container.get(TYPES.Bot); }); // Test cases here });
これで、Discordボットのチュートリアルは終了です。おめでとうございます。TypeScriptとDIを最初から配置して、きれいに構築しました。このTypeScript依存性注入の例は、任意のプロジェクトで使用するためにレパートリーに追加できるパターンです。
オブジェクト指向の世界をもたらす TypeScript JavaScriptへの変換は、フロントエンドコードとバックエンドコードのどちらで作業している場合でも、優れた拡張機能です。型だけを使用するだけで、多くのバグを回避できます。 TypeScriptに依存性注入を行うことで、JavaScriptベースの開発にさらにオブジェクト指向のベストプラクティスがプッシュされます。
もちろん、言語の制限のため、静的に型付けされた言語ほど簡単で自然なことはありません。ただし、確かなことが1つあります。それは、TypeScript、単体テスト、依存性注入により、開発しているアプリの種類に関係なく、より読みやすく、疎結合で、保守しやすいコードを記述できることです。
関連: アプリではなく、WhatsAppチャットボットを作成するユニットテストが可能で、保守が容易で、疎結合であるという意味で、よりクリーンなコードを記述したい場合は、依存性注入のデザインパターンを使用する必要があります。依存性注入を使用することで、車輪の再発明をせずに、よりクリーンなコードのレシピを得ることができます。
依存性注入を実装することにより、保守が容易なユニットテスト可能なコードを作成する必要があります。依存関係はコンストラクターを介して注入され、単体テストで簡単にモックすることができます。また、このパターンは、疎結合コードを書くことを奨励します。
TypeScriptの主な目的は、型を追加することで、よりクリーンで読みやすいJavaScriptコードを作成できるようにすることです。これは開発者への支援であり、主にIDEで役立ちます。内部的には、TypeScriptは引き続きプレーンJavaScriptに変換されます。
Discordボットは、通信にDiscordAPIを使用するWebアプリです。
Discordボットは、メッセージへの応答、役割の割り当て、反応での応答などを行うことができます。通常のユーザーと管理者が実行できるDiscordアクション用のAPIメソッドがあります。
TypeScriptの主な利点は、開発者が型を定義して使用できることです。型ヒントを使用することにより、トランスパイラー(または「ソースからソースへのコンパイラ」)は、特定のメソッドに渡されるオブジェクトの種類を認識します。エラーや無効な呼び出しはコンパイル時に検出されるため、ライブサーバーのバグが少なくなります。