Published on

MongoDB testing

อย่างที่เคยบอกในบทความก่อนหน้าว่ามีโอกาสได้เขียนเทส เลยอยากบันทึกความรู้นี้เก็บไว้ วันนี้เจมส์จะมาเขียนสิ่งที่ได้เรียนรู้เกี่ยวกับการเขียน Test MongoDB ครับ

สิ่งที่จะทำวันนี้ เราทำแบบง่าย ๆ เหมือนเดิมเน้อครับ ก็คือเราจะสร้าง API ขึ้นมา 2 route คือ

  • GET /api/todos => ดึงค่าจาก MongoDB มาแสดง

  • POST /api/todo => เพิ่มข้อมูลสิ่งที่จะทำใส่ใน MongoDB

มาเริ่มกันเลยดีกว่าครับ

ขั้นแรกเดี๋ยวเรามาสร้าง mongodb จาก docker กันดีกว่าครับ แต่เพื่อความสะดวก เจมส์จะใช้ docker-compose.yaml ในการช่วยสร้าง (เจมส์เคยเขียนบทความไว้แล้วครับ แต่เป็น Blog อันเก่าครับ [MongoDB] docker-compose up ปุ๊บสร้าง mongo database ปั๊บ)

สร้าง mongo จาก Docker โดยใช้ docker-compose

ขั้นแรกเรามาสร้าง Project ขึ้นมาก่อนครับ โดยการพิมพ์คำสั่ง

mkdir mongodb-testing

cd mongodb-testing

จากนั้นสร้างไฟล์ docker-compose.yaml ก่อนครับ และใส่ข้อมูลดังนี้ครับ

docker-compose.yaml
version: "3.7"
services:
  database:
    image: mongo:6.0.2
    ports:
      - 27017:27017
    environment:
      - MONGO_INITDB_ROOT_USERNAME=root
      - MONGO_INITDB_ROOT_PASSWORD=123456
      - MONGO_INITDB_DATABASE=example
      - MONGO_INITDB_USERNAME=kajame
      - MONGO_INITDB_PASSWORD=111111
    volumes:
      - ./init-mongo.sh:/docker-entrypoint-initdb.d/init-mongo.sh
      - ./db:/data/db

และสร้างไฟล์ init-mongo.sh และใส่โค้ดดังนี้ครับ

init-mongo.sh
mongosh -- "$MONGO_INITDB_DATABASE" <<EOF
  var rootUser = '$MONGO_INITDB_ROOT_USERNAME';
  var rootPassword = '$MONGO_INITDB_ROOT_PASSWORD';
  var admin = db.getSiblingDB('admin');
  admin.auth(rootUser, rootPassword);
  var user = '$MONGO_INITDB_USERNAME';
  var passwd = '$MONGO_INITDB_PASSWORD';

  db.createUser({
    user: user,
    pwd: passwd,
    roles: ["readWrite"]
  });
EOF

จากนั้นใช้คำสั่ง docker-compose up ใน terminal ครับ (ที่ root project ของเรา)

docker-compose up

สร้าง package.json และ install dependencies

สร้าง package.json ด้วยคำสั่ง

npm init -y

เราจะ install express, body-parser, mongoose ด้วยคำสั่ง

npm install --save express body-parser mongoose

จากนั้น install jest, mongodb-memory-server ใน dev dependencies ครับ

npm install --save-dev jest mongodb-memory-server

จากนั้นเราจะสร้างไฟล์ชื่อ server.js ขึ้นมาครับ

server.js
const express = require('express')
const mongoose = require('mongoose')
const bodyParser = require('body-parser')

const { getTodos, createTodo } = require('./services/todo')
const app = express()

const PORT = 2000

// parse application/x-www-form-urlencoded
app.use(bodyParser.urlencoded({ extended: false }))

// parse application/json
app.use(bodyParser.json())

app.get('/api/todos', async (req, res) => {
  const todos = await getTodos()

  return res.json(todos)
})

