Bekerja Dengan TypeScript, Injeksi Ketergantungan, dan Bot Perselisihan

Diterbitkan: 2022-03-11

Jenis dan kode yang dapat diuji adalah dua cara paling efektif untuk menghindari bug, terutama karena kode berubah seiring waktu. Kita dapat menerapkan dua teknik ini untuk pengembangan JavaScript dengan memanfaatkan TypeScript dan pola desain injeksi ketergantungan (DI), masing-masing.

Dalam tutorial TypeScript ini, kita tidak akan membahas dasar-dasar TypeScript secara langsung, kecuali untuk kompilasi. Sebagai gantinya, kami hanya akan mendemonstrasikan praktik terbaik TypeScript saat kami berjalan melalui cara membuat bot Discord dari awal, menghubungkan tes dan DI, dan membuat layanan sampel. Kami akan menggunakan:

  • Node.js
  • TypeScript
  • Discord.js, pembungkus untuk Discord API
  • InversifyJS, kerangka kerja injeksi ketergantungan
  • Pustaka pengujian: Mocha, Chai, dan ts-mockito
  • Bonus: Mongoose dan MongoDB, untuk menulis tes integrasi

Menyiapkan Proyek Node.js Anda

Pertama, mari buat direktori baru bernama typescript-bot . Kemudian, masukkan dan buat proyek Node.js baru dengan menjalankan:

 npm init

Catatan: Anda juga bisa menggunakan yarn untuk itu, tetapi mari kita tetap npm untuk singkatnya.

Ini akan membuka wizard interaktif, yang akan mengatur file package.json . Anda dapat dengan aman menekan Enter untuk semua pertanyaan (atau memberikan beberapa informasi jika Anda mau). Kemudian, mari kita instal dependensi dan dependensi dev kita (yang hanya diperlukan untuk pengujian).

 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

Kemudian, ganti bagian "scripts" yang dihasilkan di package.json dengan:

 "scripts": { "start": "node src/index.js", "watch": "tsc -p tsconfig.json -w", "test": "mocha -r ts-node/register \"tests/**/*.spec.ts\"" },

Tanda kutip ganda di sekitar tests/**/*.spec.ts diperlukan untuk menemukan file secara rekursif. (Catatan: Sintaks dapat bervariasi untuk pengembang yang menggunakan Windows.)

Skrip start akan digunakan untuk memulai bot, skrip watch untuk mengkompilasi kode TypeScript, dan test untuk menjalankan tes.

Sekarang, file package.json kita akan terlihat seperti ini:

 { "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" }

Membuat Aplikasi Baru di Dasbor Aplikasi Discord

Untuk berinteraksi dengan Discord API, kita memerlukan token. Untuk menghasilkan token seperti itu, kita perlu mendaftarkan aplikasi di Dasbor Pengembang Discord. Untuk melakukan itu, Anda perlu membuat akun Discord dan pergi ke https://discordapp.com/developers/applications/. Kemudian, klik tombol Aplikasi Baru :

Tombol "Aplikasi Baru" Discord.

Pilih nama dan klik Buat . Kemudian, klik BotTambah Bot , dan selesai. Mari tambahkan bot ke server. Tapi jangan tutup halaman ini dulu, kita harus segera menyalin token.

Tambahkan Bot Perselisihan Anda ke Server Anda

Untuk menguji bot kami, kami membutuhkan server Discord. Anda dapat menggunakan server yang ada atau membuat yang baru. Untuk melakukannya, salin CLIENT_ID bot —ditemukan di tab Informasi Umum—dan gunakan sebagai bagian dari URL otorisasi khusus ini:

https://discordapp.com/oauth2/authorize?client_id=<CLIENT_ID>&scope=bot

Saat Anda menekan URL ini di browser, formulir akan muncul di mana Anda dapat memilih server tempat bot akan ditambahkan.

Pesan sambutan Discord Standar sebagai tanggapan atas bot kami yang bergabung dengan server.

Setelah Anda menambahkan bot ke server Anda, Anda akan melihat pesan seperti di atas.

Membuat File .env

Kami membutuhkan beberapa cara untuk menyimpan token di aplikasi kami. Untuk melakukan itu, kita akan menggunakan paket dotenv . Pertama, dapatkan token dari Discord Application Dashboard ( BotClick to Reveal Token ):

Tautan "Klik untuk Mengungkapkan Token" di bagian Bot Discord.

Sekarang, buat file .env , lalu salin dan tempel token di sini:

 TOKEN=paste.the.token.here

Jika Anda menggunakan Git, maka file ini harus ditempatkan di .gitignore , sehingga token tidak terganggu. Juga, buat file .env.example , sehingga diketahui bahwa TOKEN perlu didefinisikan:

 TOKEN=

Mengkompilasi TypeScript

Untuk mengkompilasi TypeScript, Anda dapat menggunakan perintah npm run watch . Atau, jika Anda menggunakan PHPStorm (atau IDE lain), cukup gunakan pengamat filenya dari plugin TypeScript dan biarkan IDE Anda menangani kompilasi. Mari kita uji setup kita dengan membuat file src/index.ts dengan isi:

 console.log('Hello')

