Picture of the author
Published on

เขียน Test API Node.js ด้วย Jest และใช้ DB เป็น Mongo

Jest Cover

ขอบคุณรูปภาพจาก ms314006.github.io

สวัสดีครับ ผมจะลองยกตัวอย่างการเขียน Test สั้นๆด้วย Jest โปรเจค Node ของผมใช้ Express.js และ DB เป็น MongoDB ส่วนของการ Test เป็นการเขียน Test API ครับ

Install - Jest

npm i jest

Install - Supertest

ใช้สำหรับ Call API ตอน Run test

npm i supertest

Install - Mongodb Memory

เราจะใช้ตัวนี้เพื่อเป็นการ Mock mongoDB ขึ้นมาตอน Run test ถ้าใน Function ที่เรา Test มีการต่อ DB ตอน Run test เราจะไม่ต่อ DB จริงๆ

npm i mongodb-memory-server
. . .

สร้าง connection สำหรับ mongodb memory

ใช้สำหรับ connect , clear และ close mongoDB

/config/con.memory.js
const mongoose = require("mongoose")
const {MongoMemoryServer} = require("mongodb-memory-server")

module.exports.connect = async () => {
  const mongoDb = await MongoMemoryServer.create()
  const uri = mongoDb.getUri()
  const mongooseOpts = {
    useNewUrlParser: true,
    useUnifiedTopology: true,
    maxPoolSize: 10,
  }
  await mongoose.connect(uri, mongooseOpts)
}

module.exports.closeDatabase = async () => {
  await mongoose.connection.dropDatabase()
  await mongoose.connection.close()
}

module.exports.clearDatabase = async () => {
  const collections = mongoose.connection.collections
  for (const key in collections) {
    const collection = collections[key]
    await collection.deleteMany()
  }
}
. . .

แก้ไข config เมื่อใช้ npm test

package.json
"test": "jest --watchAll --coverage "
  • --watchAll - Run ทุก test
  • --coverage - แสดง % การ test ของแต่ล่ะไฟล์ว่า test ไปกี่ % แล้ว และแสดงด้วยว่าแต่ล่ะไฟล์มี line ไหนที่ยังไม่ได้ test
. . .

Function ที่เราจะ Test

/controller/signup.controller.js
exports.signup = async (req, res) => {
  const countF = await Farm.collection.countDocuments()
  console.log("farm count : ", countF)

  const farm = new Farm({
    code: "F" + String(countF + 1).padStart(4, "0"),
    name: req.body.farmName,
    lineToken: null,
  })

  const user = new User({
    username: req.body.username,
    password: bcrypt.hashSync(req.body.password, 8),
    farm: farm,
  })

  const farmResp = await farm.save()
  console.log("farm saved : ", farmResp)

  const userResp = await user.save()
  console.log("user saved : ", userResp)

  res.status(200).send({message: "Registered Successfully."})
}
. . .

สร้างไฟล์สำหรับ Test

ต้องสร้างชื่อไฟล์ตามด้วย .test เสมอ เพื่อให้ jest มองเห็น เมื่อ run มันจะวิ่งหาไฟล์ที่มี .test ทั้งหมด

/test/auth.test.js
const express = require("express")
const request = require("supertest")
const authController = require("../controllers/auth.controller")
const {verifySignUp} = require("../middlewares")
const db = require("../config/conn.memory")

//เรียก function นี้ก่อน run test ทั้งหมดสำหรับไฟล์นี้
beforeAll(async () => {
  //เชื่อมต่อ db memory
  await db.connect()
})

//เรียก function นี้ทุกครั้งหลัง run เสร็จแต่ล่ะ test
afterEach(async () => {
  //เคลียร์ข้อมูลทั้งหมดใน db memory
  await db.clearDatabase()
})

//เรียก function นี้เมื่อ run ครบทุก test
afterAll(async () => {
  //ยกเลิกเชื่อมต่อ db และ drop db ทิ้ง
  await db.closeDatabase()
})

//อธิบายว่ากลุ่มการ test นี้ทำอะไร
describe("Signup ", () => {
  let app

  //เรียก Function นี้ก่อนจะ run แต่ล่ะ test
  beforeEach(() => {
    app = express()
    app.use(express.json())

    //กำหนด Path API ที่เราจะ Test
    //API นี้มี Middleware check dupplicate username ด้วย
    app.post("/signup", [verifySignUp.checkDuplicateUsername], authController.signup)
  })

  //Test นี้คือต้องสร้าง user สำเร็จ
  test("Should create a new user", async () => {
    const res = await request(app).post("/signup").send({
      username: "signuptest",
      farmName: "signuptest",
      password: "P@ssw0rd",
    })

    //status ต้องเท่ากับ 200
    expect(res.status).toEqual(200)
    //body ต้องได้ { message : 'Registered Successfully.'}
    expect(res.body).toHaveProperty("message", "Registered Successfully.")
  })

  //Test นี้คือต้องสร้าง user ไม่สำเร็จ เพราะมีการใช้ username ซ้ำ
  test("Should return error if user already exists", async () => {
    await request(app).post("/signup").send({
      username: "signuptest",
      farmName: "signuptest",
      password: "P@ssw0rd",
    })
    const res = await request(app).post("/signup").send({
      username: "signuptest",
      farmName: "signuptest",
      password: "P@ssw0rd",
    })
    //status ต้องเท่ากับ 400
    expect(res.status).toEqual(400)
    //body ต้องได้ { message : 'Failed! Username is already in use!'}
    expect(res.body).toHaveProperty("message", "Failed! Username is already in use!")
  })
})

สามารถเข้าไปดูการใช้ expect เพิ่มเติมได้ที่ https://jestjs.io/docs/expect

. . .

Run

npm test
  • กรณีผ่านทุก Test
Test result

จะเห็นว่าแสดงผล % การ Test ทุกไฟล์ที่มีการ Test เลย และในคอลัมน์สุดท้าย Uncoverd Line บอกด้วยว่าบรรทัดที่เท่าไหร่บ้างที่ไม่ถูก Test ซึ่งเราก็ควรจะเขียน Test case ให้ครบจนกว่าจะเป็น 100 และเขียวหมด แสดงว่า Code ทุกบรรทัดที่เราเขียนได้ถูก Test หมดแล้ว

  • กรณีมี Test fail
Test result

กรณี Run test ไม่ผ่าน jest จะบอกเลย value ที่ได้มาคืออะไร แล้ว expect อะไร fail ที่บรรทัดไหน ในรูปคือ return status 400 มาแต่เรา expect 500 ทำให้ Test ไม่ผ่าน