app.post('/api/todo', async (req, res) => {
  const { task } = req.body

  if(!task || task.trim() === '') {
    return res.status(400).json({ success: false, message: 'Bad request' })
  }

  const todo = await createTodo({ task })

  return res.json(todo)
})

app.listen(PORT, () => {
  console.log(`Running on port: ${PORT}`)
})

mongoose.set("strictQuery", true);
mongoose.connect(`mongodb://kajame:111111@localhost:27017/example`)
const db = mongoose.connection

db.on('error', () => {
  console.log(`mongoose connection failed`)
})

db.once('open', () => {
  console.log(`mongoose connected`)
})

จากนั้นสร้าง directory ชื่อ services และสร้างไฟล์ชื่อ todo.js ขึ้นมาครับ

services/todo.js
const Todo = require("../models/Todo")

async function getTodos() {
  const todos = await Todo.find({})

  return todos
}

async function createTodo({ task, status = 'pending' }) {
  try {
    const todo = await Todo.create({ task, status })

    return { success: true, _id: todo._id }
  } catch(error) {
    return { success: false }
  }
}

module.exports = {
  getTodos,
  createTodo
}

จากนั้นสร้าง directory ชื่อ models และสร้างไฟล์ชื่อ Todo.js ขึ้นมาครับ

models/Todo.js
const mongoose = require('mongoose')
const Schema = mongoose.Schema

const TodoSchema = new Schema({
  task: String,
  status: {
    type: String,
    enum: ['pending', 'completed']
  }
}, { timestamps: true })

const Todo = mongoose.model('Todo', TodoSchema)

module.exports = Todo

จากนั้นให้เราสร้าง directory ชื่อ __mock__ ใน directory services และสร้างไฟล์ชื่อ database.js ขึ้นมาครับ

services/__mock__/database.js
const mongoose = require('mongoose')

const mockTodos = [
  {
    "_id" : mongoose.Types.ObjectId("63baf3d18f8a71f2531cca60"),
    "task" : "test",
    "status" : "pending",
    "createdAt" : new Date("2023-01-08T23:48:17.103+07:00"),
    "updatedAt" : new Date("2023-01-08T23:48:17.103+07:00"),
    "__v" : 0
  }
]

module.exports = {
  mockTodos
}

สุดท้ายสร้างไฟล์ชื่อ todo.test.js ใน directory services ดังนี้ครับ

services/todo.test.js
const mongoose = require('mongoose')
const { MongoMemoryServer } = require('mongodb-memory-server')

const { getTodos, createTodo } = require('./todo')
const Todo = require('../models/Todo')
const { mockTodos } = require('./__mock__/database')

async function initDefaultData() {
  Todo.create()

  await Promise.all([
    Todo.create(mockTodos),
  ])
}

let mongod = null

beforeAll(async () => {
  mongod = await MongoMemoryServer.create()
  const dbUrl = mongod.getUri()

  await mongoose.connect(dbUrl)

  await initDefaultData()
})

afterAll(async () => {
  await mongoose.connection.close()
  await mongod.stop()
})

describe('getTodos function', () => {
  it('Should return todos length equal 1', async () => {
    const todos = await getTodos()

    expect(todos).toMatchObject(mockTodos)
    expect(todos.length).toEqual(1)
  })
})

describe('createTodo function', () => {
  it('Should return success', async () => {
    const task = 'Test MongoDB'
    const todo = await createTodo({ task })

    expect(todo.success).toEqual(true)
    expect(todo._id).not.toBe(undefined)
  })

  it('Should have todos length equal 2', async () => {
    const todos = await getTodos()

    expect(todos.length).toEqual(2)
  })
})