Juga, mari kita buat file tsconfig.json seperti di bawah ini. InversifyJS membutuhkan experimentalDecorators , emitDecoratorMetadata , es6 , dan 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" ] }

Jika file watcher berfungsi dengan baik, itu akan menghasilkan file src/index.js , dan menjalankan npm start akan menghasilkan:

 > node src/index.js Hello

Membuat Kelas Bot

Sekarang, mari kita mulai menggunakan fitur paling berguna TypeScript: types. Silakan dan buat file src/bot.ts berikut:

 import {Client, Message} from "discord.js"; export class Bot { public listen(): Promise<string> { let client = new Client(); client.on('message', (message: Message) => {}); return client.login('token should be here'); } }

Sekarang, kita dapat melihat apa yang kita butuhkan di sini: sebuah token! Apakah kita hanya akan menyalin-menempelnya di sini, atau memuat nilainya langsung dari lingkungan?

Juga tidak. Sebagai gantinya, mari kita menulis kode yang lebih dapat dipelihara, dapat diperpanjang, dan dapat diuji dengan menyuntikkan token menggunakan kerangka kerja injeksi ketergantungan pilihan kita, InversifyJS.

Juga, kita dapat melihat bahwa ketergantungan Client di-hardcode. Kami akan menyuntikkan ini juga.

Mengonfigurasi Wadah Injeksi Ketergantungan

Wadah injeksi ketergantungan adalah objek yang tahu cara membuat instance objek lain. Biasanya, kami mendefinisikan dependensi untuk setiap kelas, dan wadah DI menangani penyelesaiannya.

InversifyJS merekomendasikan untuk meletakkan dependensi dalam file inversify.config.ts , jadi mari kita lanjutkan dan tambahkan wadah DI kita di sana:

 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<Bot>(TYPES.Bot).to(Bot).inSingletonScope(); container.bind<Client>(TYPES.Client).toConstantValue(new Client()); container.bind<string>(TYPES.Token).toConstantValue(process.env.TOKEN); export default container;

Juga, dokumen InversifyJS merekomendasikan untuk membuat file types.ts , dan mencantumkan setiap jenis yang akan kita gunakan, bersama dengan Symbol . Ini cukup merepotkan, tetapi memastikan bahwa tidak ada tabrakan penamaan saat aplikasi kita berkembang. Setiap Symbol adalah pengidentifikasi unik, bahkan ketika parameter deskripsinya sama (parameter hanya untuk keperluan debugging).

 export const TYPES = { Bot: Symbol("Bot"), Client: Symbol("Client"), Token: Symbol("Token"), };

Tanpa menggunakan Symbol s, berikut adalah tampilannya ketika terjadi tabrakan penamaan:

 Error: Ambiguous match found for serviceIdentifier: MessageResponder Registered bindings: MessageResponder MessageResponder

Pada titik ini, bahkan lebih merepotkan untuk memilah MessageResponder mana yang harus digunakan, terutama jika wadah DI kita bertambah besar. Menggunakan Symbol s menanganinya, dan kami tidak menemukan literal string yang aneh jika memiliki dua kelas dengan nama yang sama.

Menggunakan Wadah di Aplikasi Bot Discord

Sekarang, mari kita ubah kelas Bot kita untuk menggunakan wadah. Kita perlu menambahkan @injectable dan @inject() untuk melakukannya. Inilah kelas Bot baru:

 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 < string > { this.client.on('message', (message: Message) => { console.log("Message received! Contents: ", message.content); }); return this.client.login(this.token); } }

Terakhir, mari kita instantiate bot kita di file 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<Bot>(TYPES.Bot); bot.listen().then(() => { console.log('Logged in!') }).catch((error) => { console.log('Oh no! ', error) });

Sekarang, mulai bot dan tambahkan ke server Anda. Kemudian, jika Anda mengetik pesan di saluran server, itu akan muncul di log pada baris perintah seperti ini:

 > node src/index.js Logged in! Message received! Contents: Test

Akhirnya, kami telah menyiapkan fondasi: tipe TypeScript dan wadah injeksi ketergantungan di dalam bot kami.

Menerapkan Logika Bisnis

Mari kita langsung ke inti dari artikel ini: membuat basis kode yang dapat diuji. Singkatnya, kode kita harus menerapkan praktik terbaik (seperti SOLID), tidak menyembunyikan dependensi, tidak menggunakan metode statis.

Juga, seharusnya tidak menimbulkan efek samping saat dijalankan, dan mudah diolok-olok.

Demi kesederhanaan, bot kami hanya akan melakukan satu hal: Ini akan mencari pesan masuk, dan jika ada yang berisi kata "ping", kami akan menggunakan salah satu perintah bot Discord yang tersedia agar bot merespons dengan "pong! ” kepada pengguna itu.

