播种数据库

2025-06-10

播种数据库

当你为后端编写测试时,你需要测试四种不同类型的操作:

  1. 创建(用于向数据库添加内容)
  2. 读取(从数据库中获取数据)
  3. 更新(用于更改数据库)
  4. 删除(从数据库中删除内容)

最容易测试的类型是创建操作。你将某些内容放入数据库,然后测试它是否存在。

对于其他三种类型的操作,您需要在编写测试之前将一些内容放入数据库中。

将数据放入数据库

向数据库添加初始内容的过程称为播种

假设您想向数据库添加三个用户。这些用户包含一个姓名和一个电子邮件地址。

const users = [
  {
    name: "Zell",
    email: "testing1@gmail.com"
  },
  {
    name: "Vincy",
    email: "testing2@gmail.com"
  },
  {
    name: "Shion",
    email: "testing3@gmail.com"
  }
];
Enter fullscreen mode Exit fullscreen mode

您可以在测试开始时使用模型来播种数据库。

const User = require("../model/User"); // Link to User model

it("does something", async done => {
  // Add users to the database
  for (const u of users) {
    const user = new User(u);
    await user.save();
  }

  // Create the rest of your test here
});
Enter fullscreen mode Exit fullscreen mode

如果每次测试都需要这些用户,最好的方法是通过beforeEach钩子添加它们。beforeEach钩子在每个it声明之前运行。

// Seed the database with users
beforeEach(async () => {
  for (u of users) {
    const user = new User(u);
    await user.save();
  }
});
Enter fullscreen mode Exit fullscreen mode

你也可以使用 Mongoose 的create函数来实现同样的效果。它运行new Model()save(),所以下面的代码和上面的代码做的是相同的。

// Seed the database with users
beforeEach(async () => {
  await User.create(users);
});
Enter fullscreen mode Exit fullscreen mode

创建 vs 插入多个

Mongoose 有第二种方法可以帮助你为数据库设置种子。这种方法称为insertManyinsertMany它比 更快create,因为:

  • insertMany向服务器发送一个操作
  • create为每个文档发送一个操作

但是,insertMany不运行save中间件。

触发保存中间件重要吗?

这取决于你的种子数据。如果你的种子数据需要经过save中间件,则需要使用create。例如,假设你想将用户密码保存到数据库中。你有以下数据:

const users = [
  {
    name: "Zell",
    email: "testing1@gmail.com",
    password: "12345678"
  },
  {
    name: "Vincy",
    email: "testing2@gmail.com",
    password: "12345678"
  },
  {
    name: "Shion",
    email: "testing3@gmail.com",
    password: "12345678"
  }
];
Enter fullscreen mode Exit fullscreen mode

当我们将用户密码保存到数据库时,出于安全考虑,我们需要对密码进行哈希处理。我们通常通过save中间件对密码进行哈希处理。

// Hashes password automatically
userSchema.pre("save", async function(next) {
  if (!this.isModified("password")) return next();
  const salt = bcrypt.genSaltSync(10);
  const hashedPassword = bcrypt.hashSync(password, salt);
  this.password = hashedPassword;
});
Enter fullscreen mode Exit fullscreen mode

如果您使用create,您将获得具有散列密码的用户:

创建运行保存中间件。

如果您使用insertMany,您将获得没有散列密码的用户:

InsertMany 不运行保存中间件。

何时使用 create,何时使用 insertMany

由于insertMany比更快,因此您希望尽可能create使用。insertMany

以下是我的做法:

  1. 如果种子数据不需要save中间件,则使用insertMany
  2. 如果种子数据需要save中间件,请使用create。然后覆盖种子数据,使其不再需要save中间件。

对于上面的密码示例,我会create先运行。然后,我复制粘贴散列后的密码种子数据。然后,我会insertMany从此处继续运行。

如果要覆盖复杂的种子数据,可能需要直接从 MongoDB 获取 JSON。为此,您可以使用mongoexport

mongoexport --db <databaseName> --collection <collectionName> --jsonArray --pretty --out output.json
Enter fullscreen mode Exit fullscreen mode

这说:

  1. 导出<collection><databaseName>
  2. 将输出创建为 JSON 数组,并进行美化,保存在名为 的文件中output.json。该文件将放置在运行命令的文件夹中。

播种多个测试文件和集合

您需要一个地方来存储种子数据,以便在所有测试和集合中使用它们。这是我使用的系统:

  1. 我根据种子文件的模型来命名它们。我User用这个user.seed.js文件来为一个模型播种。
  2. 我把种子文件放在seeds文件夹中
  3. 我循环遍历每个种子文件来为数据库播种。

要循环遍历每个种子文件,您需要使用fs模块。fs代表文件系统。

循环遍历文件最简单的方法是index.js在同一个seeds文件夹中创建一个文件。创建好index.js文件后,你可以使用以下代码查找所有文件*.seed.js

const fs = require("fs");
const util = require("util");

// fs.readdir is written with callbacks.
// This line converts fs.readdir into a promise
const readDir = util.promisify(fs.readdir);

async function seedDatabase() {
  // Gets list of files in the directory
  // `__dirname` points to the `seeds/` folder
  const dir = await readDir(__dirname);

  // Gets a list of files that matches *.seed.js
  const seedFiles = dir.filter(f => f.endsWith(".seed.js"));
}
Enter fullscreen mode Exit fullscreen mode

