发布于 2026-01-06 0 阅读
0

将逻辑与 Express 路由分离,以便于测试

将逻辑与 Express 路由分离,以便于测试

本文最初发布于coreycleary.me。这是我内容博客的转载文章。我每隔一两周会发布新内容,如果您想直接在邮箱中接收我的文章,可以订阅我的电子报!我还会定期发送速查表、其他优秀教程(由其他人撰写)的链接以及其他免费资源。

你是否曾经对如何构建 Express 应用程序以使其易于测试感到困惑?

与 Node.js 世界中的大多数事物一样,编写和构建 Express 应用程序的方法有很多种。

不过,最好的入手点通常是经典的“Hello World”示例,以下是Express 文档中的示例:

const express = require('express')
const app = express()
const port = 3000

app.get('/', (req, res) => res.send('Hello World!'))

app.listen(port, () => console.log(`Example app listening on port ${port}!`))
Enter fullscreen mode Exit fullscreen mode

该线路app.get('/', (req, res) => res.send('Hello World!'))是实际发送响应的路径。

因此,基于此,如果我们想要添加一个新的 HTTP 路由,那么似乎应该遵循相同的模式,将路由处理代码添加到回调.get().post方法中。

如果我们有一个网页论坛应用程序,并且想要创建一个用户,那么代码可能如下所示:

app.post('/api/user', async (req, res) => {
  const userName = req.body.user_name
  const userType = req.body.user_type
  try {
    await insert(userType, userName)
    res.sendStatus(201)
  } catch(e) {
    res.sendStatus(500)
    console.log(e)
  }
})
Enter fullscreen mode Exit fullscreen mode

……遵循“Hello World”示例结构

但到了实际测试的时候该怎么办呢?我们如何对路由进行端到端测试,以及如何对路由处理程序中包含的实际用户创建逻辑进行单元测试?

目前来看,测试可能如下所示:

describe('POST /api/user', () => {
  before(async () => {
    await createTable('admin')
    await createTable('member')
  })

  after(async () => {
    await dropTable('admin')
    await dropTable('member')
  })

  it('should respond with 201 if user account created successfully', async () => {
    const response = await request(app)
      .post('/api/user')
      .send({user_name: "ccleary00", user_type: "admin"})
      .set('Accept', 'application/json')

      expect(response.statusCode).to.equal(201)
  })
})
Enter fullscreen mode Exit fullscreen mode

目前用户创建逻辑位于回调函数中,因此我们不能直接“导出”回调函数。为了测试该逻辑,我们始终需要向服务器发送请求,使其实际访问 POST /api/user 路由。

这就是我们上面所做的,使用supertest发送请求并对服务器返回的响应进行断言。

空气中的气味

但总觉得哪里不太对劲……

为一个应该作为单元进行测试的东西编写这样的端到端测试,感觉很奇怪。

如果用户创建逻辑变得越来越复杂——比如需要调用邮件服务发送用户注册邮件,需要检查用户账户是否已存在等等——那该怎么办?我们就必须测试所有这些不同的逻辑分支,而使用 Supertest 进行端到端测试很快就会变得非常繁琐。

幸运的是,解决这个问题的方法很简单,可以进行测试。更重要的是,它还能帮助我们更好地实现关注点分离,将 HTTP 代码与业务逻辑代码分离。

从路由中提取逻辑

要使这条路由可测试,最简单的方法是将当前回调函数中的代码放到一个单独的函数中:

export default async function createUser (req, res) => {
  const userName = req.body.user_name
  const userType = req.body.user_type
  try {
    await insert(userType, userName)
    res.sendStatus(201)
  } catch(e) {
    res.sendStatus(500)
    console.log(e)
  }
}
Enter fullscreen mode Exit fullscreen mode

然后将导入到 Express 路线中:

const createUser = require('./controllers/user')
app.post('/api/user', createUser)
Enter fullscreen mode Exit fullscreen mode

现在我们仍然可以为路由编写端到端测试,使用与以前相同的许多测试代码,但我们也可以将createUser()函数作为一个单元进行更多测试。

一块砖一块砖地

例如,如果我们有验证/转换逻辑来禁止使用大写字母或全大写的用户名,我们可以添加该逻辑并断言数据库中存储的名称确实是小写字母:

export default async function createUser (req, res) => {
  const userName = req.body.user_name.toLowerCase() // QUIETER!!
  const userType = req.body.user_type
  try {
    await insert(userType, userName)
    res.sendStatus(201)
  } catch(e) {
    res.sendStatus(500)
    console.log(e)
  }
}
Enter fullscreen mode Exit fullscreen mode

这种验证/转换逻辑可能会变得更加复杂,例如需要从用户名中删除空格或在创建用户之前检查是否存在冒犯性名称等等。你应该明白我的意思了。

那时,我们可以将该逻辑提取到一个单独的函数中,并作为一个整体进行测试

export function format(userName) {
  return userName.trim().toLowerCase()
}

describe('#format', () => {
  it('should trim white space from ends of user name', () => {
    const formatted = format('  ccleary00 ')
    expect(formatted).to.equal('ccleary00')
  })

  it('should convert the user name to all lower case', () => {
    const formatted = format('CCLEARY00')
    expect(formatted).to.equal('ccleary00')
  })
})
Enter fullscreen mode Exit fullscreen mode

因此,与其将所有逻辑都放在路由的回调函数中,不如将其拆分成单独的单元,以便更容易地进行测试,而无需模拟很多东西。

虽然理论上我们可以使用原来的方法,即向 Express 路由发送请求来编写这些测试,但这会困难得多。而当编写测试很困难时,它们往往就根本不会被编写出来……

总结

Express 应用程序的结构有很多种,你还可以通过将核心用户创建逻辑提取到一个“服务”中来进一步细化,同时让路由控制器处理验证。

但就目前而言,最关键的一点是避免在路由回调函数中编写逻辑。这样做会大大简化未来的测试和重构工作。

测试应该很简单,而不是很困难。如果你发现编写应用程序的测试非常痛苦,这通常是你需要重构或重写部分代码的第一个信号。有时,你甚至在编写了大量代码之后才意识到这一点,而重构过程会更加痛苦。

我发现避免这种情况的最佳方法是使用测试驱动开发 (TDD)——它最终让我避免编写糟糕的代码很多次(例如我在本文中用作起始示例的 Express 用户路由代码)。

先写测试再写代码可能会感觉很奇怪,但如果你想获得一些关于如何培养这种思维方式的指导,以帮助你“理解”,请查看我在这里写的另一篇关于 TDD 的文章

此外,我正在撰写大量新内容,旨在让 JavaScript(以及 JavaScript 本身)的测试变得更加轻松。之所以说“更加轻松”,是因为我认为它不必像有时那样复杂。如果您不想错过任何一篇新文章,请再次点击此链接订阅我的新闻邮件!

文章来源:https://dev.to/ccleary00/separating-logic-from-express-routes-for-easier-testing-4e4h