Tyypit ja testattava koodi ovat kaksi tehokkainta tapaa välttää virheitä, varsinkin kun koodi muuttuu ajan myötä. Voimme soveltaa näitä kahta tekniikkaa JavaScript-kehitykseen hyödyntämällä vastaavasti TypeScript- ja riippuvuusinjektiosuunnittelumallia (DI).
Tässä TypeScript-opetusohjelmassa emme käsittele TypeScript-perusteita suoraan kääntämistä lukuun ottamatta. Sen sijaan esitämme yksinkertaisesti TypeScriptin parhaat käytännöt, kun käymme läpi kuinka tehdä Discord-botti tyhjästä, liittää testit ja DI ja luoda esimerkkipalvelu. Käytämme:
Luo ensin uusi hakemisto nimeltä typescript-bot
. Syötä sitten se ja luo uusi Node.js-projekti suorittamalla:
npm init
Huomaa: Voit käyttää myös yarn
mutta pidämme kiinni npm
lyhyyden vuoksi.
Tämä avaa vuorovaikutteisen ohjatun toiminnon, joka määrittää package.json
tiedosto. Voit turvallisesti vain painaa Tulla sisään kaikkiin kysymyksiin (tai anna joitain tietoja, jos haluat). Asennetaan sitten riippuvuutemme ja dev-riippuvuutemme (ne, joita tarvitaan vain testeihin).
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
Korvaa sitten luotu 'scripts'
osio package.json
kanssa:
'scripts': { 'start': 'node src/index.js', 'watch': 'tsc -p tsconfig.json -w', 'test': 'mocha -r ts-node/register 'tests/**/*.spec.ts'' },
Kaksoislainausmerkit tests/**/*.spec.ts
: n ympärillä tarvitaan tiedostojen rekursiiviseen etsimiseen. (Huomaa: Syntaksi voi vaihdella Windowsia käyttävissä kehittäjissä.)
start
komentosarjaa käytetään botin, watch
komentosarja, jolla käännetään TypeScript-koodi, ja test
suorittaa testit.
Nyt meidän package.json
tiedoston pitäisi näyttää tältä:
{ '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' }
Tarvitsemme tunnuksen voidaksemme olla vuorovaikutuksessa Discord-sovellusliittymän kanssa. Tällaisen tunnuksen luomiseksi meidän on rekisteröitävä sovellus Discord Developer Dashboardiin. Tätä varten sinun on luotava Discord-tili ja mentävä https://discordapp.com/developers/applications/ . Napsauta sitten Uusi sovellus painike:
Valitse nimi ja napsauta Luoda . Napsauta sitten Bot → Lisää Bot , ja olet valmis. Lisätään botti palvelimeen. Mutta älä sulje tätä sivua vielä, meidän on kopioitava tunniste pian.
Botin testaamiseksi tarvitsemme Discord-palvelimen. Voit käyttää olemassa olevaa palvelinta tai luoda uuden. Voit tehdä tämän kopioimalla botin CLIENT_ID
- löydetyn Yleiset tiedot -välilehdeltä - ja käyttämällä sitä osana tätä erityislupa URL:
https://discordapp.com/oauth2/authorize?client_id=&scope=bot
Kun osut tähän URL-osoitteeseen selaimessa, näkyviin tulee lomake, jossa voit valita palvelimen, johon botti lisätään.
Kun olet lisännyt botin palvelimellesi, sinun pitäisi nähdä yllä olevan kaltainen viesti.
.env
TiedostoTarvitsemme jonkin tavan tallentaa tunnuksen sovellukseemme. Tätä varten aiomme käyttää dotenv
-merkkiä paketti. Ensin hanki tunnus Discord Application Dashboardista ( Bot → Napsauta paljastaa tunnus ):
Luo nyt .env
tiedosto, kopioi ja liitä tunnus sitten tähän:
TOKEN=paste.the.token.here
Jos käytät Git-tiedostoa, tämä tiedosto tulisi sijoittaa kohtaan .gitignore
, jotta tunnusta ei vaaranneta. Luo myös .env.example
tiedosto, niin että tiedetään, että TOKEN
tarvitsee määritellä:
TOKEN=
Voit kääntää TypeScriptin käyttämällä npm run watch
komento. Vaihtoehtoisesti, jos käytät PHPStormia (tai muuta IDE: tä), käytä sen tiedostovalvoja TypeScript-laajennuksesta ja anna IDE: n hoitaa kokoamista. Testataan asetuksiamme luomalla src/index.ts
tiedosto sisällöllä:
console.log('Hello')
Luodaan myös tsconfig.json
tiedosto kuten alla. InversifyJS vaatii experimentalDecorators
, emitDecoratorMetadata
, es6
ja 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' ] }
Jos tiedostojen tarkkailija toimii oikein, sen pitäisi luoda src/index.js
tiedosto ja käynnissä npm start
pitäisi johtaa:
> node src/index.js Hello
Aloitetaan nyt vihdoin TypeScriptin hyödyllisin ominaisuus: tyypit. Mene eteenpäin ja luo seuraava src/bot.ts
tiedosto:
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'); } }
Nyt voimme nähdä mitä tarvitsemme: merkin! Aiomme vain kopioida-liittää sen tähän vai ladata arvo suoraan ympäristöstä?
Ei kumpikaan. Sen sijaan kirjoitetaan ylläpidettävämpi, laajennettavissa oleva ja testattavampi koodi injektoimalla tunnus käyttämällä valitsemaamme riippuvuuden injektiokehystä InversifyJS.
Voimme myös nähdä, että Client
riippuvuus on kovakoodattu. Aiomme pistää myös tämän.
TO riippuvuusinjektiosäiliö on esine, joka osaa instantisoida muita esineitä. Tyypillisesti määritämme riippuvuudet kullekin luokalle, ja DI-kontti huolehtii niiden ratkaisemisesta.
InversifyJS suosittelee riippuvuuksien sijoittamista inversify.config.ts
tiedosto, joten mennään eteenpäin ja lisätään DI-kontti sinne:
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;
Myös, InversifyJS-asiakirjat suosittelevat luoden types.ts
tiedosto ja luettelo jokaisesta tyypistä, jota aiomme käyttää, sekä siihen liittyvä Symbol
. Tämä on melko hankalaa, mutta se varmistaa, että nimeämistörmäyksiä ei tapahdu sovelluksemme kasvaessa. Kukin Symbol
on yksilöllinen tunniste, vaikka sen kuvausparametri on sama (parametri on tarkoitettu vain virheenkorjausta varten).
export const TYPES = { Bot: Symbol('Bot'), Client: Symbol('Client'), Token: Symbol('Token'), };
Käyttämättä Symbol
s: ää, tässä näyttää miltä nimitörmäys tapahtuu:
Error: Ambiguous match found for serviceIdentifier: MessageResponder Registered bindings: MessageResponder MessageResponder
Tässä vaiheessa se on tasainen lisää hankalaa selvittää mikä MessageResponder
tulisi käyttää, varsinkin jos DI-säiliömme kasvaa suureksi. Symbol
S: n käyttäminen huolehtii siitä, emmekä ole keksineet outoja merkkijonolitraaleja, jos meillä on kaksi samaa nimeä luokkaa.
Muutetaan nyt Bot
luokan käyttämään astiaa. Meidän on lisättävä @injectable
ja @inject()
merkinnät siihen. Tässä on uusi Bot
luokka:
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); } }
Lopuksi, tehkäämme botti index.ts
-kenttään tiedosto:
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) });
Käynnistä botti ja lisää se palvelimeesi. Jos sitten kirjoitat viestin palvelinkanavalle, sen pitäisi näkyä komentorivin lokeissa näin:
> node src/index.js Logged in! Message received! Contents: Test
Lopuksi meillä on perustukset perustettu: TypeScript-tyypit ja riippuvuusinjektiosäiliö botin sisällä.
Mennään suoraan tämän artikkelin ytimeen: testattavan koodipohjan luomiseen. Lyhyesti sanottuna koodissamme tulisi ottaa käyttöön parhaat käytännöt (kuten KIINTEÄ ), älä piilota riippuvuuksia, älä käytä staattisia menetelmiä.
Myös, sen ei pitäisi aiheuttaa haittavaikutuksia ajettaessa, ja sen on oltava helposti pilkattavissa .
Yksinkertaisuuden vuoksi botti tekee vain yhden asian: se etsii saapuvia viestejä, ja jos yksi sisältää sanan 'ping', käytämme jotakin käytettävissä olevista Discord-botti-komennoista, jotta botti vastaa 'pong!' ” käyttäjälle.
Osoittaa, kuinka mukautettuja objekteja lisätään Bot
objekti ja testaa ne, luomme kaksi luokkaa: PingFinder
ja MessageResponder
. Annamme pistoksen MessageResponder
osaksi Bot
luokka ja PingFinder
osaksi MessageResponder
Tässä on src/services/ping-finder.ts
tiedosto:
import {injectable} from 'inversify'; @injectable() export class PingFinder { private regexp = 'ping'; public isPing(stringToSearch: string): boolean { return stringToSearch.search(this.regexp) >= 0; } }
Sitten injektoimme kyseisen luokan src/services/message-responder.ts
tiedosto:
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(); } }
Viimeiseksi tässä on muokattu Bot
luokka, joka käyttää MessageResponder
luokka:
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); } }
Tässä tilassa sovellus ei toimi, koska MessageResponder
: lle ei ole määritelmiä ja PingFinder
luokat. Lisätään seuraava inversify.config.ts
tiedosto:
container.bind(TYPES.MessageResponder).to(MessageResponder).inSingletonScope(); container.bind(TYPES.PingFinder).to(PingFinder).inSingletonScope();
Lisäksi aiomme lisätä tyypin symboleja types.ts
:
MessageResponder: Symbol('MessageResponder'), PingFinder: Symbol('PingFinder'),
Nyt sovelluksen uudelleenkäynnistyksen jälkeen botin on vastattava jokaiseen viestiin, joka sisältää pingin:
Ja näin näyttää lokit:
> 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!
Nyt kun riippuvuudet on injektoitu oikein, yksikkötestien kirjoittaminen on helppoa. Aiomme käyttää Chai ja ts-mockito siihen; Voit kuitenkin käyttää monia muita testijuoksijoita ja pilkkaavia kirjastoja.
Ts-mockiton pilkkaava syntakse on melko yksityiskohtainen, mutta myös helppo ymmärtää. Näin määrität MessageResponder
huolto ja pistää PingFinder
pilkkaa sitä:
let mockedPingFinderClass = mock(PingFinder); let mockedPingFinderInstance = instance(mockedPingFinderClass); let service = new MessageResponder(mockedPingFinderInstance);
Nyt kun olemme asettaneet pilkkaa, voimme määritellä, mikä tulos isPing()
puheluiden tulisi olla ja tarkistaa reply()
puhelut. Asia on, että yksikötesteissä määritämme isPing()
-tuloksen soita: true
tai false
. Ei ole väliä mikä viestin sisältö on, joten testeissä käytämme vain 'Non-empty string'
when(mockedPingFinderClass.isPing('Non-empty string')).thenReturn(true); await service.handle(mockedMessageInstance) verify(mockedMessageClass.reply('pong!')).once();
Näin koko testipaketti voisi näyttää:
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); } });
Testit PingFinder
ovat melko vähäpätöisiä, koska ei ole pilkattavia riippuvuuksia. Tässä on esimerkki testitapauksesta:
describe('PingFinder', () => { let service: PingFinder; beforeEach(() => { service = new PingFinder(); }) it('should find 'ping' in the string', () => { expect(service.isPing('ping')).to.be.true }) });
Yksikkötestien lisäksi voimme kirjoittaa myös integraatiotestejä. Suurin ero on, että näiden testien riippuvuuksia ei pilkata. On kuitenkin joitain riippuvuuksia, joita ei pitäisi testata, kuten ulkoiset API-yhteydet. Tällöin voimme luoda pilkkaa ja rebind
ne säiliöön niin, että pilkko injektoidaan sen sijaan. Tässä on esimerkki siitä, miten se tehdään:
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 });
Tämä vie meidät Discord bot -opetusohjelmamme loppuun. Onnittelut, olet rakentanut sen siististi, kun TypeScript ja DI ovat paikallaan alusta alkaen! Tämä TypeScript-riippuvuussyöttöesimerkki on malli, jonka voit lisätä ohjelmistoosi käytettäväksi minkä tahansa projektin kanssa.
Tuo kohteen suuntautunut maailma TypeScript JavaScript on suuri parannus, työskentelemme joko käyttöliittymän tai taustakoodin parissa. Pelkkä tyyppien käyttö antaa meille mahdollisuuden välttää monia virheitä. Riippuvuussyöttö TypeScriptissä työntää entistä objektiivisempia parhaita käytäntöjä JavaScript-pohjaiseen kehitykseen.
Kielen rajoitusten vuoksi se ei tietenkään ole koskaan yhtä helppoa ja luonnollista kuin staattisesti kirjoitetuilla kielillä. Mutta yksi asia on varma: TypeScript, yksikötestit ja riippuvuusinjektio antavat meille mahdollisuuden kirjoittaa luettavampaa, löyhemmin kytkettyä ja ylläpidettävää koodia - riippumatta siitä, millaista sovellusta kehitämme.
Liittyvät: Luo WhatsApp Chatbot, ei sovellusSinun tulisi käyttää riippuvuuden ruiskutussuunnittelumallia, jos haluat kirjoittaa puhtaamman koodin siinä mielessä, että se on testattava yksiköllä, ylläpidettävämpi ja löyhästi kytketty. Käyttämällä riippuvuusinjektiota sinulla on resepti puhtaampaan koodiin keksimättä pyörää uudelleen.
Toteuttamalla riippuvuusinjektio pakotetaan kirjoittamaan yksitesti testattava koodi, jota on helppo ylläpitää. Riippuvuudet injektoidaan rakentajien kautta, ja ne voidaan helposti pilkata yksikkökokeissa. Tämä malli kannustaa meitä myös kirjoittamaan löyhästi kytkettyä koodia.
TypeScriptin päätarkoitus on antaa puhtaampi ja helpommin luettavissa oleva JavaScript-koodi lisäämällä tyyppejä. Se on apu kehittäjille, enimmäkseen hyödyllinen IDE: ssä. Konepellin alla TypeScript muunnetaan edelleen tavalliseksi JavaScriptiksi.
Discord-botti on verkkosovellus, joka käyttää Discord-sovellusliittymää viestintään.
Discord-botti voi vastata viesteihin, määrittää rooleja, vastata reaktioilla ja paljon muuta. On olemassa API-menetelmiä kaikille ristiriitatoiminnoille, joita tavalliset käyttäjät ja järjestelmänvalvojat voivat suorittaa.
TypeScriptin tärkein etu on, että kehittäjä voi määritellä ja käyttää tyyppejä. Tyyppivihjeitä käyttämällä transpiler (tai 'lähde lähteestä lähteeseen kääntäjä') tietää, millainen objekti tulisi siirtää tietylle menetelmälle. Mahdolliset virheet tai virheelliset puhelut havaitaan kääntämisen aikana, mikä johtaa vähemmän virheitä live-palvelimelle.