เราจะมาคุยกันแต่ละส่วนดีกว่าครับว่าไฟล์ที่สร้างทำอะไรบ้าง

  • models/Todo.js => คือ mongoose model ของ Todo collection ครับ
  • services/mock/database.js => เราจะ mock ข้อมูล todos ขึ้นมาครับ ซึ่งอันนี้เราจะใช้สำหรับ Test อย่างเดียวครับ
  • services/todo.test.js => เป็นไฟล์สำหรับ test (เราจะดูไฟล์นี้เป็นหลักนะครับสำหรับบทความนี้)
  • services/todo.js => เป็นไฟล์ที่เจมส์เขียนแยกการทำงานของ todo ออกมาเป็นฟังก์ชันไว้ครับ ซึี่งมี 2 ส่วนคือ getTodo กับ createTodo (เราจะ Test ไฟล์นี้แหละครับ)
  • server.js => เป็น API สำหรับลอง Run แบบจริง ๆ

services/todo.test.js

เรามาดูไฟล์ todo.test.js กันดีกว่าครับว่าเจมส์ Test อะไรยังไงบ้าง

สิ่งแรกคือเจมส์ import mongodb-memory-server มาใช้งาน คือเอาไว้จำลอง mongodb จะได้ไม่กระทบกับของจริงครับ

ในฟังก์ชัน initDefaultData เจมส์ก็ให้สร้างข้อมูลที่ Mock ขึ้นมา โดย import มาจาก services/__mock__/database.js นั่นเอง

ในส่วนฟังก์ชัน beforeAll คือก่อน Test จะให้ตั้งค่าต่าง ๆ ในฟังก์ชั่นนี้ ซึ่งในที่นี้คือจะสร้าง mongoDB จำลองขึ้นมา และเรียกฟังก์ชันสำหรับเอาค่าที่เรา mock ไว้มาใส่

afterAll จะเคลียร์ค่าต่าง ๆ เมื่อ Run คำสั่ง Test ต่าง ๆ เรียบร้อยแล้ว ซึ่งในที่นี้คือ close connection ของ mongoose และ stop mongoDB ที่เราจำลองขึ้นมา

ในส่วนการ Test สำหรับฟังก์ชั่น getTodos เจมส์ทดสอบโดยการเรียกใช้ฟังก์ชันที่สร้าง ซึ่งมันดึงค่า todos จาก mongo มา

ในที่นี้จะมีค่าที่เรา mock ไว้ในตอนต้น ซึ่งสิ่งที่เราคาดหวังจากการ Test นี้คือ ข้อมูลที่ได้จาก getTodos จะมีค่าเท่ากับค่าที่ mock ไว้

ในส่วนการ Test สำหรับฟังก์ชัน createTodo เจมส์ทดสอบโดยการเรียกใช้ฟังก์ชันที่สร้าง โดย Test ว่าเมื่อส่ง task เข้าไปแล้ว มันจะ return success เป็น true ออกมา และมีค่า _id response กลับมาด้วย

จากนั้นก็ลอง Test โดยการเรียกใช้ getTodos และคาดหวังว่าจะมีข้อมูลทั้งหมดคือ 2 ข้อมูล

ปรับไฟล์ package.json

ปรับไฟล์ package.json อีกนิดครับ ในส่วนของ script เราจะปรับเป็นแบบนี้ครับ

package.json
"scripts": {
  "start": "node server.js",
  "test": "jest"
},

เมื่อเราลอง run คำสั่ง

npm run test

จะพบว่า ผ่านหมด ซึ่งการทำงานก็ตามที่ได้อธิบายไปก่อนหน้าแล้ว

เมื่อเราลอง run คำสั่ง

npm run start

แล้วลองเข้า url: http://localhost:2000/api/todos จะพบว่าไม่มีข้อมูล

สิ่งที่เจมส์จะสื่อคือว่า run test ในส่วนที่เกี่ยวข้องกับ mongoDB มันจะไม่ยุ่งกับ mongoDB ของจริง ทำให้ทดสอบได้อย่างปลอดภัย หายห่วง ^^

หากบทความนี้มีส่วนไหนผิดพลาด ก็ขออภัยมา ณ ที่นี้ด้วยเน้อครับ ยังไงถ้าติดตรงไหนก็สามารถทักแชทมาแจ้งก็ได้เน้อครับ เจมส์จะได้อัพเดทให้ครับ

Github: https://github.com/jame3032002/mongodb-testing