การรวมและการทดสอบแบบ end-to-end ทำได้ง่ายด้วย Node.js และ MongoDB
เผยแพร่แล้ว: 2022-03-11การทดสอบเป็นส่วนสำคัญในการสร้างแอปพลิเคชัน Node.js ที่แข็งแกร่ง การทดสอบที่เหมาะสมสามารถเอาชนะข้อบกพร่องมากมายที่นักพัฒนาอาจชี้ให้เห็นเกี่ยวกับโซลูชันการพัฒนา Node.js ได้อย่างง่ายดาย
แม้ว่านักพัฒนาหลายคนจะเน้นไปที่การทดสอบหน่วยความครอบคลุม 100% แต่สิ่งสำคัญคือโค้ดที่คุณเขียนไม่ได้เป็นเพียงการทดสอบแบบแยกส่วน การผสานรวมและการทดสอบแบบ end-to-end ช่วยให้คุณมั่นใจยิ่งขึ้นด้วยการทดสอบส่วนต่างๆ ของแอปพลิเคชันร่วมกัน ชิ้นส่วนเหล่านี้อาจทำงานได้ดีในตัวเอง แต่ในระบบขนาดใหญ่ หน่วยของโค้ดมักจะทำงานแยกจากกัน
Node.js และ MongoDB รวมกันเป็นหนึ่งในดูโอ้ที่ได้รับความนิยมมากที่สุดเมื่อไม่นานนี้ หากคุณเป็นหนึ่งในหลาย ๆ คนที่ใช้มันแสดงว่าคุณโชคดี
ในบทความนี้ คุณจะได้เรียนรู้วิธีเขียนการผสานรวมและการทดสอบแบบ end-to-end อย่างง่ายดายสำหรับแอปพลิเคชัน Node.js และ MongoDB ที่ทำงานบนอินสแตนซ์จริงของฐานข้อมูล โดยไม่ต้องตั้งค่าสภาพแวดล้อมที่ซับซ้อนหรือโค้ดการตั้งค่า/การฉีกขาดที่ซับซ้อน .
คุณจะเห็นว่าแพ็คเกจ mongo-unit ช่วยในการรวมและการทดสอบแบบ end-to-end ใน Node.js ได้อย่างไร สำหรับภาพรวมที่ครอบคลุมมากขึ้นของการทดสอบการรวม Node.js โปรดดูบทความนี้
การจัดการกับฐานข้อมูลจริง
โดยทั่วไป สำหรับการทดสอบแบบบูรณาการหรือแบบ end-to-end สคริปต์ของคุณจะต้องเชื่อมต่อกับฐานข้อมูลเฉพาะจริงเพื่อการทดสอบ สิ่งนี้เกี่ยวข้องกับการเขียนโค้ดที่รันที่จุดเริ่มต้นและจุดสิ้นสุดของทุกกรณีทดสอบ/ชุด เพื่อให้แน่ใจว่าฐานข้อมูลอยู่ในสถานะที่คาดการณ์ได้อย่างชัดเจน
การดำเนินการนี้อาจใช้ได้ผลดีสำหรับบางโครงการ แต่มีข้อจำกัดบางประการ:
- สภาพแวดล้อมการทดสอบนั้นค่อนข้างซับซ้อน คุณจะต้องให้ฐานข้อมูลทำงานอยู่ที่ไหนสักแห่ง ซึ่งมักจะต้องใช้ความพยายามเป็นพิเศษในการตั้งค่ากับเซิร์ฟเวอร์ CI
- ฐานข้อมูลและการดำเนินการได้ค่อนข้างช้า เนื่องจากฐานข้อมูลจะใช้การเชื่อมต่อเครือข่ายและการดำเนินการต่างๆ จะต้องมีกิจกรรมระบบไฟล์ การทดสอบหลายพันครั้งอย่างรวดเร็วจึงไม่ใช่เรื่องง่าย
- ฐานข้อมูลจะคงสถานะไว้ และไม่สะดวกสำหรับการทดสอบ การทดสอบควรเป็นอิสระจากกัน แต่การใช้ฐานข้อมูลร่วมกันอาจทำให้การทดสอบหนึ่งมีผลกับการทดสอบอื่นๆ
ในทางกลับกัน การใช้ฐานข้อมูลจริงทำให้สภาพแวดล้อมการทดสอบใกล้เคียงกับการผลิตมากที่สุด ซึ่งถือได้ว่าเป็นข้อดีอย่างหนึ่งของแนวทางนี้
การใช้ฐานข้อมูลจริงในหน่วยความจำ
การใช้ฐานข้อมูลจริงสำหรับการทดสอบดูเหมือนจะมีความท้าทายอยู่บ้าง แต่ข้อดีของการใช้ฐานข้อมูลจริงนั้นดีเกินกว่าจะส่งต่อ เราจะหลีกเลี่ยงความท้าทายและรักษาความได้เปรียบได้อย่างไร
การนำโซลูชันที่ดีมาใช้ซ้ำจากแพลตฟอร์มอื่นและนำไปใช้กับโลก Node.js อาจเป็นวิธีที่จะไปที่นี่
โปรเจ็กต์ Java ใช้ DBUnit กับฐานข้อมูลในหน่วยความจำ (เช่น H2) อย่างกว้างขวางเพื่อจุดประสงค์นี้
DBUnit ถูกรวมเข้ากับ JUnit (ตัวดำเนินการทดสอบ Java) และให้คุณกำหนดสถานะฐานข้อมูลสำหรับชุดการทดสอบ/การทดสอบแต่ละชุด ฯลฯ ซึ่งจะลบข้อจำกัดที่กล่าวถึงข้างต้น:
- DBUnit และ H2 เป็นไลบรารี Java ดังนั้นคุณไม่จำเป็นต้องตั้งค่าสภาพแวดล้อมเพิ่มเติม ทุกอย่างทำงานใน JVM
- ฐานข้อมูลในหน่วยความจำทำให้การจัดการสถานะนี้รวดเร็วมาก
- DBUnit ทำให้การกำหนดค่าฐานข้อมูลง่ายมาก และช่วยให้คุณรักษาสถานะฐานข้อมูลที่ชัดเจนสำหรับแต่ละกรณี
- H2 เป็นฐานข้อมูล SQL และเข้ากันได้กับ MySQL บางส่วน ดังนั้น ในกรณีหลัก แอปพลิเคชันสามารถทำงานร่วมกับฐานข้อมูลนี้ได้เช่นเดียวกับฐานข้อมูลที่ใช้งานจริง
จากแนวคิดเหล่านี้ ฉันจึงตัดสินใจสร้างสิ่งที่คล้ายกันสำหรับ Node.js และ MongoDB: Mongo-unit
Mongo-unit เป็นแพ็คเกจ Node.js ที่สามารถติดตั้งได้โดยใช้ NPM หรือ Yarn มันรัน MongoDB ในหน่วยความจำ ทำให้การทดสอบการรวมทำได้ง่ายโดยการผสานรวมกับ Mocha และจัดเตรียม API อย่างง่ายเพื่อจัดการสถานะฐานข้อมูล
ไลบรารีใช้แพ็คเกจ NPM ที่สร้างไว้ล่วงหน้า mongodb ซึ่งมีไบนารี MongoDB ที่สร้างไว้ล่วงหน้าสำหรับระบบปฏิบัติการยอดนิยม อินสแตนซ์ MongoDB เหล่านี้สามารถทำงานในโหมดในหน่วยความจำ
การติดตั้ง Mongo-unit
ในการเพิ่ม mongo-unit ในโครงการของคุณ คุณสามารถเรียกใช้:
npm install -D mongo-unitหรือ
yarn add mongo-unitและนั่นคือมัน คุณไม่จำเป็นต้องติดตั้ง MongoDB บนคอมพิวเตอร์เพื่อใช้แพ็คเกจนี้
การใช้ Mongo-unit สำหรับการทดสอบการรวม
สมมติว่าคุณมีแอปพลิเคชัน Node.js แบบง่ายสำหรับจัดการงาน:
// service.js const mongoose = require('mongoose') const mongoUrl = process.env.MONGO_URL || 'mongodb://localhost:27017/example' mongoose.connect(mongoUrl) const TaskSchema = new mongoose.Schema({ name: String, started: Date, completed: Boolean, }) const Task = mongoose.model('tasks', TaskSchema) module.exports = { getTasks: () => Task.find(), addTask: data => new Task(data).save(), deleteTask: taskId => Task.findByIdAndRemove(taskId) }URL การเชื่อมต่อ MongoDB ไม่ได้ฮาร์ดโค้ดที่นี่ เช่นเดียวกับแบ็กเอนด์ของเว็บแอปพลิเคชันส่วนใหญ่ เราจะดึงมันมาจากตัวแปรสภาพแวดล้อม ซึ่งจะทำให้เราสามารถแทนที่ URL ใด ๆ ในระหว่างการทดสอบ
const express = require('express') const bodyParser = require('body-parser') const service = require('./service') const app = express() app.use(bodyParser.json()) app.use(express.static(`${__dirname}/static`)) app.get('/example', (req, res) => { service.getTasks().then(tasks => res.json(tasks)) }) app.post('/example', (req, res) => { service.addTask(req.body).then(data => res.json(data)) }) app.delete('/example/:taskId', (req, res) => { service.deleteTask(req.params.taskId).then(data => res.json(data)) }) app.listen(3000, () => console.log('started on port 3000'))นี่เป็นตัวอย่างแอปพลิเคชันที่มีอินเทอร์เฟซผู้ใช้ รหัสสำหรับ UI ถูกตัดออกเนื่องจากความกระชับ คุณสามารถดูตัวอย่างทั้งหมดได้ที่ GitHub
ผสมผสานกับมอคค่า
เพื่อให้ Mocha เรียกใช้การทดสอบการรวมกับ mongo-unit เราจำเป็นต้องเรียกใช้อินสแตนซ์ฐานข้อมูล mongo-unit ก่อนโหลดโค้ดแอปพลิเคชันในบริบท Node.js ในการทำเช่นนี้ เราสามารถใช้พารามิเตอร์ mocha --require และไลบรารี Mocha-prepare ซึ่งช่วยให้คุณดำเนินการแบบอะซิงโครนัสในสคริปต์ require
// it-helper.js const prepare = require('mocha-prepare') const mongoUnit = require('mongo-unit') prepare(done => mongoUnit.start() .then(testMongoUrl => { process.env.MONGO_URL = testMongoUrl done() }))การเขียนแบบทดสอบบูรณาการ
ขั้นตอนแรกคือการเพิ่มการทดสอบลงในฐานข้อมูลการทดสอบ ( testData.json ):

