著者:しょっさん
ソフトウエアを正しく作るために、エンジニアたちはどんなことを知らなければならないでしょうか。実際のコードを使って、より良くしていくためのステップを考えてみましょう。第3回は、前回のプロジェクトの計画や方針に基づいて2回の開発サイクル(イテレーション)を回します。
シェルスクリプトマガジン Vol.63は以下のリンク先でご購入できます。![]()
![]()
図4 TypeScriptに書き換えたメインのプログラム(index.ts)
import Express from 'express';
const app = Express();
import { Expense } from './models/expense';
import bodyParser from 'body-parser';
import cookieParser from 'cookie-parser';
import session from 'express-session';
const users = {
'user01': 'p@ssw0rd',
'user02': 'ewiojfsad'
};
app.use(cookieParser());
app.use(bodyParser.urlencoded({ extended: false }));
app.use(session({
secret: 'secret',
resave: false,
saveUninitialized: false,
cookie: {
maxAge: 24 * 30 * 60 * 1000
}
}));
app.get('/login', (req: Express.Request, res: Express.Response): void => {
res.send('<h1>LOGIN</h1><form action="/login" method="post">ユーザーID:<input type="text" name="user" size="40"><br />パスワード<input type="password" name="password"><input type="submit" value="ログイン">');
});
app.post('/login', (req: Express.Request, res: Express.Response): void => {
if (eval("users." + req.body.user) === req.body.password) {
if (req.session) {
req.session.user = req.body.user;
}
}
res.redirect('/');
});
app.post('/expense', (req: Express.Request, res: Express.Response): void => {
Expense.create(req.body)
.then(() => {
res.redirect('/');
});
});
app.get('/', (req: Express.Request, res: Express.Response): void => {
const user = req!.session!.user || '名無しの権兵衛';
res.writeHead(200, { "Content-Type": "text/html" });
res.write(<h1>Hello ${user}</h1><table><tr><th>ID</th><th>申請者名</th><th>日付</th><th>経費タイプ</th><th>経費詳細</th><th>金額</th></tr>);
Expense.findAll()
.then(results => {
for (let i in results) {
res.write(<tr><td>${results[i].id}</td><td>${results[i].user_name}</td><td>${results[i].date}</td><td>${results[i].type}</td><td>${results[i].description}</td><td>${results[i].amount}</td></tr>);
}
res.write('</table><a href="/login">ログイン</a><a href="/submit">経費入力</a>');
res.end();
});
});
app.get('/submit', (req: Express.Request, res: Express.Response): void => {
const user = req!.session!.user || '名無しの権兵衛';
res.send(<h2>経費入力</h2><form action="/expense" method="post">申請者名:<input type="text" name="user_name" value="${user}"><br />日付:<input type="date" name="date"><br />経費タイプ:<input type="text" name="type"><br />経費詳細:<input type="text" name="description"><br />金額:<input type="number" name="amount"><br /><input type="submit" value="経費申請">);
});
const port = process.env.PORT || 3000;
app.listen(port, () => {
console.log(http://localhost:${port});
})
export default app;
図5 TypeScriptで作成した自作のモデルファイル(expense.ts)
import { Sequelize, Model, DataTypes } from 'sequelize';
// todo: データベース接続を定義する Typescript モジュール
const env = process.env.NODE_ENV || 'development';
const config = require(__dirname + '/../config/config.json')[env];
let sequelize;
if (config.use_env_variable) {
const config_url: any = process.env[config.use_env_variable];
sequelize = new Sequelize(config_url, config);
} else {
sequelize = new Sequelize(config.database, config.username, config.password, config);
}
class Expense extends Model {
public id!: number;
public user_name!: string;
public date!: Date;
public type!: string;
public description!: string | null;
public amount!: number;
public readonly careated_at!: Date;
public readonly updated_at!: Date;
}
Expense.init({
id: {
type: DataTypes.INTEGER.UNSIGNED,
autoIncrement: true,
allowNull: false,
primaryKey: true,
},
user_name: {
type: DataTypes.STRING(256),
allowNull: false,
defaultValue: ''
},
date: {
type: DataTypes.DATE,
allowNull: false
},
type: {
type: DataTypes.STRING(256),
allowNull: false
},
description: {
type: DataTypes.TEXT,
},
amount: {
type: DataTypes.INTEGER.UNSIGNED,
allowNull: false
}
}, {
tableName: 'expenses',
underscored: true,
sequelize: sequelize
});
export { Expense };
図6 トランスパイルオプションファイル(tsconfig.json)
{
"compilerOptions": {
"target": "es2018",
"module": "commonjs",
"lib": [
"es2018"
],
"sourceMap": true,
"outDir": "./dist",
"strict": true,
"moduleResolution": "node",
"esModuleInterop": true,
"experimentalDecorators": true,
"emitDecoratorMetadata": true
},
"include": [
"./src/**/*.ts"
],
"exclude": [
"node_modules",
"**/*.test.ts"
]
}
図7 gulpfile.jsファイルの内容
const { src, dest, parallel, series } = require('gulp');
const ts = require('gulp-typescript');
const tsconfig = require('./tsconfig.json');
// gulp固有の設定
const config = {
output: 'dist/',
json: {
source: 'src/**/*.json'
}
};
// typescript のトランスパイルオプション ← tsconfig.json を再利用する
const typescript = () => {
return src(tsconfig.include)
.pipe(ts(tsconfig.compilerOptions))
.pipe(dest(config.output));
};
// json ファイルのアウトプットディレクトリへのコピーを司る指令
const json = () => {
return src(config.json.source)
.pipe(dest(config.output));
};
// 実行時オプション
exports.typescript = typescript;
exports.default = series(parallel(typescript, json));
図8 jest.config.jsの内容
module.exports = {
coverageDirectory: "coverage",
preset: 'ts-jest',
testEnvironment: "node",
};
図9 index.test.tsの内容
test('1 adds 2 is equal 3', () => {
expect(1 + 2).toBe(3);
})
図11 super.test.tsファイルの内容
import app from '../src';
import request from 'supertest';
describe('Root', () => {
it('Root index is valid', async () => {
const response = await request(app).get("/");
expect(response.status).toBe(200);
});
});
describe('Login', () => {
const userCredentials = {
user: 'user01',
password: 'p@ssw0rd'
};
it('login form is varid', async () => {
const response = await request(app).get("/login");
expect(response.status).toBe(200);
});
it('login with user credential is valid', async () => {
const response = await request(app).post("/login")
.send(userCredentials);
expect(response.status).toBe(302);
})
});
describe('submit', () => {
it('A submit form is valid', async () => {
const response = await request(app).get("/submit");
expect(response.status).toBe(200);
});
});
図13 .circleci/config.ymlファイルの内容
# Javascript Node CircleCI 2.0 configuration file
#
# Check https://circleci.com/docs/2.0/language-javascript/ for more details
#
version: 2
jobs:
build:
docker:
# specify the version you desire here
- image: circleci/node:12.10.0
# Specify service dependencies here if necessary
# CircleCI maintains a library of pre-built images
# documented at https://circleci.com/docs/2.0/circleci-images/
# - image: circleci/mongo:3.4.4
working_directory: ~/repo
steps:
- checkout
# Download and cache dependencies
- restore_cache:
keys:
- v1-dependencies-{{ checksum "package.json" }}
# fallback to using the latest cache if no exact match is found
- v1-dependencies-
- run: npm install
- save_cache:
paths:
- node_modules
key: v1-dependencies-{{ checksum "package.json" }}
# npm migrate
- run: npm run migrate
# run tests!
- run: npm test
図14 書き換えたsuper.test.tsの内容
import app from '../src';
import request from 'supertest';
describe('Root', () => {
it('Root index is valid', async () => {
const response = await request(app).get("/");
expect(response.status).toBe(200);
});
});
describe('Login', () => {
const userCredentials = {
user: 'user01',
password: 'p@ssw0rd'
};
it('login form is varid', async () => {
const response = await request(app).get("/login");
expect(response.status).toBe(200);
});
it('login with user credential is valid', async () => {
const response = await request(app).post("/login")
.send(userCredentials);
expect(response.status).toBe(302);
})
});
describe('submit', () => {
it('A submit form is valid', async () => {
const response = await request(app).get("/expenses/submit");
expect(response.status).toBe(200);
});
});
describe('payment', () => {
it('A payment list is valid', async () => {
const response = await request(app).get("/expenses/payment");
expect(response.status).toBe(200);
});
});
// エラーテスト
describe('/expense', () => {
it('This URI is not valid', async () => {
const response = await request(app).get("/expense");
expect(response.status).toBe(404);
});
});
図15 index.tsファイルの内容
import Express from 'express';
const router = Express.Router();
// GET / 最初に開く画面
router.get('/', (req: Express.Request, res: Express.Response) => {
const user = req!.session!.user || '名無しの権兵衛';
res.send(<h1>Hello ${user}</h1><a href="/login">ログイン</a><br /><a href="/expenses/submit">経費入力</a><br /><a href="/expenses/payment">支払い処理</a>);
})
export default router;
図16 login.tsファイルの内容
import Express from 'express';
const router = Express.Router();
// ユーザー&パスワード
const users = {
'user01': 'p@ssw0rd',
'user02': 'ewiojfsad'
};
// GET /login ユーザーログインフォーム
router.get('/', (req: Express.Request, res: Express.Response): void => {
res.send('<h1>LOGIN</h1><form action="/login" method="post">ユーザーID:<input type="text" name="user" size="40"><br />パスワード<input type="password" name="password"><input type="submit" value="ログイン"><br /><a href="/login">ログイン</a><br /><a href="/expenses/submit">経費入力</a><br /><a href="/expenses/payment">支払い処理</a>');
});
// POST / ユーザーの認証処理
router.post('/', (req: Express.Request, res: Express.Response): void => {
if (eval("users." + req.body.user) === req.body.password) {
if (req.session) {
req.session.user = req.body.user;
}
}
res.redirect('/');
});
export default router;
図17 payment.tsファイルの内容
import Express from 'express';
const router = Express.Router();
import { Expense } from '../../models/expense';
// GET /expenses/payment もともと、最初に開かれる画面だった部分
router.get('/', (req: Express.Request, res: Express.Response): void => {
const user = req!.session!.user || '名無しの権兵衛';
res.writeHead(200, { "Content-Type": "text/html" });
res.write(<h1>Hello ${user}</h1><table><tr><th>ID</th><th>申請者名</th><th>日付</th><th>経費タイプ</th><th>経費詳細</th><th>金額</th></tr>);
Expense.findAll()
.then(results => {
for (let i in results) {
res.write(<tr><td>${results[i].id}</td><td>${results[i].user_name}</td><td>${results[i].date}</td><td>${results[i].type}</td><td>${results[i].description}</td><td>${results[i].amount}</td></tr>);
}
res.write('</table><a href="/login">ログイン</a><br /><a href="/expenses/submit">経費入力</a><br /><a href="/expenses/payment">支払い処理</a>');
res.end();
});
});
export default router;
図18 submit.tsファイルの内容
import Express from 'express';
const router = Express.Router();
import { Expense } from '../../models/expense';
// GET /expenses/submit 入力フォーム
router.post('/', (req: Express.Request, res: Express.Response): void => {
Expense.create(req.body)
.then(() => {
res.redirect('/');
});
});
// POST /expenses/submit 経費の申請
router.get('/', (req: Express.Request, res: Express.Response): void => {
const user = req!.session!.user || '名無しの権兵衛';
res.send(<h2>経費入力</h2><form action="/expenses/submit" method="post">申請者名:<input type="text" name="user_name" value="${user}"><br />日付:<input type="date" name="date"><br />経費タイプ:<input type="text" name="type"><br />経費詳細:<input type="text" name="description"><br />金額:<input type="number" name="amount"><br /><input type="submit" value="経費申請"><br /><a href="/login">ログイン</a><br /><a href="/expenses/submit">経費入力</a><br /><a href="/expenses/payment">支払い処理</a>);
});
export default router;
図20 分割後のindex.tsファイルの内容
import Express from 'express';
const app = Express();
import bodyParser from 'body-parser';
import cookieParser from 'cookie-parser';
import session from 'express-session';
app.use(cookieParser());
app.use(bodyParser.urlencoded({ extended: false }));
app.use(session({
secret: 'secret',
resave: false,
saveUninitialized: false,
cookie: {
maxAge: 24 * 30 * 60 * 1000
}
}));
// ルーティング
import index from './routes/index';
import login from './routes/login'; // ログイン機能
import payment from './routes/expenses/payment'; // 支払い機能
import submit from './routes/expenses/submit'; // 請求機能
app.use('/', index);
app.use('/login', login);
app.use('/expenses/payment', payment);
app.use('/expenses/submit', submit);
const port = process.env.PORT || 3000;
app.listen(port, () => {
console.log(http://localhost:${port});
})
export default app;
図21 package.jsonファイルの内容
{
"name": "webapp",
"version": "0.3.0",
"description": "This application is sample code of Shell Script Magazine",
"main": "dist/index.js",
"scripts": {
"dev": "ts-node ./src/index.ts",
"test": "jest",
"clean": "rimraf dist/*",
"migrate": "sequelize db:migrate",
"transpile": "gulp",
"build": "npm run clean && npm run migrate && npm run transpile",
"start": "node ."
},
"engines": {
"node": "12.10.0"
},
"author": "しょっさん",
"license": "ISC",
"dependencies": {
"cookie-parser": "^1.4.4",
"express": "^4.16.4",
"express-session": "^1.16.1",
"gulp": "^4.0.2",
"gulp-cli": "^2.2.0",
"gulp-typescript": "^5.0.1",
"pg": "^7.12.1",
"rimraf": "^3.0.0",
"sequelize": "^5.18.0",
"sequelize-cli": "^5.5.1"
},
"devDependencies": {
"@types/bluebird": "^3.5.27",
"@types/cookie-parser": "^1.4.2",
"@types/express": "^4.17.1",
"@types/express-session": "^1.15.14",
"@types/jest": "^24.0.18",
"@types/node": "^12.7.3",
"@types/pg": "^7.11.0",
"@types/supertest": "^2.0.8",
"@types/validator": "^10.11.3",
"jest": "^24.9.0",
"jest-junit": "^8.0.0",
"sqlite3": "^4.1.0",
"supertest": "^4.0.2",
"ts-jest": "^24.0.2",
"ts-loader": "^6.0.4",
"ts-node": "^8.3.0",
"typescript": "^3.6.2"
}
}