TypeScript, Dependency Injection ve Discord Botlarıyla Çalışmak

Yayınlanan: 2022-03-11

Tü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:

Discord'un "Yeni Uygulama" düğmesi.

Bir ad seçin ve Oluştur 'u tıklayın. Ardından, BotBot 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.

Botumuzun sunucuya katılmasına yanıt olarak standart Discord karşılama mesajı.

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 ( BotBelirteci Göstermek için Tıklayın ):

Discord'un Bot bölümündeki "Göstergeyi Göstermek için Tıkla" bağlantısı.

Ş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:

Bot, "ping" kelimesini içeren bir mesaja yanıt veriyor.

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.

İlgili: Bir Uygulama Değil, WhatsApp Sohbet Robotu Oluşturun