20210910のNode.jsに関する記事は5件です。

CodeceptJSとPuppeteerでE2Eテスト(データ駆動、並列実行)

初めに CodeceptJSとPuppeteerを使ってE2Eテストを書いてみました。 コードの書き方も独特ですが便利な機能が多いと思います。 途中詰まってしまった部分もあり、それらを含めて備忘録的にまとめていきます。 内容に間違いがあればご指摘ください。 目次 CodeceptJSとは 準備 テストコードを書く コード詳細 I.amOnPage I.fillField I.pressKey I.wait I.grabTextFrom I.see 躓きポイント スクリーンショットで記録 データ駆動してみる 並列実行してみる テストレポートを見る 終わり 参考 CodeceptJSとは まずCodeceptJSとはなんなのか。 最後に記載しました参考サイトにわかりやすく書いてありました。 ざっくりいうとCodeceptJSはNode製のE2Eテストフレームワークで、 いろいろなブラウザ操作ライブラリと組み合わせることでブラウザテストを実行します。 ここでおお、と思ったのは CodeceptJSの独特な書き方を使えばWebdriverIO(Selenium)、Appium、Nightmare、Puppeteerなどのいろんなツールでコードを使いまわすこともできる、ということです。 今回は複数あるツールの中で、手軽で実行速度も速いというPuppeteerを使ってテストをしてみました。 準備 ・Node.js v8.9以上 ・npm         が必要です。 あとはCodeceptJSとPuppeteerをインストールして、 初期設定でいろいろ聞かれるのでそのときに使いたいツールを選択します。 npm install codeceptjs puppeteer --save-dev npx codeceptjs init 設定の細かな部分はこちらのサイトが分かりやすかったです テストコードを書く 初期設定でテストファイルの記載も設定していると思います(デフォルトは./*_test.js) その書き方でファイルを作成し、コードを書いていきます。 ブラウザを立ち上げ、検索欄に入力して表示内容を確認するテストから作成していきます。 ↓↓こんな感じのコードになりました。 Feature('はじめてのCodeceptJS').retry(1); Scenario('Googleで検索、表示内容の確認', async({ I }) => { I.amOnPage('https://www.google.com'); I.fillField('q', 'CodeceptJS'); I.pressKey('Enter'); I.wait(1); let firstResult = await I.grabTextFrom('h3'); I.click(firstResult); I.fillField('#algolia-search-input', 'puppetter'); I.pressKey('Enter'); I.see('Now you can use Puppeteer for Firefox (min version: Firefox/63.0.4)'); }); 動かすときは--stepsをつけると細かく結果がでてくれます。 ファイル名を付ければそのファイルのみの実行も可能です。 npx codeceptjs run --steps コード詳細 Feature('はじめてのCodeceptJS').retry(1) .retry(1) という記載をしています。 これは全てのシナリオに対して失敗した場合は1回までリトライをしてくれる機能です。 1度失敗したテストもリトライして成功すればそれは成功とみなされます。 I.amOnPage('https://www.google.com'); I.fillField('q', 'CodeceptJS'); I.pressKey('Enter'); I.wait(1); let firstResult = await I.grabTextFrom('h3'); 上記コードの説明をしていきます。 ↓↓詳細は公式にわかりやすく書いてますのでそちらをご参照ください。 I.amOnPage ex) I.amOnPage('開きたいページ') URLがプロトコル(http://またはhttps://)で始まらない場合は相対URLと見なされます I.fillField ex) I.fillField('入力したい場所', '入力内容') 入力場所はラベル、name、CSS、XPathで指定できます。 ラベルでも指定ができるのでどこに入力したいのかが分かりやすいです。 複数ヒットしてしまい、うまく探せないときはwithin()で絞り込む必要がありますがここでは詳細を省きます。 I.pressKey ex) I.pressKey('Enter') 'Alt'、'Control'、'Shift'などいろいろ対応してます。 I.wait ex) I.wait(1) 待機したい秒数を書きます。 I.grabTextFrom ex) I.grabTextFrom('h3') CSSまたはXPathによって配置された要素からテキストを取得し、テストに返します。 テストの実行を再開するためにasync/awaitが必要です。 複数の要素が見つかった場合、最初の要素を返します。 I.see ex) I.see('確認したいテキスト') ページに表示されているテキストが含まれていることを確認します。 コンテキストパラメータを使用して検索を絞り込むこともできます。 ex) I.see('確認したいテキスト', '.content'); 躓きポイント 私は2点で引っ掛かりました。1つは↓の記載です。 Scenario('Googleで検索、表示内容の確認', async({ I }) => { 今回CodeceptJSのバージョン3を使用しているのですが、参考にしたサイトのバージョンが古いことに気が付かず async( I ) => { 上記で記載していたところエラー この書き方のバージョン古いですよ、みたいな内容のエラー文が出たので探したところこれが原因でした。 もう一つはgrabTextFromの使い方です。 async/awaitが必要になるところを書かずに動かそうとしてエラー 調べたら思いっきりサイトに書いてありました。反省。 スクリーンショットで記録 CodeceptJSでは動作ごとにスクリーンショットをとる設定ができます。 //codecept.conf.js plugins: { stepByStepReport: { enabled: true, deleteSuccessful: false,  } } 上記の追記だけでできるので簡単で有難いです。 任意のタイミングでのみ取りたいときにはsaveScreenshotでとることができます。 データ駆動してみる キーワードを入れて検索した結果を確認するテストです。 testWordsにそれぞれ検索ワードと確認したい内容をいれてます。 const testWords = [ { 'searchWord': 'puppetter', 'responseWord': 'Now you can use Puppeteer for Firefox (min version: Firefox/63.0.4)' }, { 'searchWord': 'selenium', 'responseWord': 'Testing with WebDriver' }, ] Data(testWords).Scenario('データ駆動テスト', async({I, current}) => { I.amOnPage('https://www.google.co.jp/') I.fillField('q', 'CodeceptJS'); I.pressKey('Enter'); I.wait(1); let firstResult =await I.grabTextFrom('h3'); I.click(firstResult); I.fillField('#algolia-search-input', current['searchWord']); I.pressKey('Enter'); I.see(current['responseWord']); }) そのtestWordsをData().Scenarioに入れてあげるとデータの分繰り返しテストします。 Data(testWords).Scenario('データ駆動テスト', async({I, current}) => { ここでcurrentにtestWordsのデータが入るようです。 今回私はcurrent['responseWord']、の書き方で書いてますが 公式を見た感じはcurrent.login、のように書いてます(なぜかうまくいかなかった) 二つしかデータがないのであれですが↓のように 書いた方が良かった気もしています。 //サイトから抜粋 let accounts = new DataTable(['login', 'password']); // accounts.add(['davert', '123456']); // adding records to a table accounts.add(['admin', '123456']); 並列実行してみる CodeceptJSは並列実行も簡単でした。 //codecept.conf.js multiple: { parallel: { chunks: 3 } } npx codeceptjs run-workers 3 codecept.conf.jsに追記して上記で実行するとブラウザが三つ立ち上がって処理が進みます。 テストレポートを見る CodeceptJSにプラグインをインストールすることでこんな感じのレポートを出力することができます。 npm install -g allure-commandline --save-dev //codecept.conf.js "plugins": { "allure": {} } npx codeceptjs run --plugins allure allure serve output 方法はこれまた公式に書いてある通りです。とりあえず公式サイトを調べれば何とかなるのでは。 インストール、codecept.conf.jsに追記、プラグインを有効にしてテスト実行、Allureサーバーを起動、の流れです。 テストを実行した際にできたレポートがoutputディレクトリ(デフォルトの場合)に保存されます。 終わり 便利な機能が多く、設定も簡単でした。 今回触っていないプラグインにも便利そうなものがまだまだあり、 ・Cookieをファイルに保存し、セッションを再利用できるプラグインでログイン処理を省略 ・実行されたテストごとにビデオを録画および保存(※Dockerが必要) なんてことも可能なようです。 Jenkinsとの連携もできるようですが、私には少し解読が必要そうです。 触っていて楽しかったでのもう少し深堀していきたいと思ってます。 間違いなど何かあればご教示ください。 参考
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Node.js 使用流れ

Node.jsの学習の備忘録と振り返りの記事です。 何かの参考になれば幸いです。 付け足しや訂正などある場合ご教授いただけると 大変嬉しく思います!! Node.js 手順 1 npmインストール(パッケージ管理システム) 2 パッケージインストールnpm install パッケージ名 3 パッケージ読み込みconst パッケージ名 = require("パッケージ名");     &   使用準備const app = パッケージ名(); 4 サーバー起動app.listen(localhost); 5 ファイル実行ターミナルで『node ファイル名』 ルーティング作成(URLに対応する処理) node.js app.get('/URL',(req,res) => { 処理内容 }); 見た目を表示する処理 ルーティング処理の中でres.render('表示したいファイル名');とかくと指定したビューファイルをブラウザに表示できる node.js //app.get('/URL',(req,res) => { res.render('表示したいファイル'); //}); 画面作成簡易手順 1 ejsに対応するルーティング&処理していく 2 ejsファイル作り 3 ファイル内のコード作成 CSS適用 1 読み込むフォルダの指定 ルーティングファイル内のパッケージ読み込み下あたりにCSSを読み込むコードを書く app.use(express.static('読み込みたいフォルダ名')); //const express = require('express'); //const app = express(); app.use(express.static('読み込みたいフォルダ名')); //app.get('/URL',(req,res) => { //処理内容 //}); 2 CSSのパス指定 EJSファイル内の<head>の中の<link>タグ内にCSSのパスを指定し読み込む href = "/フォルダ名/ファイル名" <!-- <!DOCTYPE html> --> <!-- <html> --> <!-- <head> --> <!-- <meta charset="utf-8"> --> <!-- <title>LIST APP</title> --> <link rel="stylesheet" href="/フォルダ名/ファイル名"> <!-- </head> --> 画像の読み込み EJSファイル内の<img>タグ内にCSSのパスを指定し読み込む img src = "/フォルダ名/ファイル名" css簡易適用手順 1 CSSを置くフォルダの作成 2 ルーティングファイル内で読み込みたいフォルダの指定 3 EJS内で使用したいCSSパスを指定し読み込む(画像の読み込みも同様) 4 CSSファイルの作成 5 コード書く ページ間リンクボタン作成 1 ルーティング内でそれぞれファイルのURLを設定 2 EJS内でボタンクラス内のリンクタグに遷移したいファイルのURLを指定
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

yarnの処理でEACCESエラーが出たときの対処法

参考 エラー内容 EACCESのpermission deniedエラーが出ました。 FATAL EACCES: permission denied, mkdir '/home/xx/Documents/xxx-toC/frontend/node_modules/.cache/nuxt' ╭──────────────────────────────────────────────────────────────────────────────╮│ ││ ✖ Nuxt Fatal Error ││ ││ Error: EACCES: permission denied, mkdir ││ '/home/xx/Documents/xxx-toC/frontend/node_modules/.cache/nuxt' ││ │╰──────────────────────────────────────────────────────────────────────────────╯ error Command failed with exit code 1. 対処法 node modules のcacheへの権限がなさそうなので、そこに権限を与えました。 sudo chmod 777 node_modules/.cache/
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【TypeScript】jestでioredis-mockを正しくインポートする

はじめに ioredisはNode.jsにおける主要なRedisクライアントパッケージの一つです。 単体テスト等で、Redisサーバーに実際にアクセスすることなく実行するためにioredisをモック化したい場合には、ioredis-mockを使うことができます。 TypeScriptでは単体テストフレームワークとしてjestを使用することが多いと思いますが、jestでioredis-mockを使用する際には、そのインポート方法を誤ると実行時にエラーが発生してしまします。 私はこれに引っかかってしまったので、本記事では、TypeScript+jestを使用している環境において、ioredisの型情報を利用できる形でioredis-mockをインポートする際の、正しい方法と誤った方法、そのエラー内容についてまとめています。 検証環境 Node.js: 14.17.0 TypeScript: 4.3.5 ioredis: 4.27.7 ioredis-mock: 5.6.0 jest: 27.0.6 正しい方法 ioredis-mock公式リポジトリのREADME1に記載されている方法です。 ちゃんとREADMEを読んで素直にこの方法に従わなかった自分を恨みます。 import Redis from 'ioredis'; jest.mock('ioredis', () => require('ioredis-mock/jest')); 誤った方法1 公式のGitHubにある型定義の利用についてのIssueに書かれている方法2です。 jestで使うと後述する特定の状況でエラーが発生します。 import Redis from 'ioredis'; const RedisMock: typeof Redis = require('ioredis-mock'); jest.mock('ioredis'); エラーの内容は以下の通りです。 TypeError: Cannot read property 'map' of undefined このエラーは特定のコマンド実行時に発生します。全て調べたわけではないですが、hset等のHash系のコマンドで発生することが多いです。 例えば、hsetコマンドを利用した以下のコードで発生します。 const redis = new RedisMock(); await redis.hset('aa', 'bb', 7); 誤った方法2 GitHubのIssueに書かれている方法2と公式リポジトリのREADME1に記載されている方法を組み合わせてしまったものです。 実行時に上記コードにあるインポート部分でエラーになります。 import Redis from 'ioredis'; const RedisMock: typeof Redis = require('ioredis-mock'); jest.mock('ioredis', () => require('ioredis-mock/jest')); エラーの内容は以下の通りです。 TypeError: Cannot read property '_transformer' of undefined ioredis-mockのjestでのセットアップ(公式リポジトリ) ↩ Are typings available? ↩
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

node.js実践編(メッセージボード編)

※node.js超入門ノートの続きになります。 モデルの実装 以下のコマンドを実行します。 npx sequelize-cli model:generate --name Board --attributes userId:integer, message:string アソシエーションの設定 作成したファイルを修正します。 models/board.js // 従モデル 'use strict'; module.exports = (sequelize, DataTypes) => { const Board = sequelize.define('Board', { userId: { type: DataTypes.INTEGER, validate: { notEmpty: { msg: "利用者は必須です。" } } }, message: { type: DataTypes.STRING, validate: { notEmpty: { msg: "メッセージは必須です。" } } } }, {}); Board.associate = function(models) { Board.belongsTo(models.User); // 従モデル }; return Board; }; 以下も修正します。 models/user.js User.associate = function(models) { User.hasMany(models.Board); // 主モデル }; 以下のコマンドでマイグレーションを行います。 npx sequelize-cli db:migrate ログイン処理 ログイン処理を追記します。 routes/users.js router.get('/login', (req, res, next) => { var data = { title:'Users/Login', content: '名前とパスワードを入力してください。' } res.render('users/login', data); }); router.post('/login', (req, res, next) => { db.User.findOne({ where:{ name: req.body.name, pass:req.body.pass, } }).then(usr => { if (usr != null) { req.session.login = usr; let back = req.session.back; if (back == null){ back = '/'; } res.redirect(back); } else { var data = { title: 'Users/Login', content:'名前かパスワードに問題があります。再度入力して下さい。' } res.render('users/login', data); } }) }); 以下のファイルを作成します。 routes/boards.js const express = require('express'); const router = express.Router(); const db = require('../models/index'); const { Op } = require("sequelize"); const { route } = require('./users'); const pnum = 10; // ログインのチェック function check(req, res) { if (req.session.login == null) { // ログイン後に戻る値 req.session.back = '/boards'; res.redirect('/users/login'); return true; } else { return false; } } // トップページ router.get('/', (req, res, next) => { res.redirect('/boards/0'); }); router.get('/:page',(req, res, next) => { if (check(req, res)){ return }; const pg = req.params.page * 1; db.Board.findAll({ offset: pg * pnum, limit: pnum, order: [ ['createdAt', 'DESC'] ], include: [{ model: db.User, required: true }] }).then(brds => { var data = { title: 'Boards', login: req.session.login, content: brds, page: pg } res.render('boards/index', data); }); }); // メッセージフォームの送信処理 router.post('/add', (req, res, next) => { if (check(req, res)){ return }; db.sequelize.sync() .then(() => db.Board.create({ userId: req.session.login.id, message: req.body.msg }) .then(brd => { res.redirect('/boards'); }) .catch((err) => { res.redirect('/boards'); }) ) }); // 利用者のホーム router.get('/home/:user/:id/:page', (req,res, next) => { if (check(req, res)){ return }; const id = req.params.id * 1; const pg = req.params.page * 1; db.Board.findAll({ where: {userId: id}, offset: pg * pnum, limit: pnum, order: [ ['createdAt', 'DESC'] ], include: [{ model: db.User, required: true }] }).then(brds => { var data = { title: 'Boards', login: req.session.login, userId: id, userName: req.params.user, content: brds, page: pg } res.render('boards/home', data); }); }); module.exports = router; app.jsに組み込みます。 app.js var boardsRouter = require('./routes/boards'); app.use('/boards', boardsRouter); テンプレート作成 以下のファイルを作成します。 views/users/login.ejs <!DOCTYPE html> <html lang="ja"> <head> <meta charset="UTF-8"> <meta http-equiv="content-type" content="text/html"> <title><%= title %></title> <link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.4.1/css/bootstrap.min.css" crossorigin="anonymous"> <link rel="stylesheet" href="/stylesheets/style.css" /> </head> <body class="container"> <header> <h1 class="display-4"> <%= title %> </h1> </header> <div role="main"> <p><%- content %></p> <form action="/users/login" method="post"> <div class="form-group"> <label for="name">NAME</label> <input type="text" name="name" id="name" class="form-control"> </div> <div class="form-group"> <label for="pass">PASSWORD</label> <input type="text" name="pass" id="pass" class="form-control"> </div> <input type="submit" value="ログイン" class="btn btn-primary"> </form> <p class="mt-4"><a href="/boards">&lt;&lt; Top へ戻る</a> <a href="/users/add">アカウントの作成&gt;&gt;</a></p> </div> </body> </html> views/boards/index.ejs <!DOCTYPE html> <html lang="ja"> <head> <meta charset="UTF-8"> <meta http-equiv="content-type" content="text/html"> <title><%= title %></title> <link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.4.1/css/bootstrap.min.css" crossorigin="anonymous"> <link rel="stylesheet" href="/stylesheets/style.css" /> </head> <body class="container"> <header> <h1 class="display-4"> <%= title %> </h1> </header> <div role="main"> <p class="h4">Wlcome to <%= login.name %>.</p> <form action="/boards/add" method="POST"> <div class="row"> <div class="col-10"> <input type="text" name="msg" class="form-control"> </div> <input type="submit" value="送信" class="btn btn-primary col-2"> </div> </form> <table class="table mt-5"> <% for(let i in content) { %> <%- include('data_item', {val: content[i]}) %> <% } %> </table> <ul class="pagination justify-content-center"> <li class="page-item"> <a href="/boards/<%= page - 1 %>" class="page-link">&lt;&lt; prev</a> </li> <li class="page-item"> <a href="/boards/<%= page + 1 %>" class="page-link">Next &gt;&gt;</a> </li> </ul> </div> </body> </html> views/boards/home.ejs <!DOCTYPE html> <html lang="ja"> <head> <meta charset="UTF-8"> <meta http-equiv="content-type" content="text/html"> <title><%= title %></title> <link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.4.1/css/bootstrap.min.css" crossorigin="anonymous"> <link rel="stylesheet" href="/stylesheets/style.css" /> </head> <body class="container"> <header> <h1 class="display-4"> <%= title %> </h1> </header> <div role="main"> <p class="h4"><%= userName %>'s messages.</p> <table class="table mt-5"> <% for(let i in content) { %> <%- include('data_item', {val: content[i]}) %> <% } %> </table> <ul class="pagination justify-content-center"> <li class="page-item"> <a href="/boards/home/<%= userName %>/<%= userId %>/<%= page - 1 %>" class="page-link">&lt;&lt; prev</a> </li> <li class="page-item"> <a href="/boards/home/<%= userName %>/<%= userId %>/<%= page + 1 %>" class="page-link">Next &gt;&gt;</a> </li> </ul> </div> <div class="text-left"> <a href="/boards">&lt;&lt; Top.</a> </div> </body> </html> views/boards/data_item.ejs <% if (val != null) { %> <tr class = "row"> <th class="col-2"> <a class="text-dark" href="/boards/home/<%=val.User.name %>/<%= val.userId %>/0"> <%= val.User.name %> </a> </th> <td class="col-7"><%= val.message %></td> <% var d = new Date(val.createdAt); var dstr = d.getFullYear() + '-' + (d.getMonth() + 1) + '-' + d.getDate() + ' ' + d.getHours() + ':' + d.getMinutes() + ':' + d.getSeconds(); %> <td class="col-3"><%= dstr %></td> </tr> <% } %> 結果 ログイン画面 メッセージボード画面 ユーザーのホーム画面
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む