{ "tasks": [ { "name": "test", "started": "2017-08-28T16:07:38.268Z", "completed": false } ] }ขั้นตอนต่อไปคือการเพิ่มการทดสอบด้วยตัวเอง:
const expect = require('chai').expect const mongoose = require('mongoose') const mongoUnit = require('../index') const service = require('./app/service') const testMongoUrl = process.env.MONGO_URL describe('service', () => { const testData = require('./fixtures/testData.json') beforeEach(() => mongoUnit.initDb(testMongoUrl, testData)) afterEach(() => mongoUnit.drop()) it('should find all tasks', () => { return service.getTasks() .then(tasks => { expect(tasks.length).to.equal(1) expect(tasks[0].name).to.equal('test') }) }) it('should create new task', () => { return service.addTask({ name: 'next', completed: false }) .then(task => { expect(task.name).to.equal('next') expect(task.completed).to.equal(false) }) .then(() => service.getTasks()) .then(tasks => { expect(tasks.length).to.equal(2) expect(tasks[1].name).to.equal('next') }) }) it('should remove task', () => { return service.getTasks() .then(tasks => tasks[0]._id) .then(taskId => service.deleteTask(taskId)) .then(() => service.getTasks()) .then(tasks => { expect(tasks.length).to.equal(0) }) }) })และ ว้าว!
สังเกตว่ามีโค้ดเพียงไม่กี่บรรทัดที่เกี่ยวข้องกับการตั้งค่าและการฉีกขาด
อย่างที่คุณเห็น มันง่ายมากในการเขียนการทดสอบการรวมโดยใช้ไลบรารี mongo-unit เราไม่ได้ล้อเลียน MongoDB และเราสามารถใช้แบบจำลอง Mongoose เดียวกันได้ เรามีการควบคุมข้อมูลฐานข้อมูลอย่างสมบูรณ์และไม่แพ้ผลการทดสอบมากนัก เนื่องจาก MongoDB ปลอม กำลังทำงานอยู่ในหน่วยความจำ
สิ่งนี้ยังช่วยให้เราใช้แนวปฏิบัติการทดสอบหน่วยที่ดีที่สุดสำหรับการทดสอบการรวม:
- ทำให้การทดสอบแต่ละรายการเป็นอิสระจากการทดสอบอื่นๆ เราโหลดข้อมูลใหม่ก่อนการทดสอบแต่ละครั้ง ทำให้เรามีสถานะอิสระโดยสิ้นเชิงสำหรับการทดสอบแต่ละครั้ง
- ใช้สถานะขั้นต่ำที่จำเป็นสำหรับการทดสอบแต่ละครั้ง เราไม่จำเป็นต้องเติมฐานข้อมูลทั้งหมด เราจำเป็นต้องตั้งค่าข้อมูลขั้นต่ำที่จำเป็นสำหรับการทดสอบแต่ละรายการเท่านั้น
- เราสามารถนำการเชื่อมต่อหนึ่งรายการมาใช้ซ้ำสำหรับฐานข้อมูลได้ เป็นการเพิ่มประสิทธิภาพการทดสอบ
เป็นโบนัส เราสามารถเรียกใช้แอปพลิเคชันเองกับ mongo-unit ได้ ช่วยให้เราทำการทดสอบแบบ end-to-end สำหรับแอปพลิเคชันของเรากับฐานข้อมูลจำลอง
การทดสอบแบบ end-to-end ด้วยซีลีเนียม
สำหรับการทดสอบแบบ end-to-end เราจะใช้ Selenium WebDriver และ Hermione E2E test runner
ขั้นแรก เราจะบูตสแตรปไดรเวอร์และตัววิ่งทดสอบ:
const mongoUnit = require('mongo-unit') const selenium = require('selenium-standalone') const Hermione = require('hermione') const hermione = new Hermione('./e2e/hermione.conf.js') //hermione config seleniumInstall() //make sure selenium is installed .then(seleniumStart) //start selenium web driver .then(mongoUnit.start) // start mongo unit .then(testMongoUrl => { process.env.MONGO_URL = testMongoUrl //store mongo url }) .then(() => { require('./index.js') //start application }) .then(delay(1000)) // wait a second till application is started .then(() => hermione.run('', hermioneOpts)) // run hermiona e2e tests .then(() => process.exit(0)) .catch(() => process.exit(1))นอกจากนี้เรายังต้องการฟังก์ชันตัวช่วย (การจัดการข้อผิดพลาดถูกลบออกเพื่อความกระชับ):
function seleniumInstall() { return new Promise(resolve => selenium.install({}, resolve)) } function seleniumStart() { return new Promise(resolve => selenium.start(resolve)) } function delay(timeout) { return new Promise(resolve => setTimeout(resolve, timeout)) }หลังจากกรอกข้อมูลบางส่วนลงในฐานข้อมูลและทำความสะอาดเมื่อการทดสอบเสร็จสิ้น เราสามารถเรียกใช้การทดสอบครั้งแรกของเราได้:
const expect = require('chai').expect const co = require('co') const mongoUnit = require('../index') const testMongoUrl = process.env.MONGO_URL const DATA = require('./fixtures/testData.json') const ui = { task: '.task', remove: '.task .remove', name: '#name', date: '#date', addTask: '#addTask' } describe('Tasks', () => { beforeEach(function () { return mongoUnit.initDb(testMongoUrl, DATA) .then(() => this.browser.url('http://localhost:3000')) }) afterEach(() => mongoUnit.dropDb(testMongoUrl)) it('should display list of tasks', function () { const browser = this.browser return co(function* () { const tasks = yield browser.elements(ui.task) expect(tasks.length, 1) }) }) it('should create task', function () { const browser = this.browser return co(function* () { yield browser.element(ui.name).setValue('test') yield browser.element(ui.addTask).click() const tasks = yield browser.elements(ui.task) expect(tasks.length, 2) }) }) it('should remove task', function () { const browser = this.browser return co(function* () { yield browser.element(ui.remove).click() const tasks = yield browser.elements(ui.task) expect(tasks.length, 0) }) }) })อย่างที่คุณเห็น การทดสอบแบบ end-to-end ดูคล้ายกับการทดสอบการรวมระบบมาก
สรุป
การบูรณาการและการทดสอบแบบ end-to-end มีความสำคัญต่อการใช้งานขนาดใหญ่ โดยเฉพาะอย่างยิ่ง แอปพลิเคชัน Node.js จะได้รับประโยชน์อย่างมากจากการทดสอบอัตโนมัติ ด้วย mongo-unit คุณสามารถเขียนการรวมและการทดสอบแบบ end-to-end โดยไม่ต้องกังวลเกี่ยวกับความท้าทายที่มาพร้อมกับการทดสอบดังกล่าว
คุณสามารถดูตัวอย่างการใช้ mongo-unit ทั้งหมดบน GitHub ได้
อ่านเพิ่มเติมในบล็อก Toptal Engineering:
- การสร้าง Node.js/TypeScript REST API ส่วนที่ 3: MongoDB การตรวจสอบสิทธิ์ และการทดสอบอัตโนมัติ