Untuk menunjukkan cara menyuntikkan objek khusus ke objek Bot dan menguji unitnya, kita akan membuat dua kelas: PingFinder dan MessageResponder . Kami akan menyuntikkan MessageResponder ke dalam kelas Bot , dan PingFinder ke MessageResponder .

Berikut adalah file 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; } }

Kami kemudian menyuntikkan kelas itu ke file 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<Message | Message[]> { if (this.pingFinder.isPing(message.content)) { return message.reply('pong!'); } return Promise.reject(); } }

Terakhir, berikut adalah kelas Bot yang dimodifikasi, yang menggunakan kelas 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<string> { 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); } }

Dalam keadaan itu, aplikasi akan gagal berjalan karena tidak ada definisi untuk kelas MessageResponder dan PingFinder . Mari tambahkan berikut ini ke file inversify.config.ts :

 container.bind<MessageResponder>(TYPES.MessageResponder).to(MessageResponder).inSingletonScope(); container.bind<PingFinder>(TYPES.PingFinder).to(PingFinder).inSingletonScope();

Juga, kita akan menambahkan simbol tipe ke types.ts :

 MessageResponder: Symbol("MessageResponder"), PingFinder: Symbol("PingFinder"),

Sekarang, setelah memulai ulang aplikasi kita, bot akan merespons setiap pesan yang berisi “ping”:

Bot menanggapi pesan yang berisi kata "ping."

Dan inilah tampilannya di log:

 > 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!

Membuat Tes Unit

Sekarang kita memiliki dependensi yang disuntikkan dengan benar, menulis tes unit menjadi mudah. Kita akan menggunakan Chai dan ts-mockito untuk itu; namun, ada banyak test runner dan library mocking lainnya yang dapat Anda gunakan.

Sintaks mengejek di ts-mockito cukup verbose, tetapi juga mudah dimengerti. Berikut cara menyiapkan layanan MessageResponder dan menyuntikkan tiruan PingFinder ke dalamnya:

 let mockedPingFinderClass = mock(PingFinder); let mockedPingFinderInstance = instance(mockedPingFinderClass); let service = new MessageResponder(mockedPingFinderInstance);

Sekarang setelah kita menyiapkan tiruan, kita dapat menentukan apa hasil dari panggilan isPing() seharusnya dan memverifikasi panggilan reply() . Intinya adalah bahwa dalam pengujian unit, kita mendefinisikan hasil dari panggilan isPing() : true atau false . Tidak masalah apa isi pesannya, jadi dalam pengujian kami hanya menggunakan "Non-empty string" .

 when(mockedPingFinderClass.isPing("Non-empty string")).thenReturn(true); await service.handle(mockedMessageInstance) verify(mockedMessageClass.reply('pong!')).once();

Begini tampilan keseluruhan rangkaian pengujian:

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

Tes untuk PingFinder cukup sepele karena tidak ada dependensi yang bisa diejek. Berikut adalah contoh kasus uji:

 describe('PingFinder', () => { let service: PingFinder; beforeEach(() => { service = new PingFinder(); }) it('should find "ping" in the string', () => { expect(service.isPing("ping")).to.be.true }) });

Membuat Tes Integrasi

Selain pengujian unit, kami juga dapat menulis pengujian integrasi. Perbedaan utama adalah bahwa dependensi dalam tes tersebut tidak diejek. Namun, ada beberapa dependensi yang tidak boleh diuji, seperti koneksi API eksternal. Dalam hal ini, kita dapat membuat tiruan dan rebind kembali ke wadah, sehingga tiruan tersebut disuntikkan sebagai gantinya. Berikut ini contoh cara melakukannya:

 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<Client>(TYPES.Client) .toConstantValue(discordInstance); bot = container.get<Bot>(TYPES.Bot); }); // Test cases here });

Ini membawa kita ke akhir tutorial bot Discord kami. Selamat, Anda membuatnya dengan rapi, dengan TypeScript dan DI dari awal! Contoh injeksi ketergantungan TypeScript ini adalah pola yang dapat Anda tambahkan ke repertoar Anda untuk digunakan dengan proyek apa pun.

TypeScript dan Injeksi Ketergantungan: Bukan Hanya untuk Pengembangan Bot Discord

Membawa dunia TypeScript yang berorientasi objek ke dalam JavaScript adalah peningkatan yang hebat, baik kita sedang mengerjakan kode front-end atau back-end. Hanya menggunakan tipe saja memungkinkan kita untuk menghindari banyak bug. Memiliki injeksi ketergantungan di TypeScript mendorong lebih banyak praktik terbaik berorientasi objek ke pengembangan berbasis JavaScript.

Tentu saja, karena keterbatasan bahasa, tidak akan pernah semudah dan sealami bahasa yang diketik secara statis. Namun satu hal yang pasti: TypeScript, pengujian unit, dan injeksi ketergantungan memungkinkan kita untuk menulis kode yang lebih mudah dibaca, digabungkan secara longgar, dan dapat dipelihara—apa pun jenis aplikasi yang kita kembangkan.

Terkait: Buat Chatbot WhatsApp, Bukan Aplikasi