TypeScript, Dependency Injection ve Discord Botlarıyla Çalışmak
Yayınlanan: 2022-03-11Türler ve test edilebilir kod , özellikle kod zaman içinde değiştikçe, hatalardan kaçınmanın en etkili yollarından ikisidir. Sırasıyla TypeScript ve bağımlılık ekleme (DI) tasarım modelinden yararlanarak bu iki tekniği JavaScript geliştirmeye uygulayabiliriz.
Bu TypeScript eğitiminde, derleme dışında doğrudan TypeScript temellerini ele almayacağız. Bunun yerine, sıfırdan bir Discord botunun nasıl oluşturulacağını, testleri ve DI'yi nasıl bağlayacağınızı ve örnek bir hizmet oluşturacağımızı incelerken TypeScript en iyi uygulamalarını göstereceğiz. Kullanacağız:
- Node.js
- TypeScript
- Discord.js, Discord API için bir sarmalayıcı
- InversifyJS, bir bağımlılık enjeksiyon çerçevesi
- Kitaplıkları test etme: Mocha, Chai ve ts-mockito
- Bonus: Bir entegrasyon testi yazmak için Mongoose ve MongoDB
Node.js Projenizi Kurma
İlk olarak typescript-bot
adında yeni bir dizin oluşturalım. Ardından, girin ve aşağıdakileri çalıştırarak yeni bir Node.js projesi oluşturun:
npm init
Not: Bunun için yarn
de kullanabilirsiniz, ancak kısa olması için npm
bağlı kalalım.
Bu, package.json
dosyasını kuracak olan etkileşimli bir sihirbaz açacaktır. Tüm sorular için güvenle Enter'a basabilirsiniz (veya isterseniz biraz bilgi verebilirsiniz). Ardından bağımlılıklarımızı ve geliştirici bağımlılıklarımızı (yalnızca testler için gerekli olanlar) yükleyelim.
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
Ardından, package.json
oluşturulan "scripts"
bölümünü şununla değiştirin:
"scripts": { "start": "node src/index.js", "watch": "tsc -p tsconfig.json -w", "test": "mocha -r ts-node/register \"tests/**/*.spec.ts\"" },
Dosyaları yinelemeli olarak bulmak için tests/**/*.spec.ts
etrafındaki çift tırnak işaretleri gereklidir. (Not: Sözdizimi, Windows kullanan geliştiriciler için değişiklik gösterebilir.)
start
komut dosyası botu başlatmak için, watch
komut dosyası TypeScript kodunu derlemek için ve test
çalıştırmak için test için kullanılacaktır.
Şimdi package.json
dosyamız şöyle görünmelidir:
{ "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 Uygulamaları Kontrol Panelinde Yeni Bir Uygulama Oluşturma
Discord API ile etkileşim kurmak için bir jetona ihtiyacımız var. Böyle bir jeton oluşturmak için Discord Developer Dashboard'a bir uygulama kaydetmemiz gerekiyor. Bunu yapmak için bir Discord hesabı oluşturmanız ve https://discordapp.com/developers/applications/ adresine gitmeniz gerekir. Ardından, Yeni Uygulama düğmesine tıklayın:
Bir ad seçin ve Oluştur 'u tıklayın. Ardından, Bot → Bot Ekle'yi tıklayın ve işiniz bitti. Botu bir sunucuya ekleyelim. Ancak bu sayfayı henüz kapatmayın, yakında bir jeton kopyalamamız gerekecek.
Discord Botunuzu Sunucunuza Ekleyin
Botumuzu test etmek için bir Discord sunucusuna ihtiyacımız var. Mevcut bir sunucuyu kullanabilir veya yeni bir tane oluşturabilirsiniz. Bunu yapmak için, Genel Bilgiler sekmesinde bulunan botun CLIENT_ID
kopyalayın ve bu özel yetkilendirme URL'sinin bir parçası olarak kullanın:
https://discordapp.com/oauth2/authorize?client_id=<CLIENT_ID>&scope=bot
Bir tarayıcıda bu URL'ye bastığınızda, botun ekleneceği sunucuyu seçebileceğiniz bir form görünür.
Botu sunucunuza ekledikten sonra yukarıdaki gibi bir mesaj görmelisiniz.
.env
Dosyasını Oluşturma
Belirteci uygulamamızda kaydetmenin bir yoluna ihtiyacımız var. Bunu yapmak için dotenv
paketini kullanacağız. İlk olarak, Discord Uygulama Panosundan jetonu alın ( Bot → Belirteci Göstermek için Tıklayın ):
Şimdi bir .env
dosyası oluşturun, ardından belirteci kopyalayıp buraya yapıştırın:
TOKEN=paste.the.token.here
Git kullanıyorsanız, belirtecin tehlikeye atılmaması için bu dosya .gitignore
içine yerleştirilmelidir. Ayrıca, bir .env.example
dosyası oluşturun, böylece TOKEN
tanımlanması gerektiğinin bilinmesi gerekir:
TOKEN=
TypeScript'i Derleme
TypeScript'i derlemek için npm run watch
komutunu kullanabilirsiniz. Alternatif olarak, PHPStorm (veya başka bir IDE) kullanıyorsanız, TypeScript eklentisinden dosya izleyicisini kullanın ve IDE'nizin derlemeyi işlemesine izin verin. İçeriği içeren bir src/index.ts
dosyası oluşturarak kurulumumuzu test edelim:
console.log('Hello')
Ayrıca aşağıdaki gibi bir tsconfig.json
dosyası oluşturalım. InversifyJS, experimentalDecorators
, emitDecoratorMetadata
, es6
ve Reflect reflect-metadata
gerektirir:
{ "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" ] }
Dosya izleyici düzgün çalışıyorsa, bir src/index.js
dosyası oluşturmalı ve npm start
çalıştırılması aşağıdakilerle sonuçlanmalıdır:
> node src/index.js Hello
Bot Sınıfı Oluşturma
Şimdi nihayet TypeScript'in en kullanışlı özelliğini kullanmaya başlayalım: türleri. Devam edin ve aşağıdaki src/bot.ts
dosyasını oluşturun:
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'); } }
Şimdi burada neye ihtiyacımız olduğunu görebiliriz: bir jeton! Buraya kopyalayıp yapıştıracak mıyız yoksa değeri doğrudan ortamdan mı yükleyeceğiz?
Hiç biri. Bunun yerine, tercih edilen bağımlılık enjeksiyon çerçevemiz InversifyJS'yi kullanarak belirteci enjekte ederek daha sürdürülebilir, genişletilebilir ve test edilebilir kod yazalım.
Ayrıca, Client
bağımlılığının sabit kodlandığını görebiliriz. Bunu da enjekte edeceğiz.
Bağımlılık Enjeksiyon Kapsayıcısını Yapılandırma
Bağımlılık ekleme kapsayıcısı , diğer nesneleri nasıl somutlaştıracağını bilen bir nesnedir. Tipik olarak, her sınıf için bağımlılıkları tanımlarız ve DI kapsayıcı bunları çözmeyi üstlenir.
InversifyJS, bağımlılıkları bir inversify.config.ts
dosyasına koymanızı önerir, bu yüzden devam edelim ve DI kapsayıcımızı buraya ekleyelim:
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;
Ayrıca, InversifyJS belgeleri bir types.ts
dosyası oluşturmanızı ve kullanacağımız her türü ilgili bir Symbol
ile birlikte listelemenizi önerir. Bu oldukça elverişsizdir, ancak uygulamamız büyüdükçe hiçbir adlandırma çakışması olmamasını sağlar. Açıklama parametresi aynı olsa bile her Symbol
benzersiz bir tanımlayıcıdır (parametre yalnızca hata ayıklama amaçlıdır).
export const TYPES = { Bot: Symbol("Bot"), Client: Symbol("Client"), Token: Symbol("Token"), };
Symbol
s kullanılmadığında, bir adlandırma çakışması gerçekleştiğinde şu şekilde görünür:
Error: Ambiguous match found for serviceIdentifier: MessageResponder Registered bindings: MessageResponder MessageResponder
Bu noktada, özellikle DI kapsayıcımız büyüyorsa, hangi MessageResponder
kullanılması gerektiğini belirlemek daha da elverişsizdir. Symbol
s kullanmak bununla ilgilenir ve aynı ada sahip iki sınıf olması durumunda garip dize değişmezleri ile karşılaşmadık.
Discord Bot Uygulamasında Kapsayıcıyı Kullanma
Şimdi, konteyneri kullanmak için Bot
sınıfımızı değiştirelim. Bunu yapmak için @injectable
ve @inject()
ek açıklamaları eklememiz gerekiyor. İşte yeni Bot
sınıfı:
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); } }
Son olarak, index.ts
dosyasında botumuzu somutlaştıralım:

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) });
Şimdi botu başlatın ve sunucunuza eklenmesini sağlayın. Ardından, sunucu kanalına bir mesaj yazarsanız, komut satırındaki günlüklerde şöyle görünmelidir:
> node src/index.js Logged in! Message received! Contents: Test
Son olarak, temelleri oluşturduk: TypeScript türleri ve botumuzun içinde bir bağımlılık enjeksiyon kabı.
İş Mantığını Uygulamak
Şimdi doğrudan bu makalenin özüne gidelim: test edilebilir bir kod tabanı oluşturmak. Kısacası, kodumuz en iyi uygulamaları (SOLID gibi) uygulamalı, bağımlılıkları gizlememeli, statik yöntemler kullanmamalıdır.
Ayrıca çalıştırıldığında yan etki oluşturmamalı ve kolayca alay edilebilir olmalıdır.
Basitlik adına, botumuz sadece bir şey yapacak: Gelen mesajları arayacak ve eğer biri "ping" kelimesini içeriyorsa, botun "pong! ” o kullanıcıya.
Bot
nesnesine özel nesnelerin nasıl enjekte edileceğini göstermek ve bunları birim testine tabi tutmak için iki sınıf oluşturacağız: PingFinder
ve MessageResponder
. Bot
sınıfına PingFinder
MessageResponder
MessageResponder
enjekte edeceğiz.
İşte src/services/ping-finder.ts
dosyası:
import {injectable} from "inversify"; @injectable() export class PingFinder { private regexp = 'ping'; public isPing(stringToSearch: string): boolean { return stringToSearch.search(this.regexp) >= 0; } }
Daha sonra bu sınıfı src/services/message-responder.ts
dosyasına enjekte ederiz:
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(); } }
Son olarak, MessageResponder
sınıfını kullanan değiştirilmiş bir Bot
sınıfı:
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); } }
Bu durumda, MessageResponder
ve PingFinder
sınıfları için tanım olmadığından uygulama çalışmayacaktır. Aşağıdakileri inversify.config.ts
dosyasına ekleyelim:
container.bind<MessageResponder>(TYPES.MessageResponder).to(MessageResponder).inSingletonScope(); container.bind<PingFinder>(TYPES.PingFinder).to(PingFinder).inSingletonScope();
Ayrıca types.ts
type sembolleri ekleyeceğiz:
MessageResponder: Symbol("MessageResponder"), PingFinder: Symbol("PingFinder"),
Şimdi, uygulamamızı yeniden başlattıktan sonra, bot "ping" içeren her mesaja yanıt vermelidir:
Ve işte günlüklerde nasıl göründüğü:
> 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!
Birim Testleri Oluşturma
Artık düzgün şekilde enjekte edilmiş bağımlılıklarımız olduğuna göre, birim testleri yazmak kolaydır. Bunun için Chai ve ts-mockito'yu kullanacağız; ancak, kullanabileceğiniz başka birçok test çalıştırıcısı ve alay kitaplığı vardır.
ts-mockito'daki alaycı sözdizimi oldukça ayrıntılıdır, ancak anlaşılması da kolaydır. MessageResponder
hizmetini nasıl kuracağınız ve PingFinder
alayını buna nasıl enjekte edeceğiniz aşağıda açıklanmıştır:
let mockedPingFinderClass = mock(PingFinder); let mockedPingFinderInstance = instance(mockedPingFinderClass); let service = new MessageResponder(mockedPingFinderInstance);
Artık alayları ayarladığımıza göre, isPing()
reply()
sonucunun ne olması gerektiğini tanımlayabilir ve answer() çağrılarını doğrulayabiliriz. Buradaki nokta, birim testlerinde isPing()
çağrısının sonucunu tanımlamamızdır: true
veya false
. Mesaj içeriğinin ne olduğu önemli değil, bu nedenle testlerde sadece "Non-empty string"
kullanıyoruz.
when(mockedPingFinderClass.isPing("Non-empty string")).thenReturn(true); await service.handle(mockedMessageInstance) verify(mockedMessageClass.reply('pong!')).once();
Test paketinin tamamı şöyle görünebilir:
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
testleri, alay edilecek herhangi bir bağımlılık olmadığından oldukça önemsizdir. İşte örnek bir test durumu:
describe('PingFinder', () => { let service: PingFinder; beforeEach(() => { service = new PingFinder(); }) it('should find "ping" in the string', () => { expect(service.isPing("ping")).to.be.true }) });
Entegrasyon Testleri Oluşturma
Birim testleri dışında entegrasyon testleri de yazabiliriz. Temel fark, bu testlerdeki bağımlılıkların alay konusu olmamasıdır. Ancak, harici API bağlantıları gibi test edilmemesi gereken bazı bağımlılıklar vardır. Bu durumda, rebind
oluşturabilir ve onları kapsayıcıya yeniden bağlayabiliriz, böylece sahte bunun yerine enjekte edilir. İşte bunun nasıl yapılacağına bir örnek:
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 });
Bu bizi Discord bot eğitimimizin sonuna getiriyor. Tebrikler, baştan TypeScript ve DI ile temiz bir şekilde oluşturdunuz! Bu TypeScript bağımlılık ekleme örneği, herhangi bir projede kullanmak üzere repertuarınıza ekleyebileceğiniz bir kalıptır.
TypeScript ve Dependency Injection: Yalnızca Discord Bot Geliştirme İçin Değil
TypeScript'in nesne yönelimli dünyasını JavaScript'e getirmek, ister ön uç, ister arka uç kodu üzerinde çalışıyor olalım, harika bir geliştirmedir. Yalnızca türleri tek başına kullanmak birçok hatadan kaçınmamızı sağlar. TypeScript'te bağımlılık eklemeye sahip olmak, JavaScript tabanlı geliştirmeye daha da fazla nesne yönelimli en iyi uygulamaları zorlar.
Elbette dilin sınırlılıkları nedeniyle hiçbir zaman statik olarak yazılan dillerdeki kadar kolay ve doğal olmayacaktır. Ancak kesin olan bir şey var: TypeScript, birim testleri ve bağımlılık ekleme, ne tür bir uygulama geliştiriyor olursak olalım daha okunaklı, gevşek birleştirilmiş ve bakımı yapılabilir kodlar yazmamıza olanak tanıyor.