有了种子文件列表后,就可以循环遍历每个种子文件来填充数据库。这里,我使用for...of循环来简化操作。

async function seedDatabase() {
  for (const file of seedFiles) {
    // Seed the database
  }
}
Enter fullscreen mode Exit fullscreen mode

要为数据库添加种子,我们需要根据种子文件的名称找到正确的 Mongoose 模型。名为 的文件user.seed.js应该可以作为模型的种子User。这意味着:

  1. 我们必须user找到user.seed.js
  2. 我们必须user利用User

这是一个粗略的版本,可以完成所需的功能。(如果您愿意,可以使用正则表达式而不是 ,使代码更健壮split)。

for (const file of seedFiles) {
  const fileName = file.split(".seed.js")[0];
  const modelName = toTitleCase(fileName);
  const model = mongoose.models[modelName];
}
Enter fullscreen mode Exit fullscreen mode

接下来,我们要确保每个文件都有一个与之对应的模型。如果找不到该模型,我们会抛出一个错误。

for (const file of seedFiles) {
  //...
  if (!model) throw new Error(`Cannot find Model '${modelName}'`);
}
Enter fullscreen mode Exit fullscreen mode

如果有对应的模型,我们需要用种子文件中的内容填充数据库。为此,我们需要先读取种子文件。由于我使用了.js扩展,因此在这里我可以直接 require 该文件。

for (const file of seedFiles) {
  //...
  const fileContents = require(path.join(__dirname, file));
}
Enter fullscreen mode Exit fullscreen mode

为了使其工作,我的种子文件必须导出一个数据数组。

module.exports = [
  {
    name: "Zell",
    email: "testing1@gmail.com",
    password: "12345678"
  },
  {
    name: "Vincy",
    email: "testing2@gmail.com",
    password: "12345678"
  },
  {
    name: "Shion",
    email: "testing3@gmail.com",
    password: "12345678"
  }
];
Enter fullscreen mode Exit fullscreen mode

一旦我有了种子文件的内容,我就可以运行createinsertMany

async function seedDatabase(runSaveMiddleware = false) {
  // ...
  for (const file of seedFiles) {
    // ...

    runSaveMiddleware
      ? model.create(fileContents)
      : model.insertMany(fileContents);
  }
}
Enter fullscreen mode Exit fullscreen mode

以下是整个seedDatabase代码:

const fs = require("fs");
const util = require("util");
const readDir = util.promisify(fs.readdir).bind(fs);
const path = require("path");
const mongoose = require("mongoose");

function toTitleCase(str) {
  return str.replace(/\w\S*/g, txt => {
    return txt.charAt(0).toUpperCase() + txt.substr(1).toLowerCase();
  });
}

async function seedDatabase(runSaveMiddleware = false) {
  const dir = await readDir(__dirname);
  const seedFiles = dir.filter(f => f.endsWith(".seed.js"));

  for (const file of seedFiles) {
    const fileName = file.split(".seed.js")[0];
    const modelName = toTitleCase(fileName);
    const model = mongoose.models[modelName];

    if (!model) throw new Error(`Cannot find Model '${modelName}'`);
    const fileContents = require(path.join(__dirname, file));

    runSaveMiddleware
      ? await model.create(fileContents)
      : await model.insertMany(fileContents);
  }
}
Enter fullscreen mode Exit fullscreen mode

为什么是 JS,而不是 JSON?

使用 JSON 存储数据是行业规范。在这种情况下,我发现使用 JavaScript 对象更方便,因为:

  1. 我不需要为每个属性写出开始和结束双引号。
  2. 我根本就不需要用双引号!(用单引号更容易,因为不需要按 Shift 键)。
// Which is easier to write. JavaScript objects or JSON?

// JavaScript objects
module.exports = [
  {
    objectName: "property"
  }
][
  // JSON
  {
    objectName: "property"
  }
];
Enter fullscreen mode Exit fullscreen mode

如果您想使用 JSON,请确保将其更改seedDatabase为使用 JSON。(我会让您自己完成代码)。

调整setupDB函数

在上一篇文章,我创建了一个setupDB函数来帮助我为测试设置数据库。seedDatabase进入该setupDB函数,因为播种是设置过程的一部分。

async function seedDatabase(runSaveMiddleware = false) {
  // ...
}

module.exports = {
  setupDB(databaseName, runSaveMiddleware = false) {
    // Connect to Mongoose
    beforeAll(/*...*/);

    // Seed Data
    beforeEach(async () => {
      await seedDatabase(runSaveMiddleware);
    });

    // Cleans up database between each test
    afterEach(/*...*/);

    // Disconnect Mongoose
    afterAll(/*...*/);
  }
};
Enter fullscreen mode Exit fullscreen mode

Github 仓库

我创建了一个Github 仓库来配合这三部分的测试系列。希望这些演示代码能帮助你开始测试你的应用程序。


感谢阅读。本文最初发布在我的博客上。如果您想阅读更多文章来帮助您成为更优秀的前端开发人员,请订阅我的新闻通讯。

鏂囩珷鏉ユ簮锛�https://dev.to/zellwk/seeding-a-database-1847
PREV
以不同的方式设置悬停、焦点和活动状态的样式
NEXT
我的 CSS 重置