20211007のNode.jsに関する記事は9件です。

Discord.jsでレベル機能を作ろう!

初めに 今回はDiscordのレベルbotを作っていきます! 必要なもの※よく読まないとエラーが出ます 1.discord.jsv12 (まあはいってるでしょう) 2.node.js (まあ入ってるでしょう) 3.canvas-senpai 4.keyv 5.discord-canvas 入れ方 npm i discord.js@12 discord-canvas keyv canvas-senpai コード const { CanvasSenpai } = require("canvas-senpai"); const canva = new CanvasSenpai(); const { Client, Collection } = require("discord.js"); const Keyv = require('keyv'); const Canvas = require("discord-canvas"); const levels = new Keyv('sqlite://db.sqlite', { table: 'levels' }); const Discord = require('discord.js'); const client = new Discord.Client(); const ranks = new Keyv('sqlite://db.sqlite', { table: 'ranks' }); client.on("message", async message => { const rank = (await ranks.get(message.author.id)) || { count: 0, rank: 1 }; //初期ポイント(0にするとエラー) const level = (await levels.get(message.author.id)) || { count: 0, level: 1 }; //初期ポイント(0にするとエラー) level.count += 1; if (level.count >= 100) { //最大レベル(最大レベルになると次のランクに行きます) level.count = 0; level.level += 1; } if (level.level >= 5) { level.level = 1; rank.rank += 1; } levels.set(message.author.id, level); ranks.set(message.author.id, rank); if (message.content === "!rank") { message.channel.send("少し待ってね") let data = await canva.rankcard({ link: "https://i.pinimg.com/originals/76/0e/d7/760ed7f52c90870503762ac92db92adc.jpg", name: message.author.username, discriminator: message.author.discriminator, level: level.level, rank: rank.rank, currentXP: 100 - level.count, fullXP: 100, avatar: message.author.displayAvatarURL({ format: "png" }) }) const attachment = new Discord.MessageAttachment(data, "rank.png"); await message.channel.send(``, attachment); //上のdataを発言 await message.channel.send("現在のランクです!") } }) client.login("YOURTOKEN"); 説明 コードの中に書きました 最後に 公式サーバー 入る サイト 見る 公式サイト 見る 私のbotをよかったらいれてください 入れる 個別に質問したい場合などはBURI#9515まで プログラムのソースコードは、そのコードを書いた人の著作物にあたる ため、著作権法による保護の対象となります。 そのため、ライセンスが定められていないプログラムは、その著作者から明示的に許可を受けない限り、ソースコードを複製したり、再頒布したり、手を加えたりすることができません。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

CodeceptJSで受信メールの検証、MailSlurp vs gmail お薦めはMailSlurp

概要 ユーザー登録機能やパスワード再設定機能を持ったシステム、あるいはお客様へのシステム更新情報などの通知ではメール送信する場合があり、送信されたメール文言の確認や貼られたURLを辿ってWeb画面の操作を行う場合があります。自動テストを行う場合、一旦WebUIを離れての操作になるため、メール機能をサポートするAPIを利用したテストになります。 よく開発・実装段階では、ローカル環境にメールサーバ(注1)を立ち上げて利用することもあり、検証環境についても同環境を利用するケースもありますが、本件で紹介するのは、外部のメールサーバを利用した事例となります。 CodeceptJSでは、MailSlurpを利用したhelperが公開されており(結局使わなかったけど)これを使った場合と、Node.jsのライブラリを利用してgmailを利用した場合とを比較してみました。 環境: Windows10 64bit 20H2 64GByte 第10世代Core i7 Node v15.8.0 簡単に両者を比較すると以下のようですが、お薦めは「MailSlurp」です。 メール受信 必要なインストール 受信手段 利用するメールサーバ 特記事項 MailSlurp MailSlurp-client メールがこちらに送信された後のLastMailメソッド MailSlurp 利用頻度によっては有料プランが必要。無料だと100通/月まで gmail inboxおよびmailParserおよびiconv メール待受け中の着信に反応 gmail IMAPが必須。gmailの外からのアプリケーションアクセスの許可が必須 MailSlurpを利用する場合は、MailSlurp(利用にはアカウント作成が要ります)とMailSlurp-clientとを使う点、汎用メールサーバを利用する場合と違い、メール送信相手先を間違えた誤送信を減らすことにもなろうかと思います。(ランダム発生されたメールアドレスを利用します) gmail等を使って既に手動テストをしている場合、メール本文の検証や記載されたリンクへの移動検証について自動テストにすることができます。ただし、MailSlurpのような閉じたメールシステムではないので、誤送信のリスクが残ります。また、IMAPを使えるような設定変更を伴いますので(gmailではデフォルトOffに設定されています)、メール誤送信・セキュリティといった2つのリスクを伴います。 参考にした資料: CodeceptJS、MailSlurpでメールも簡単自動テスト (こちらは、実際にパスワード再発行のフローをテストする事例紹介になっていました。) CodeceptJSのマニュアル(英文) MailSlurpのマニュアル(英文)(JavaやPython、Ruby等の開発言語毎にドキュメントが用意されています ) アカウント作成時のメール認証リンクをMailSlurp + Pythonで踏んでみた IMAPでGmailを受信する(Node.js編) nodejsでメール受信処理とpuppeteerを組み合わせる node.jsでGmailメール受信をトリガーしてなにか処理する CodeceptJSとPuppeteerでE2Eテスト(MailSlurp) MailSlurpを利用した方が簡単です。頑張ればgmail(あるいは他のメールクライアントも)もできますが、 非同期・同期処理などのNode.jsに関するスキルも要求されますので、自作するのは根性要るかもしれません。 注1: CodeceptJSでは、MailCatcher連携やMailHog連携で利用可能なPlugINやカスタムhelper等も公開されています。 メール受信のなにを自動テストするか 例えばメール連携のWebUI機能には、私自身がユーザーの立場では、以下のようなものがあるかと思います。(もっとあるかもしれない) 商品・サービスの利用開始にあたって、着信したメールに記載のkeyを、指定のWebサイトにて用いて、アクティベートする。 利用中のサービスを継続して利用するために、更新時期が到来したらメールに記載のWebサイトで更新手続きをする。 商品購入・サービス利用開始の後、利用できる期間限定のクーポンを入手するため、メール連絡に記載のアドレスに移動する。 ユーザー仮登録の後、着信したメールに記載のコードを使って、指定のWebサイトで本登録を実施する。 参加予定のイベントの開始日時が近づいたら、リマインドメールが着信して、カレンダーで予定の確認ができる。 商品購入後に、近くの店舗に着荷連絡をメールにて受信し、記載のリンクを携帯端末に表示させて、お店に商品を受取りに行く。 お薦めの商品・サービスについて、メール受信し、記載のリンクを訪れる。 着信したメールに記載されたキーワードとURLを参照して、URLの移動先でキーワードを使うといったテストケースが代表的なものとして挙げられると思います。 その他、受信メールの自動テストで確認しておきたいとしたら、 メール本文の長さ・行数で、検証に制約が発生しないか。文言の間違いが摘出できるか。 本文からURLの検出と移動が、例えばURLっぽいのが複数記載されていると、どうなるのか。 テストコードの実装が、どれくらい楽に出来るか。 以上を鑑みて、メール受信回りの自動テストを、Selenium学習サイトのログイン機能を使って、下記のような概要としました。 メール本文: [会員ランク] [会員氏名]様。 この度は当ホテルに会員登録いただき、誠にありがとうございました。 登録手続きが完了いたしましたので、ご連絡申し上げます。 ユーザーID:[メールアドレス] パスワードは登録時に設定頂いたものをお使い頂けます。 ご登録頂いた内容につきましては、以下のアドレスにてご確認頂けます。 [URL] それでは、ご利用を心からお待ち申し上げております。 [トップページのURL] 以上のメール本文から、ユーザーIDをログインページで入力して、ユーザー情報のページに移動した後、会員ランクと会員氏名が間違っていないか(メールに記載のテキストとユーザー情報ページに記載のテキストとが一致するか)のテストとします。 尚、後述するテストコードには、上記のメールを送信する機能も含んでいます。実際のシステムでは、 テストコードでメール送信するのではなく、例えばユーザー登録機能や会費の決済、あるいは、日時をトリガとしたシステムのメール送信機能(最新の着信でテストできるかもしれませんが、時限式に発動した場合はテストも直後に呼び出されると良いかと思います)が動作することになろうかと思います。 仮のメールを送信する手段として、MailSlurp-clientのメール送信機能とNodemailerを利用した関係上、テストコードに含まれている点、ご留意ください。 それではここから、それぞれのテスト環境・準備やテストコード、テスト結果について紹介します。 MailSlurpを利用した自動テスト環境の例 結論から言うと、CodeceptJSのサイトで公開しているhelperだけでは、どうしても旨くできませんでした。 CodeceptJSを先にインストールしてから、emailのhelperをインストールしようとすると PS C:\works\EmailTest\MailSlurp03> npm i @codeceptjs/mailslurp-helper --save-dev npm ERR! code ERESOLVE npm ERR! ERESOLVE unable to resolve dependency tree npm ERR! npm ERR! While resolving: undefined@undefined npm ERR! Found: codeceptjs@3.1.2 npm ERR! node_modules/codeceptjs npm ERR! codeceptjs@"^3.1.2" from the root project npm ERR! npm ERR! Could not resolve dependency: npm ERR! peer codeceptjs@"^2.3.0" from @codeceptjs/mailslurp-helper@1.0.5 npm ERR! node_modules/@codeceptjs/mailslurp-helper npm ERR! dev @codeceptjs/mailslurp-helper@"*" from the root project npm ERR! npm ERR! Fix the upstream dependency conflict, or retry npm ERR! this command with --force, or --legacy-peer-deps npm ERR! to accept an incorrect (and potentially broken) dependency resolution. npm ERR! npm ERR! See C:\Users\mogua\AppData\Local\npm-cache\eresolve-report.txt for a full report. npm ERR! A complete log of this run can be found in: npm ERR! C:\Users\mogua\AppData\Local\npm-cache\_logs\2021-09-20T08_51_54_779Z-debug.log PS C:\works\EmailTest\MailSlurp03> となり、インストールできません。codeceptJSのバージョンにシビアなのでしょうか? 仕方なく、先にhelperをインストールしてからcodeceptJSをインストールしましたが、今度は、データ駆動が使えませんでした。結局、helperでインストールされるコードの中を見たり、mailSlurpのホームページを見たりしながら、改めてmailSlurp-clientをインストールして(以下)、Node.jsのコードをゴリゴリ記述することになりました。 npm install mailslurp-client その他、assertContainをcodeceptjs-chai から利用したのでインストールしています。 CodeceptJSの初期化(コマンドラインでnpx codeceptjs init)については、helperにwebdriverioを利用して行います。前述のように、helperとしてMailslurp-client を使いませんでしたので、codecept.conf.js の修正は、allureレポートとCodecept-chaiのみです。 codecept.conf.js exports.config = { tests: 'email_test/*_test.js', output: './output', helpers: { WebDriver: { url: 'http://localhost', browser: 'chrome' }, "ChaiWrapper" : { "require": "codeceptjs-chai" } }, include: { I: './steps_file.js' }, bootstrap: null, mocha: {}, name: 'MailSlurp02', translation: 'ja-JP', plugins: { pauseOnFail: {}, retryFailedStep: { enabled: true }, tryTo: { enabled: true }, screenshotOnFail: { enabled: true }, "ChaiWrapper" : { "require": "codeceptjs-chai" }, allure: { enabled: true } } } テストメールを送信するために、MailSlurp-clientのメール送信機能を使っています。 メールの送信と受信機能とをNodeのモジュールとして以下のように作成して臨みます。 emailslurpCommon.js const { MailSlurp } = require("mailslurp-client"); const mailslurp = new MailSlurp({ apiKey: "ココにAPI Key" }); let currentEmail = null; module.exports.sendMail = function(rank, subject, simei, id, url){ var address = 'ここにメールアドレス@mailslurp.com'; var body01 = '様。\r\n'; var body02 = 'この度は当ホテルに会員登録いただき、誠にありがとうございました。\r\n'; var body03 = '登録手続きが完了いたしましたので、ご連絡申し上げます。\r\n'; var body04 = 'ユーザーID:'; var body05 = 'パスワードは登録時に設定頂いたものをお使い頂けます。\r\n'; var body06 = 'ご登録頂いた内容につきましては、以下のアドレスにてご確認頂けます。\r\n'; var body07 = 'それでは、ご利用を心からお待ち申し上げております。\r\nhttps://hotel.testplanisphere.dev/ja/index.html'; var body = rank + '\r\n' + simei + body01 + body02 + body03 + body04 + id + '\r\n' + body05 + body06 + url + '\r\n' + body07; // console.log(body); const sentEmail = mailslurp.inboxController.sendEmailAndConfirm( "ここにMailSlurpのID", { to: [address], subject: subject, body: body, }, ); }; module.exports.receiveMail = async function(){ var emails = await mailslurp.waitForLatestEmail('ここにMailSlurpのID', 60000, true); currentEmail = emails; return emails; }; API keyは、MailSlurpにアカウント作成すると発行されますのでコピーして使います。 また、MailSlurpにinbox(メールを受信する箱みたいなもの)を作成すると、ランダムに生成されたIDとメールアドレスが発行されますので、こちらもテストコードにコピーしておきます。 Subjectや会員情報はテストデータを使い、送信先はMailSlurpで作成したinboxのアドレスを指定します。 メール受信は、waitForLastEmailで取得できます。 メール送受信が出来るようになりましたので、受信したメールから文言やURLを拾ってWebサイトに移動してテストするのは、 emailTest_test.js let emailTestTable = new DataTable(['Subject', 'url', 'verifyTitle', 'verifyWord', '会員ランク', '会員氏名', '会員ID', '会員Pass']); emailTestTable.add(['会員登録完了のご連絡001','https://hotel.testplanisphere.dev/ja/login.html','ログイン | HOTEL PLANISPHERE - テスト自動化練習サイト','ご利用を心からお待ち申し上げております。', 'プレミアム会員', '山田一郎', 'ichiro@example.com', 'password']); emailTestTable.add(['会員登録完了のご連絡002','https://hotel.testplanisphere.dev/ja/index.html','ログイン | HOTEL PLANISPHERE - テスト自動化練習サイト','ご利用を心からお待ち申し上げております。', 'プレミアム会員', '山田一郎', 'ichiro@example.com', 'password']); emailTestTable.add(['会員登録完了のご連絡003','https://hotel.testplanisphere.dev/ja/login.html','ログイン | HOTEL PLANISPHERE - テスト自動化練習サイト','ご利用を心からお待ち申し上げております。', '一般会員', '山田一郎', 'ichiro@example.com', 'password']); emailTestTable.add(['会員登録完了のご連絡004','https://hotel.testplanisphere.dev/ja/login.html','ログイン | HOTEL PLANISPHERE - テスト自動化練習サイト','ご利用を心からお待ち申し上げます。', 'プレミアム会員', '山田一郎', 'ichiro@example.com', 'password']); emailTestTable.add(['会員登録完了のご連絡005','https://hotel.testplanisphere.dev/ja/login.html','ログイン | HOTEL PLANISPHERE - テスト自動化練習サイト','ご利用を心からお待ち申し上げております。', '一般会員', '松本さくら', 'sakura@example.com', 'pass1234']); emailTestTable.add(['会員登録完了のご連絡006','https://hotel.testplanisphere.dev/ja/login.html','ログイン | HOTEL PLANISPHERE - テスト自動化練習サイト','ご利用を心からお待ち申し上げております。', 'プレミアム会員', '林潤', 'jun@example.com', 'pa55w0rd!']); emailTestTable.add(['会員登録完了のご連絡007','https://hotel.testplanisphere.dev/ja/login.html','ログイン | HOTEL PLANISPHERE - テスト自動化練習サイト','ご利用を心からお待ち申し上げております。', '一般会員', '木村良樹', 'yoshiki@example.com', 'pass-pass']); Feature('emailTest'); Data(emailTestTable).Scenario('eMail_Test', async({I , current}) => { var mailBox = require('../emailslurpCommon'); //テスト用メール送信 mailBox.sendMail(current.会員ランク,current.Subject,current.会員氏名,current.会員ID,current.url); //ここまで //メール受信検証 let mails = await mailBox.receiveMail(); //文面の文言検証 I.assertContain(mails.body, current.verifyWord); const memberid = await mails.body.match(/[A-Za-z0-9_.-]*@example.com/)[0]; const url = await mails.body.match(/(https?|ftp)(:\/\/[\w\/:%#\$&\?\(\)~\.=\+\-]+)/g)[0]; console.log(mails.body); //文面から取得したURLに移動 await I.amOnPage(url); //移動先の検証、ここではタイトル I.seeTitleEquals(current.verifyTitle); //文面から取得したアカウントでログイン I.fillField('email', memberid); I.fillField('password', current.会員Pass); I.waitForClickable('#login-button'); I.click('#login-button'); //文面から移動した会員ページの検証 I.see(current.会員ID); I.see(current.会員氏名); I.see(current.会員ランク); I.click('#logout-form > button'); }); Selenium学習サイトに登録済みの4名を使ったテスト(会員登録完了のご連絡001 / 会員登録完了のご連絡005 / 会員登録完了のご連絡006 / 会員登録完了のご連絡007)と、 リンク先URLが間違っているケース(会員登録完了のご連絡002)、会員種別が間違っているケース(会員登録完了のご連絡003)、メール文言の一部が間違っているケース(会員登録完了のご連絡004)をテストデータとして準備しています。 mails.body だけで、メールから本文だけ取得できます。本文さえ取得できれば、あとは正規表現で会員IDだの移動先のURLだの抜き出すだけです。 gmailを利用した自動テスト環境の例 gmail については、helperによるサポートは無く、またMailSlurp-clientのような専用clientも有りませんので、自作となります。(注2) inbox と mailParserにiconv、テストメールの送信にnodemailerをインストールして臨みます。 iconvのインストールには、python2.* 必須というのも、心が折れそうになりました。 codecept.conf.js は、MailSlurp同様に、codeceptjs-chaiとAllureレポートを追加するだけです。 テストメールをnodemailerで送信するmoduleは、 ext_program.js module.exports.runSendMail = function(address,rank,subject,simei,id,url,callback_func) { var nodemailer = require('nodemailer'); var smtpConfig = { port: 465, host: 'smtp.gmail.com', secureConnection: true, auth: { user: 'ココにgmailのID', pass: 'ココにgmailのパスワード' } }; const transporter = nodemailer.createTransport(smtpConfig); var body01 = '様。\r\n'; var body02 = 'この度は当ホテルに会員登録いただき、誠にありがとうございました。\r\n'; var body03 = '登録手続きが完了いたしましたので、ご連絡申し上げます。\r\n'; var body04 = 'ユーザーID:'; var body05 = 'パスワードは登録時に設定頂いたものをお使い頂けます。\r\n'; var body06 = 'ご登録頂いた内容につきましては、以下のアドレスにてご確認頂けます。\r\n'; var body07 = 'それでは、ご利用を心からお待ち申し上げております。\r\nhttps://hotel.testplanisphere.dev/ja/index.html'; var body = rank + '\r\n' + simei + body01 + body02 + body03 + body04 + id + '\r\n' + body05 + body06 + url + '\r\n' + body07; transporter.sendMail({ from: '送信元アドレス', to: 'テストに使うgmailアドレス', subject: subject, text: body }, function(e, res){ console.log(e ? e.message : res.message); smtp.close(); }); return body; } メール本文作成して、Subjectと送信先・送信元を添えてnodemailerで送信するのは、MailSlurp-clientの送信機能同様なのですが、受信がたいへんでした。mail.body なんて訳にはいきません。 gmail_imap.js var inbox = require('inbox'); var iconv = require('iconv'); var sleep = require('sleep-async')(); const simpleParser = require('mailparser').simpleParser; var ptn_string = "Content-Type: text/plain; charset="; var ptn2_string = "Content-Type: text/html; charset="; var ptn_encode_string = "Content-Transfer-Encoding: "; var mime_string = "MIME-Version"; var client; module.exports.run3 = async function(username,password) { var res; client = inbox.createConnection(false, "imap.gmail.com", { secureConnection: true, auth:{ user: username, pass: password } }); client.connect(); sleep.sleep(2000, function(){}); let promise1 = await new Promise((resolve, reject) => { client.on("connect", function(){ resolve('Successfully connected to server' + '\n'); }); }).then(async(onRes) => { let promise2 = await new Promise((resolve, reject) => { client.openMailbox("INBOX", function(error, info){ sleep.sleep(2000, function(){}); if(error){ throw error; reject('error'); } resolve('Message count in INBOX: ' + info.count); }); }).catch((openRes) => openRes); res = onRes + promise2; }); return await res; }; module.exports.parsedMail = async function(uid) { var resMail; var messageStreame = await client.createMessageStream(uid); await simpleParser(messageStreame) .then(mail=> { resMail = mail.text; }) .catch(err=> { console.log(err); }); client.close(); return resMail; }; module.exports.receive2 = async function(){ console.log('Mail listen...'); var res; let promise3 = await new Promise(async(resolve, reject) => { await client.on("new", async function(message){ if(message == null){ reject('error'); } var body01 = '----------------------------------------------------------' + '\n'; var body02 = '日時:' + await message.date + '\n'; var body03 = '送信者:' + await message.from.name + '-' + await message.from.address + '\n'; var body04 = 'タイトル:' + await message.title + '\n'; var body = body01 + body02 + body03 + body04; resolve(message.UID); }); }).then((uid) => { res = uid; }); return await res; } module.exports.close = function(){ client.close(); } CodeceptJSはNodeで動くのですが、今までほとんどNodeを意識せずに済んだので、そもそもノン・ブロック(記述したテストコードが単純に上から順番に動作するのでは無く、処理時間が係ることについては、次の行に記述したテストコードの実行にとりかかる。待たない。)で動作すること自体が新たな発見で、結局、Node.js の参考書をお取り寄せして、Promise化だのcallBackから値を受け取るだの、学習が必要となりました。await とか、なんとなく使っていたのですが、真面目に取り組むことになりました。inboxの仕様をガッツリ見て、時にはRAWデータも眺めながらのテストコード実装となりました。inbox以外にもimapクライアントは存在するのですが、自分の環境ではbase64デコード(simpleParser)との組み合わせで唯一旨く動いています。日本語メールのデコードが文字化けしないのが、この組み合わせが唯一でした。gmail以外にもメールサーバ(IMAPが使えること必須)はありますが、inboxのような汎用imapクライアントとデコードを頑張らないと実装できません。 run3メソッドが、inboxをgmailに接続するためのもの、receive2メソッドが待受けで着信したuidを取得するためのもの、parseMailメソッドが着信したuidのメールデータから、メール本文をbase64でデコードするためのものです。 つくづくMailSlurp-clientは簡単だと思いました。メール受信と本文抽出のコード量が随分と違います。 テストデータはMailSlurpと同じものを使いましたが、gmailの場合は、テストメール送信と着信の検証をするための、それぞれのメソッドとノン・ブロックに注意したテストコードにする必要があり、MailSlurp-clientのように、最後に着信したメールを選ぶのではなく、待受けるinboxでは、難しさがあります。 mailTest_test.js let emailTestTable = new DataTable(['Subject', 'url', 'verifyTitle', 'verifyWord', '会員ランク', '会員氏名', '会員ID', '会員Pass']); emailTestTable.add(['会員登録完了のご連絡001','https://hotel.testplanisphere.dev/ja/login.html','ログイン | HOTEL PLANISPHERE - テスト自動化練習サイト','ご利用を心からお待ち申し上げております。', 'プレミアム会員', '山田一郎', 'ichiro@example.com', 'password']); emailTestTable.add(['会員登録完了のご連絡002','https://hotel.testplanisphere.dev/ja/index.html','ログイン | HOTEL PLANISPHERE - テスト自動化練習サイト','ご利用を心からお待ち申し上げております。', 'プレミアム会員', '山田一郎', 'ichiro@example.com', 'password']); emailTestTable.add(['会員登録完了のご連絡003','https://hotel.testplanisphere.dev/ja/login.html','ログイン | HOTEL PLANISPHERE - テスト自動化練習サイト','ご利用を心からお待ち申し上げております。', '一般会員', '山田一郎', 'ichiro@example.com', 'password']); emailTestTable.add(['会員登録完了のご連絡004','https://hotel.testplanisphere.dev/ja/login.html','ログイン | HOTEL PLANISPHERE - テスト自動化練習サイト','ご利用を心からお待ち申し上げます。', 'プレミアム会員', '山田一郎', 'ichiro@example.com', 'password']); emailTestTable.add(['会員登録完了のご連絡005','https://hotel.testplanisphere.dev/ja/login.html','ログイン | HOTEL PLANISPHERE - テスト自動化練習サイト','ご利用を心からお待ち申し上げております。', '一般会員', '松本さくら', 'sakura@example.com', 'pass1234']); emailTestTable.add(['会員登録完了のご連絡006','https://hotel.testplanisphere.dev/ja/login.html','ログイン | HOTEL PLANISPHERE - テスト自動化練習サイト','ご利用を心からお待ち申し上げております。', 'プレミアム会員', '林潤', 'jun@example.com', 'pa55w0rd!']); emailTestTable.add(['会員登録完了のご連絡007','https://hotel.testplanisphere.dev/ja/login.html','ログイン | HOTEL PLANISPHERE - テスト自動化練習サイト','ご利用を心からお待ち申し上げております。', '一般会員', '木村良樹', 'yoshiki@example.com', 'pass-pass']); Feature('eMailTest'); Data(emailTestTable).Scenario('eMail_Test', async ({I , current}) => { var gi = require('../gmail_imap.js'); var xp = require('../ext_program.js'); //inbox待受け startMessage = await gi.run3("ココにgmailのID","ココにgmailのパスワード"); I.wait(2); console.log(startMessage); //テスト用メール送信 var sendMail = xp.runSendMail("テストに使うgmailアドレス",current.会員ランク,current.Subject,current.会員氏名,current.会員ID,current.url,null); //ここまで //メール受信検証 let newID = await gi.receive2(); if(newID != null){ mail = await gi.parsedMail(newID); await console.log(mail); } //文面の文言検証 I.assertContain(mail, current.verifyWord); const memberid = await mail.match(/[A-Za-z0-9_.-]*@example.com/)[0]; const url = await mail.match(/(https?|ftp)(:\/\/[\w\/:%#\$&\?\(\)~\.=\+\-]+)/g)[0]; //文面から取得したURLに移動 I.amOnPage(url); //移動先の検証、ここではタイトル I.seeTitleEquals(current.verifyTitle); //文面から取得したアカウントでログイン I.fillField('email', memberid); I.fillField('password', current.会員Pass); I.waitForClickable('#login-button'); I.click('#login-button'); //文面から移動した会員ページの検証 I.see(current.会員ID); I.see(current.会員氏名); I.see(current.会員ランク); I.click('#logout-form > button'); }); sendmailをawaitにせず、ノン・ブロックで、次のreceiveで新規の着信を受けるということをしています。 新規の着信を待っているところにメールが送られてこないと受信できないからです。ここを間違えると、 いつまでもメールが受信できなかったり、次のテストデータで受信してしまったりします。 本件ではテストメール送信のためにsendMailしていますが、実際のWebシステムでは、画面操作(CodeceptJSで記述)と最後にclickが入ることになると思います。 注2: Gmail-clientというのもあるのですが、環境構築に当たって、有料プランが必要になりました。 テスト結果の確認 MailSlurpを利用した場合は、こんな感じになります。 CodeceptJSとMailSlurpでメール文面テスト pic.twitter.com/M65KQuAFZN— kazuhiroYoshino (@XSyrIQwKY7Yuy6m) October 6, 2021 コンソールに着信したメールが表示されて、引き続きWebサイトに移動して検証のようすですが、 エラー混入させたデータ含めて、7件のテストが、2分弱の所要時間で終わります。 MailSlurpの管理コンソールに着信したメールも確認できます。 gmailを利用した場合は、こんな感じ。 CodeceptJSとgmailでメール文面テスト pic.twitter.com/584BygRLZD— kazuhiroYoshino (@XSyrIQwKY7Yuy6m) October 6, 2021 gmailにテスト送信した文章が届いている様子が判ります。 AllureレポートにMailSlurpとgmailとで、テスト結果に差異はありません。 また、混入させたエラーも検出できました。 10行程度の本文でしたが、Node.jsのmatch関数では、特に問題なく本文からのキーワード抽出が出来ています。 本文には2つURLが併記されていますが、移動に利用するのを最初のURLと決めていましたので const url = await mail.match(/(https?|ftp)(:\/\/[\w\/:%#\$&\?\(\)~\.=\+\-]+)/g)[0]; それが間違っていた場合は、エラー検出の対象となり、 文面に記載のURL間違い(ログイン画面への導線なのに、トップページになっていた) 会員種別の間違いパターン(プレミアム会員なのにメール文面では一般会員) こういったエラーが発生するのかどうかはさておき、万が一、会員種別がメールで間違っていたら、不安になるのではないでしょうか。 メール文言の間違い(正しくは「ご利用を心からお待ち申し上げております」のところを「ご利用を心からお待ち申し上げます」になっている。) メール文言が一字一句、仕様通りなのかについては、自動テストしなくてもDiff等の汎用ツールで検出できると思います。ただ、このような挨拶文については、業種によってはとても大切なことである場合も、多いかと思います。 総括 ソフト開発・実装時のメール送受信確認の場合、外部にメール送信せずに、ローカル環境にSMTPを設置して(例えば、mailCatcher。こちらもCodeceptJSでhelperが存在します。)メール送信を行い、メールボックスに送信されたメールが着信して、本文の確認も行うといったことが行われていると思いますが、システムテストや受入れテスト段階になると実際に外部にメール送信して、例えばgmailで、件名や本文・貼られたリンクの検証をするケースも実際にはあろうかと思います。WebUIの外になるため、なかなか自動テストで扱いにくい案件ですが、本件のように、テスト用のメールアカウントとAPIが公開されているサービスとを使えば、自動テストも可能です。 今回の事例程度のメールなら、自動テストでサポートできるメール受信テストは、あまり魅力ないかもしれません。また、メールを受信するデバイスがテストとして有効な場合などでも、本件はあまり効果を期待できないと思います。 しかし、例えば、ワークフローにメールを使うような、メール命なシステムの場合は、メール本文に記載のキーワードに間違いが無いことのテストが自動テストでサポートされると、安心して開発に臨めると考えます。 CodeceptJSは、自動テストを推進するメンバーが、WebUIの操作や検証のためのテストコードを自前で作成しなくてもバンドルされている点が強力だと思います。 受信したメールからSubjectや送信元、本文がそれぞれ抽出できれば、あとは、Nodeの関数やCodeceptJSが活躍してくれます。CodeceptJSならWebUI操作についてテストコードを積極的に実装しないで済む分、ちょっと敷居の高そうなメール受信についても、自動テストに取り込む時間がとれるのではないでしょうか。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

TypeORMでQueryBuilderを使わずにJOIN先のテーブルのカラムをWhere句で使用する

注意 この記事の公開から時間が経っていたり、typeormのバージョンが異なる場合は、仕様が異なる場合があります。 動作確認: 2021/10/7 typeorm: v0.2.34 TL;DR JOIN先の主キー(ID)で絞り込む場合 const teenProgrammers = await getRepository(User).find({ relations: ['role'], // なくてもOK where: { age: LessThanOrEqual(19), role: { id: programmer.id } } }) JOIN先の主キー(ID)以外で絞り込む場合 const teenProgrammers = await getRepository(User).find({ join: { alias: "user", innerJoin: { role: "user.role" }, }, where: (qb: SelectQueryBuilder<User>) => { qb.where({ age: LessThanOrEqual(19) }).andWhere( "role.name = :roleName", { roleName: "programmer" } ); }, }); 前提条件 エンティティ entity/role.ts @Entity() export class Role { @PrimaryGeneratedColumn() id?: number; @Column() name: string; @OneToMany(() => User, user => user.id) users: User[]; } entity/user.ts @Entity() export class User { @PrimaryGeneratedColumn() id?: number; @Column() name: string; @Column() age: number; @ManyToOne(() => Role) role?: Role } メイン index.ts createConnection().then(async connection => { const [programmer, boxer] = await getRepository(Role).save([ { name: 'programmer' }, { name: 'boxer' } ]) await getRepository(User).save([ { name: 'alice', age: 18, role: programmer }, { name: 'bob', age: 55, role: boxer }, { name: 'carol', age: 30, role: programmer } ]) }) }).catch(error => console.log(error)) データベース内の状態 role id name 1 programmer 2 boxer user id name age roleId 1 alice 18 1 2 bob 55 2 3 carol 30 1 他の方法 クエリビルダを使う 表現力が高くSQLライクなのはいいですが、冗長です。 const teenProgrammers = await getRepository(User) .createQueryBuilder("user") .innerJoinAndSelect("user.role", "role") .where("role.id = :roleId", { roleId: programmer.id }) .andWhere("age <= 19") .getMany(); できない例 SQLも問題なく生成されますが、パラメータにnullが渡されてしまうのでダメです。 const teenProgrammers = await getRepository(User).find({ relations: ["role"], where: { age: LessThanOrEqual(19), "role.id": programmer.id, }, }); 発行されるSQL(抜粋) LEFT JOIN "role" "User__role" ON "User__role"."id"="User"."roleId" WHERE "User"."age" <= ? AND "User"."roleId" = ? -- PARAMETERS: [19,null] ID以外をQBを使わずに指定するとダメ この場合も発行されるSQLと渡されるパラメータは上記と同じでroleId = nullになります。 const teenProgrammers = await getRepository(User).find({ relations: ["role"], where: { age: LessThanOrEqual(19), role: { name: "programmer", }, }, }); QBとrelationsを併用すると微妙 role.nameと指定することはできません。 const teenProgrammers = await getRepository(User).find({ relations: ["role"], where: (qb: SelectQueryBuilder<User>) => { qb.where({ age: LessThanOrEqual(19) }).andWhere( "role.name = :roleName", { roleName: "programmer" } ); }, }); FROM "user" "User" LEFT JOIN "role" "User__role" ON "User__role"."id"="User"."roleId" WHERE "User"."age" <= ? AND role.name = ? -- PARAMETERS: [19 "programmer"] User__role.nameと書けば冒頭のコードと同様に動作しますが、内部的に自動生成された(されるはずの)文字列を指定するのは微妙な気がします。 const teenProgrammers = await getRepository(User).find({ relations: ["role"], where: (qb: SelectQueryBuilder<User>) => { qb.where({ age: LessThanOrEqual(19) }).andWhere( "User__role.name = :roleName", { roleName: "programmer" } ); }, }); 参考 Finding entity with with relation condition #4396
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Postmanではform-dataのリクエストが通るのにaxiosで失敗する場合

はじめに Postmanだとリクエスト通るのに、axiosだとうまくいかなかった(404が返ってくる)ので調べました。 結論 ここに書いてあるとおりですが、headerを下記のようにするとうまく通りました。 const formData = new FormData(); const headers = { ...formData.getHeaders(), "Content-Length": formData.getLengthSync() }; getHeadersだけではContent-Lengthが入ってないようですね。 なくても良いような気がするんですが、送信先で必須とかになってるのかもしれません。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

TypeORMでSQLiteを指定してQueryFailedError: SQLITE_CONSTRAINT: NOT NULL constraint failed エラーが出たときの対処法

解決方法 解決策は4つあります。4つ目が本命です。 1. synchronizeをやめる これでエラーはなくなるけど、エンティティが同期されなくなるので不便。。。 ormconfig.json { "synchronize": true } 2. カラムをnullableにする これも根本的な解決じゃない。。。 entity/user.ts @Column({ nullable: true }) name: string; 3. SQLiteを使わない 身も蓋もないですが、ありだと思います。 4. トランスパイルする 私の環境では、ts-nodeを使わないようにすると、エラーが解決されました。 以下の手順を行ってください。 STEP 1 tsconfig.jsonのCompilerOptionsに"skipLibCheck": trueを指定する。 これは、STEP3のtscコマンド実行時にエラーを表示させないためです。 基本的に推奨されるオプションではないので、エラーが出なかったら消してください。 tsconfig.json { "compilerOptions": { "lib": [ "es5", "es6" ], "module": "commonjs", "moduleResolution": "node", "outDir": "./build", "emitDecoratorMetadata": true, "experimentalDecorators": true, "sourceMap": true, "skipLibCheck": true } } STEP 2 ormconfig.jsonを以下のように書き換えます。 これは、トランスパイルしたJavaScriptを実行するためにパスを変更しています。 /src → /build .ts → .js ormconfig.json { "type": "sqlite", "database": "database.sqlite", "synchronize": true, "logging": false, "entities": [ "build/entity/**/*.js" ], "migrations": [ "build/migration/**/*.js" ], "subscribers": [ "build/subscriber/**/*.js" ], "cli": { "entitiesDir": "build/entity", "migrationsDir": "build/migration", "subscribersDir": "build/subscriber" } } STEP 3 tscでトランスパイルします。 $ tsc すると、buildディレクトリに.jsファイルがたくさん書き出されるはずです。 実行できないときは、typescriptをグローバルにインストールしてください。 $ npm i -g typescript STEP 4 nodeでトランスパイルしたJavaScriptファイルを実行します。 $ node build/index.js これでエラーが出なければOKです。 STEP 5 毎回コマンドを打ち込むのは大変なので、package.jsonに書き加えてスクリプトにします。 package.json { ... "scripts": { "start": "tsc && node build/index.js", } } 最初は実行できたけど、後でエラーになった場合 自分は遭遇してませんが、前の実行で出力したJavaScriptファイルが残っていると、エラーになる場合があるようです。その場合、以下のようなコマンドで実行前にbuildディレクトリを削除してください。 $ rm -r build && tsc && node dist/index.js 参考: [typeORM] NOT NULL constraint Failedなどのsynchronize回りのエラーの解決方法 発生したエラーの詳細 $ ts-node src/index.ts QueryFailedError: SQLITE_CONSTRAINT: NOT NULL constraint failed: temporary_user.name at new QueryFailedError (/Users/src/error/QueryFailedError.ts:11:9) at Statement.handler (/Users/src/driver/sqlite/SqliteQueryRunner.ts:79:26) { errno: 19, code: 'SQLITE_CONSTRAINT', query: 'INSERT INTO "temporary_user"("id") SELECT "id" FROM "user"', parameters: [] }
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Node.js glitchやreplitなどでエラー回避術

glitchやreplitなどでエラー回避術 今回はサーバーでエラーが出た時node.jsが止まる問題を回避する方法を書きます ズバリこうです TerminalやconsoleやCmdに打つ yarn global add nodemon yarn add nodemon --dev npx nodemon これはなんなんだということを説明すると まずグローバル環境にnodemon を入れます その次にnpxで起動します node startでは有りません npx はローカルインストールしたコマンドを実行するために使う
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Node.js glitchやreplitなどでのエラー回避術

glitchやreplitなどでのエラー回避術 今回はサーバーでエラーが出た時node.jsが止まる問題を回避する方法を書きます ズバリこうです TerminalやconsoleやCmdに打つ yarn global add nodemon yarn add nodemon --dev npx nodemon これはなんなんだということを説明すると まずグローバル環境にnodemon を入れます その次にnpxで起動します node startでは有りません npx はローカルインストールしたコマンドを実行するために使う
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

TypeORMのMigrationでCannot use import statement outside a moduleエラーの対処法

TL;DR あまり綺麗ではないですが、これで解決しました。 typeormコマンドを使用せず、直接ts-nodeでtypeormを呼び出します。 $ ts-node ./node_modules/typeorm/cli.js migration:generate -n 'User' NestJSを使用しているときにこのエラーに遭遇した場合は、ページ下部のormconfig.jsonの書き換えを試してみてください。 よくみたらドキュメントに書いてあった... TypeORM > If entities files are in typescript ドキュメントの推奨している方法だと、、、 STEP 1 ts-nodeをグローバルにインストール $ npm install -g ts-node STEP 2 package.jsonにスクリプトを書き加える "scripts": { ... "typeorm": "node --require ts-node/register ./node_modules/typeorm/cli.js" } STEP 3 そしたら、こんな感じで実行する --は必要なので注意! $ npm run typeorm migration:generate -- -n UserMigration 遭遇したエラー内容 $ npx typeorm init $ npm install $ typeorm migration:generate -n UserMigration Error during migration generation: /Users/michinosuke/archive/web-app/typeorm-playground/src/entity/role.ts:1 import { Entity, PrimaryGeneratedColumn, Column, OneToMany } from 'typeorm' ^^^^^^ SyntaxError: Cannot use import statement outside a module at wrapSafe (internal/modules/cjs/loader.js:984:16) at Module._compile (internal/modules/cjs/loader.js:1032:27) at Object.Module._extensions..js (internal/modules/cjs/loader.js:1097:10) at Module.load (internal/modules/cjs/loader.js:933:32) at Function.Module._load (internal/modules/cjs/loader.js:774:14) at Module.require (internal/modules/cjs/loader.js:957:19) at require (internal/modules/cjs/helpers.js:88:18) at /usr/local/lib/node_modules/typeorm/util/DirectoryExportedClassesLoader.js:42:39 at Array.map (<anonymous>) at Object.importClassesFromDirectories (/usr/local/lib/node_modules/typeorm/util/DirectoryExportedClassesLoader.js:42:10) 上のやつで解決しないとき 一応、他の可能性も考えてみます。 ts-nodeも直接パスを指定する ts-nodeがグローバルにインストールされていない場合は、これで解決するかも。 $ ./node_modules/.bin/ts-node ./node_modules/typeorm/cli.js migration:generate -n 'User' 環境変数をつける TS_NODE_PROJECTでtsconfig.jsonのパスを渡します。 TS_NODE_TRANSPILE_ONLYにtrueを指定して、TypeScriptのtranspileModuleを使ってみる。 transpileModuleについての参考 ts-node Typechecking TypeScriptコンパイラとそのAPI TS_NODE_PROJECT=tsconfig.json TS_NODE_TRANSPILE_ONLY=true ./node_modules/.bin/ts-node ./node_modules/typeorm/cli.js migration:generate -n 'User' tsconfig.jsonを書き換える compilerOptionsのmoduleがcommonjs以外になっているときは、commonjsに変更してみてください。 tsconfig.json { "compilerOptions": { "module": "commonjs" } } さらに他の解決法 一旦TypeScriptで.tsファイルを.jsにトランスパイルしちゃう方法です。綺麗な解決策ではありませんが、ほぼ確実に解決できると思います。 もしあなたがNestJSなどを使用してこのエラーに遭遇している場合、下の手順3のormconfig.jsonの書き換えだけで解決する可能性が高いです。 手順1 tsconfig.jsonのcompilerOptionsのskipLibCheckにtrueを指定します。 あまりいい方法じゃないんですが、これを指定しないとトランスパイルにエラーが大量に出ます。 参考: ERROR TS1086: An accessor cannot be declared in an ambient context { "compilerOptions": { "skipLibCheck": true } } 手順2 トランスパイルします。 $ tsc 上のコマンドが打てなかったら、TypeScriptをグローバルインストールしてください。 $ npm i -g typescript 実行できたら、プロジェクトルートにbuildディレクトリが作成されて、jsファイルがたくさん入ってるはずです。 手順3 ormconfig.jsonを以下のように書き換えます。 * src/ → build/ * .ts → .js Before { "type": "sqlite", "database": "database.sqlite", "synchronize": true, "logging": false, "entities": [ "src/entity/**/*.ts" ], "migrations": [ "src/migration/**/*.ts" ], "subscribers": [ "src/subscriber/**/*.ts" ], "cli": { "entitiesDir": "src/entity", "migrationsDir": "src/migration", "subscribersDir": "src/subscriber" } } After { "type": "sqlite", "database": "database.sqlite", "synchronize": true, "logging": false, "entities": [ "build/entity/**/*.js" ], "migrations": [ "build/migration/**/*.js" ], "subscribers": [ "build/subscriber/**/*.js" ], "cli": { "entitiesDir": "build/entity", "migrationsDir": "build/migration", "subscribersDir": "build/subscriber" } } 手順4 nodeでtypeormを呼び出します。 $ node ./node_modules/typeorm/cli.js migration:generate -n 'User' 参考 migration:generate throws a SyntaxError with clean project template #5087 同じようなissue一覧
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

共通のtypescriptの型をフロントエンドとバックエンドで使う

はじめに フロントエンドとバックエンドでtypescriptを使っていると、リクエストやレスポンスの型を共有したくなります。 別々で型を管理すると、面倒なコピペをするハメになったり、ミスって不整合が起きたりするので大変ですよね。 そこで、一箇所に型を置いて、一回の変更でどちらにも変更が適用されるようにします。 1. プロジェクトをモノレポにする 型情報が即時に反映されるように、プロジェクトを一つにまとめます。 ディレクトリ構成の例 /api /common/requests /web 2. 型だけを定義したパッケージを作る 型パッケージは最低限package.jsonとindex.d.tsがあれば大丈夫です。 /common/requests/package.json { "name": "@types/your-requests", "version": "1.0.0", "description": "", "main": "", "types": "index.d.ts", "author": "", "license": "UNLICENSED" } /common/requests/index.d.ts export type SampleRequestBody = {id: string; amount: number}; 3. 型へのシンボリックリンクを張る 普通のローカルパッケージのインストールではなく、シンボリックリンクを使います。 これだと型情報を変更するたびにインストールをしなくてもよくなります。 yarnを使った例 cd api yarn add link:../common/requests cd ../web yarn add link:../common/requests これで、型情報の編集がそれぞれのパッケージに伝わるようになります。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む