当前位置: 首页 > news >正文

如何为 Nestjs 编写单元测试和 E2E 测试

前言

最近在给一个 nestjs 项目写单元测试(Unit Testing)和 e2e 测试(End-to-End Testing,端到端测试,简称 e2e 测试),这是我第一次给后端项目写测试,发现和之前给前端项目写测试还不太一样,导致在一开始写测试时感觉无从下手。后来在看了一些示例之后才想明白怎么写测试,所以打算写篇文章记录并分享一下,以帮助和我有相同困惑的人。

同时我也写了一个 demo 项目,相关的单元测试、e2e 测试都写好了,有兴趣可以看一下。代码已上传到 Github: nestjs-interview-demo。

单元测试和 E2E 测试的区别

单元测试和 e2e 测试都是软件测试的方法,但它们的目标和范围有所不同。

单元测试是对软件中的最小可测试单元进行检查和验证。比如一个函数、一个方法都可以是一个单元。在单元测试中,你会对这个函数的各种输入给出预期的输出,并验证功能的正确性。单元测试的目标是快速发现函数内部的 bug,并且它们容易编写、快速执行。

而 e2e 测试通常通过模拟真实用户场景的方法来测试整个应用,例如前端通常使用浏览器或无头浏览器来进行测试,后端则是通过模拟对 API 的调用来进行测试。

在 nestjs 项目中,单元测试可能会测试某个服务(service)、某个控制器(controller)的一个方法,例如测试 Users 模块中的 update 方法是否能正确的更新一个用户。而一个 e2e 测试可能会测试一个完整的用户流程,如创建一个新用户,然后更新他们的密码,然后删除该用户。这涉及了多个服务和控制器。

编写单元测试

为一个工具函数或者不涉及接口的方法编写单元测试,是非常简单的,你只需要考虑各种输入并编写相应的测试代码就可以了。但是一旦涉及到接口,那情况就复杂了。用代码来举例:

async validateUser(username: string,password: string,
): Promise<UserAccountDto> {const entity = await this.usersService.findOne({ username });if (!entity) {throw new UnauthorizedException('User not found');}if (entity.lockUntil && entity.lockUntil > Date.now()) {const diffInSeconds = Math.round((entity.lockUntil - Date.now()) / 1000);let message = `The account is locked. Please try again in ${diffInSeconds} seconds.`;if (diffInSeconds > 60) {const diffInMinutes = Math.round(diffInSeconds / 60);message = `The account is locked. Please try again in ${diffInMinutes} minutes.`;}throw new UnauthorizedException(message);}const passwordMatch = bcrypt.compareSync(password, entity.password);if (!passwordMatch) {// $inc update to increase failedLoginAttemptsconst update = {$inc: { failedLoginAttempts: 1 },};// lock account when the third try is failedif (entity.failedLoginAttempts + 1 >= 3) {// $set update to lock the account for 5 minutesupdate['$set'] = { lockUntil: Date.now() + 5 * 60 * 1000 };}await this.usersService.update(entity._id, update);throw new UnauthorizedException('Invalid password');}// if validation is sucessful, then reset failedLoginAttempts and lockUntilif (entity.failedLoginAttempts > 0 ||(entity.lockUntil && entity.lockUntil > Date.now())) {await this.usersService.update(entity._id, {$set: { failedLoginAttempts: 0, lockUntil: null },});}return { userId: entity._id, username } as UserAccountDto;
}

上面的代码是 auth.service.ts 文件里的一个方法 validateUser,主要用于验证登录时用户输入的账号密码是否正确。它包含的逻辑如下:

  1. 根据 username 查看用户是否存在,如果不存在则抛出 401 异常(也可以是 404 异常)
  2. 查看用户是否被锁定,如果被锁定则抛出 401 异常和相关的提示文字
  3. password 加密后和数据库中的密码进行对比,如果错误则抛出 401 异常(连续三次登录失败会被锁定账户 5 分钟)
  4. 如果登录成功,则将之前登录失败的计数记录进行清空(如果有)并返回用户 idusername 到下一阶段

可以看到 validateUser 方法包含了 4 个处理逻辑,我们需要对这 4 点都编写对应的单元测试代码,以确定整个 validateUser 方法功能是正常的。

第一个测试用例

在开始编写单元测试时,我们会遇到一个问题,findOne 方法需要和数据库进行交互,它要通过 username 查找数据库中是否存在对应的用户。但如果每一个单元测试都得和数据库进行交互,那测试起来会非常麻烦。所以可以通过 mock 假数据来实现这一点。

举例,假如我们已经注册了一个 woai3c 的用户,那么当用户登录时,在 validateUser 方法中能够通过 const entity = await this.usersService.findOne({ username }); 拿到用户数据。所以只要确保这行代码能够返回想要的数据,即使不和数据库交互也是没有问题的。而这一点,我们能通过 mock 数据来实现。现在来看一下 validateUser 方法的相关测试代码:

import { Test } from '@nestjs/testing';
import { AuthService } from '@/modules/auth/auth.service';
import { UsersService } from '@/modules/users/users.service';
import { UnauthorizedException } from '@nestjs/common';
import { TEST_USER_NAME, TEST_USER_PASSWORD } from '@tests/constants';describe('AuthService', () => {let authService: AuthService; // Use the actual AuthService typelet usersService: Partial<Record<keyof UsersService, jest.Mock>>;beforeEach(async () => {usersService = {findOne: jest.fn(),};const module = await Test.createTestingModule({providers: [AuthService,{provide: UsersService,useValue: usersService,},],}).compile();authService = module.get<AuthService>(AuthService);});describe('validateUser', () => {it('should throw an UnauthorizedException if user is not found', async () => {await expect(authService.validateUser(TEST_USER_NAME, TEST_USER_PASSWORD),).rejects.toThrow(UnauthorizedException);});// other tests...});
});

我们通过调用 usersServicefineOne 方法来拿到用户数据,所以需要在测试代码中 mock usersServicefineOne 方法:

 beforeEach(async () => {usersService = {findOne: jest.fn(), // 在这里 mock findOne 方法};const module = await Test.createTestingModule({providers: [AuthService, // 真实的 AuthService,因为我们要对它的方法进行测试{provide: UsersService, // 用 mock 的 usersService 代替真实的 usersService useValue: usersService,},],}).compile();authService = module.get<AuthService>(AuthService);});

通过使用 jest.fn() 返回一个函数来代替真实的 usersService.findOne()。如果这时调用 usersService.findOne() 将不会有任何返回值,所以第一个单元测试用例就能通过了:

it('should throw an UnauthorizedException if user is not found', async () => {await expect(authService.validateUser(TEST_USER_NAME, TEST_USER_PASSWORD),).rejects.toThrow(UnauthorizedException);
});

因为在 validateUser 方法中调用 const entity = await this.usersService.findOne({ username });findOne 是 mock 的假函数,没有返回值,所以 validateUser 方法中的第 2-4 行代码就能执行到了:

if (!entity) {throw new UnauthorizedException('User not found');
}

抛出 401 错误,符合预期。

第二个测试用例

validateUser 方法中的第二个处理逻辑是判断用户是否锁定,对应的代码如下:

if (entity.lockUntil && entity.lockUntil > Date.now()) {const diffInSeconds = Math.round((entity.lockUntil - Date.now()) / 1000);let message = `The account is locked. Please try again in ${diffInSeconds} seconds.`;if (diffInSeconds > 60) {const diffInMinutes = Math.round(diffInSeconds / 60);message = `The account is locked. Please try again in ${diffInMinutes} minutes.`;}throw new UnauthorizedException(message);
}

可以看到如果用户数据里有锁定时间 lockUntil 并且锁定结束时间大于当前时间就可以判断当前账户处于锁定状态。所以需要 mock 一个具有 lockUntil 字段的用户数据:

it('should throw an UnauthorizedException if the account is locked', async () => {const lockedUser = {_id: TEST_USER_ID,username: TEST_USER_NAME,password: TEST_USER_PASSWORD,lockUntil: Date.now() + 1000 * 60 * 5, // The account is locked for 5 minutes};usersService.findOne.mockResolvedValueOnce(lockedUser);await expect(authService.validateUser(TEST_USER_NAME, TEST_USER_PASSWORD),).rejects.toThrow(UnauthorizedException);
});

在上面的测试代码里,先定义了一个对象 lockedUser,这个对象里有我们想要的 lockUntil 字段,然后将它作为 findOne 的返回值,这通过 usersService.findOne.mockResolvedValueOnce(lockedUser); 实现。然后 validateUser 方法执行时,里面的用户数据就是 mock 出来的数据了,从而成功让第二个测试用例通过。

单元测试覆盖率

剩下的两个测试用例就不写了,原理都是一样的。如果剩下的两个测试不写,那么这个 validateUser 方法的单元测试覆盖率会是 50%,如果 4 个测试用例都写完了,那么 validateUser 方法的单元测试覆盖率将达到 100%。

单元测试覆盖率(Code Coverage)是一个度量,用于描述应用程序代码有多少被单元测试覆盖或测试过。它通常表示为百分比,表示在所有可能的代码路径中,有多少被测试用例覆盖。

单元测试覆盖率通常包括以下几种类型:

  • 行覆盖率(Lines):测试覆盖了多少代码行。
  • 函数覆盖率(Funcs):测试覆盖了多少函数或方法。
  • 分支覆盖率(Branch):测试覆盖了多少代码分支(例如,if/else 语句)。
  • 语句覆盖率(Stmts):测试覆盖了多少代码语句。

单元测试覆盖率是衡量单元测试质量的一个重要指标,但并不是唯一的指标。高的覆盖率可以帮助检测代码中的错误,但并不能保证代码的质量。覆盖率低可能意味着有未被测试的代码,可能存在未被发现的错误。

下图是 demo 项目的单元测试覆盖率结果:

在这里插入图片描述

像 service 和 controller 之类的文件,单元测试覆盖率一般尽量高点比较好,而像 module 这种文件就没有必要写单元测试了,也没法写,没有意义。上面的图片表示的是整个单元测试覆盖率的总体指标,如果你想查看某个函数的测试覆盖率,可以打开项目根目录下的 coverage/lcov-report/index.html 文件进行查看。例如我想查看 validateUser 方法具体的测试情况:

在这里插入图片描述

可以看到原来 validateUser 方法的单元测试覆盖率并不是 100%,还是有两行代码没有执行到,不过也无所谓了,不影响 4 个关键的处理节点,不要片面的追求高测试覆盖率。

编写E2E 测试

在单元测试中我们展示了如何为 validateUser() 的每一个功能点编写单元测试,并且使用了 mock 数据的方法来确保每个功能点都能够被测试到。而在 e2e 测试中,我们需要模拟真实的用户场景,所以要连接数据库来进行测试。因此,这次测试的 auth.service.ts 模块里的方法都会和数据库进行交互。

auth 模块主要有以下几个功能:

  • 注册
  • 登录
  • 刷新 token
  • 读取用户信息
  • 修改密码
  • 删除用户

e2e 测试需要将这六个功能都测试一遍,从注册开始,到删除用户结束。在测试时,我们可以建一个专门的测试用户来进行测试,测试完成后再删除这个测试用户,这样就不会在测试数据库中留下无用的信息了。

beforeAll(async () => {const moduleFixture: TestingModule = await Test.createTestingModule({imports: [AppModule],}).compile()app = moduleFixture.createNestApplication()await app.init()// 执行登录以获取令牌const response = await request(app.getHttpServer()).post('/auth/register').send({ username: TEST_USER_NAME, password: TEST_USER_PASSWORD }).expect(201)accessToken = response.body.access_tokenrefreshToken = response.body.refresh_token
})afterAll(async () => {await request(app.getHttpServer()).delete('/auth/delete-user').set('Authorization', `Bearer ${accessToken}`).expect(200)await app.close()
})

beforeAll 钩子函数将在所有测试开始之前执行,所以我们可以在这里注册一个测试账号 TEST_USER_NAMEafterAll 钩子函数将在所有测试结束之后执行,所以在这删除测试账号 TEST_USER_NAME 是比较合适的,还能顺便对注册和删除两个功能进行测试。

在上一节的单元测试中,我们编写了关于 validateUser 方法的相关单元测试。其实这个方法是在登录时执行的,用于验证用户账号密码是否正确。所以这一次的 e2e 测试也将使用登录流程来展示如何编写 e2e 测试用例。

整个登录测试流程总共包含了五个小测试:

describe('login', () => {it('/auth/login (POST)', () => {// ...})it('/auth/login (POST) with user not found', () => {// ...})it('/auth/login (POST) without username or password', async () => {// ...})it('/auth/login (POST) with invalid password', () => {// ...})it('/auth/login (POST) account lock after multiple failed attempts', async () => {// ...})})

这五个测试分别是:

  1. 登录成功,返回 200
  2. 如果用户不存在,抛出 401 异常
  3. 如果不提供密码或用户名,抛出 400 异常
  4. 使用错误密码登录,抛出 401 异常
  5. 如果账户被锁定,抛出 401 异常

现在我们开始编写 e2e 测试:

// 登录成功
it('/auth/login (POST)', () => {return request(app.getHttpServer()).post('/auth/login').send({ username: TEST_USER_NAME, password: TEST_USER_PASSWORD }).expect(200)
})// 如果用户不存在,应该抛出 401 异常
it('/auth/login (POST) with user not found', () => {return request(app.getHttpServer()).post('/auth/login').send({ username: TEST_USER_NAME2, password: TEST_USER_PASSWORD }).expect(401) // Expect an unauthorized error
})

e2e 的测试代码写起来比较简单,直接调用接口,然后验证结果就可以了。比如登录成功测试,我们只要验证返回结果是否是 200 即可。

前面四个测试都比较简单,现在我们看一个稍微复杂点的 e2e 测试,即验证账户是否被锁定。

it('/auth/login (POST) account lock after multiple failed attempts', async () => {const moduleFixture: TestingModule = await Test.createTestingModule({imports: [AppModule],}).compile()const app = moduleFixture.createNestApplication()await app.init()const registerResponse = await request(app.getHttpServer()).post('/auth/register').send({ username: TEST_USER_NAME2, password: TEST_USER_PASSWORD })const accessToken = registerResponse.body.access_tokenconst maxLoginAttempts = 3 // lock user when the third try is failedfor (let i = 0; i < maxLoginAttempts; i++) {await request(app.getHttpServer()).post('/auth/login').send({ username: TEST_USER_NAME2, password: 'InvalidPassword' })}// The account is locked after the third failed login attemptawait request(app.getHttpServer()).post('/auth/login').send({ username: TEST_USER_NAME2, password: TEST_USER_PASSWORD }).then((res) => {expect(res.body.message).toContain('The account is locked. Please try again in 5 minutes.',)})await request(app.getHttpServer()).delete('/auth/delete-user').set('Authorization', `Bearer ${accessToken}`)await app.close()
})

当用户连续三次登录失败的时候,账户就会被锁定。所以在这个测试里,我们不能使用测试账号 TEST_USER_NAME,因为测试成功的话这个账户就会被锁定,无法继续进行下面的测试了。我们需要再注册一个新用户 TEST_USER_NAME2,专门用来测试账户锁定,测试成功后再删除这个用户。所以你可以看到这个 e2e 测试的代码非常多,需要做大量的前置、后置工作,其实真正的测试代码就这几行:

// 连续三次登录
for (let i = 0; i < maxLoginAttempts; i++) {await request(app.getHttpServer()).post('/auth/login').send({ username: TEST_USER_NAME2, password: 'InvalidPassword' })
}// 测试账号是否被锁定
await request(app.getHttpServer()).post('/auth/login').send({ username: TEST_USER_NAME2, password: TEST_USER_PASSWORD }).then((res) => {expect(res.body.message).toContain('The account is locked. Please try again in 5 minutes.',)})

可以看到编写 e2e 测试代码还是相对比较简单的,不需要考虑 mock 数据,不需要考虑测试覆盖率,只要整个系统流程的运转情况符合预期就可以了。

应不应该写测试

如果有条件的话,我是比较建议大家写测试的。因为写测试可以提高系统的健壮性、可维护性和开发效率。

提高系统健壮性

我们一般编写代码时,会关注于正常输入下的程序流程,确保核心功能正常运作。但是一些边缘情况,比如异常的输入,这些我们可能会经常忽略掉。但当我们开始编写测试时,情况就不一样了,这会逼迫你去考虑如何处理并提供相应的反馈,从而避免程序崩溃。可以说写测试实际上是在间接地提高系统健壮性。

提高可维护性

当你接手一个新项目时,如果项目包含完善的测试,那将会是一件很幸福的事情。它们就像是项目的指南,帮你快速把握各个功能点。只看测试代码就能够轻松地了解每个功能的预期行为和边界条件,而不用你逐行的去查看每个功能的代码。

提高开发效率

想象一下,一个长时间未更新的项目突然接到了新需求。改了代码后,你可能会担心引入 bug,如果没有测试,那就需要重新手动测试整个项目——浪费时间,效率低下。而有了完整的测试,一条命令就能得知代码更改有没有影响现有功能。即使出错了,也能够快速定位,找到问题点。

什么时候不建议写测试?

短期项目需求迭代非常快的项目不建议写测试。比如某些活动项目,活动结束就没用了,这种项目就不需要写测试。另外,需求迭代非常快的项目也不要写测试,我刚才说写测试能提高开发效率是有前提条件的,就是功能迭代比较慢的情况下,写测试才能提高开发效率。如果你的功能今天刚写完,隔一两天就需求变更了要改功能,那相关的测试代码都得重写。所以干脆就别写了,靠团队里的测试人员测试就行了,因为写测试是非常耗时间的,没必要自讨苦吃。

根据我的经验来看,国内的绝大多数项目(尤其是政企类项目)都是没有必要写测试的,因为需求迭代太快,还老是推翻之前的需求,代码都得加班写,那有闲情逸致写测试。

总结

在细致地讲解了如何为 Nestjs 项目编写单元测试及 e2e 测试之后,我还是想重申一下测试的重要性,它能够提高系统的健壮性、可维护性和开发效率。如果没有机会写测试,我建议大家可以自己搞个练习项目来写,或者说参加一些开源项目,给这些项目贡献代码,因为开源项目对于代码要求一般都比较严格。贡献代码可能需要编写新的测试用例或修改现有的测试用例。

最后,再推荐一下我的其他文章,如果你有兴趣,不妨一读:

  • 带你入门前端工程
  • 从零开始实现一个玩具版浏览器渲染引擎
  • 手把手教你写一个简易的微前端框架
  • 前端监控 SDK 的一些技术要点原理分析
  • 可视化拖拽组件库一些技术要点原理分析
  • 可视化拖拽组件库一些技术要点原理分析(二)
  • 可视化拖拽组件库一些技术要点原理分析(三)
  • 可视化拖拽组件库一些技术要点原理分析(四)
  • 低代码与大语言模型的探索实践
  • 前端性能优化 24 条建议(2020)
  • 手把手教你写一个脚手架
  • 手把手教你写一个脚手架(二)

参考资料

  • NestJS: A framework for building efficient, scalable Node.js server-side applications.
  • MongoDB: A NoSQL database used for data storage.
  • Jest: A testing framework for JavaScript and TypeScript.
  • Supertest: A library for testing HTTP servers.

相关文章:

如何为 Nestjs 编写单元测试和 E2E 测试

前言 最近在给一个 nestjs 项目写单元测试&#xff08;Unit Testing&#xff09;和 e2e 测试&#xff08;End-to-End Testing&#xff0c;端到端测试&#xff0c;简称 e2e 测试&#xff09;&#xff0c;这是我第一次给后端项目写测试&#xff0c;发现和之前给前端项目写测试还…...

基于Python的LSTM网络实现单特征预测回归任务(TensorFlow)

单特征&#xff1a;数据集中只包含2列&#xff0c;时间列价格列&#xff0c;仅利用价格来预测价格 目录 一、数据集 二、任务目标 三、代码实现 1、从本地路径中读取数据文件 2、数据归一化 3、创建配置类&#xff0c;将LSTM的各个超参数声明为变量&#xff0c;便于后续…...

Spring - 8 ( 10000 字 Spring 入门级教程 )

一&#xff1a; MyBatis 1.1 引入 MyBatis 我们学习 MySQL 数据库时&#xff0c;已经学习了 JDBC 来操作数据库, 但是 JDBC 操作太复杂了. 我们先来回顾⼀下 JDBC 的操作流程: 创建数据库连接池 DataSource通过 DataSource 获取数据库连接 Connection编写要执行带 ? 占位符…...

鸿蒙内核源码分析(忍者ninja篇) | 都忍者了能不快吗

ninja | 忍者 ninja是一个叫 Evan Martin的谷歌工程师开源的一个自定义的构建系统,最早是用于 chrome的构建,Martin给它取名 ninja(忍者)的原因是因为它strikes quickly(快速出击).这是忍者的特点,可惜Martin不了解中国文化,不然叫小李飞刀更合适些.究竟有多块呢? 用Martin自…...

Linux——守护进程化(独立于用户会话的进程)

目录 前言 一、进程组ID与会话ID 二、setsid() 创建新会话 三、daemon 守护进程 前言 在之前&#xff0c;我们学习过socket编程中的udp通信与tcp通信&#xff0c;但是当时我们服务器启动的时候&#xff0c;都是以前台进程的方式启动的&#xff0c;这样很不优雅&#xff0c…...

安卓开发--按键跳转页面,按键按下变色

前面已经介绍了一个空白按键工程的建立以及响应方式&#xff0c;可以参考这里&#xff1a;安卓开发–新建工程&#xff0c;新建虚拟手机&#xff0c;按键事件响应。 安卓开发是页面跳转是基础&#xff01;&#xff01;&#xff01;所以本篇博客介绍利用按键实现页面跳转&#…...

Ps基础学习笔记

Ps基础学习笔记 Adobe Photoshop&#xff08;简称Ps&#xff09;是一款非常流行的图像处理软件&#xff0c;被广泛应用于图像编辑、修饰和设计等领域。作为一名初学者&#xff0c;了解Ps的基础知识是非常重要的&#xff0c;本文将介绍Ps的基本操作和常用工具&#xff0c;帮助你…...

spring开发问题总结(持续更新)

开始 最近在做项目的时候&#xff0c;总遇到一些大小不一&#xff0c;奇形怪状的问题。 现在终于有时间来总结一下遇到的问题&#xff0c;以备复习之用。 以下提到的问题经过简化&#xff0c;不代表任何项目代码或问题。 问题1&#xff1a;未完成任务状态搜索结果有误&#x…...

Android 状态栏WiFi图标的显示逻辑

1. 状态栏信号图标 1.1 WIFI信号显示 WIFI信号在状态栏的显示如下图所示 当WiFi状态为关闭时&#xff0c;状态栏不会有任何显示。当WiFi状态打开时&#xff0c;会如上图所示&#xff0c;左侧表示有可用WiFi&#xff0c;右侧表示当前WiFi打开但未连接。 当WiFi状态连接时&#x…...

更改 DeepXDE 的后端

DeepXDE 库为科学计算和工程优化等领域提供了深度学习方法&#xff0c;是一个非常有用的工具。其中一个重要的功能是它允许用户自定义后端。在本文中&#xff0c;我们将指导如何更改 DeepXDE 的后端&#xff0c;并且验证更改是否成功。 更改 DeepXDE 的后端 DeepXDE 支持多种…...

SpringBoot之Zuul服务

概述 Spring Cloud Netflix zuul组件是微服务架构中的网关组件,Zuul作为统一网关,是所有访问该平台的请求入口,核心功能是路由和过滤。 目前公司业务就是基于Zuul搭建的网关服务,且提供的服务包括转发请求(路由)、黑名单IP访问拦截、URL资源访问时的权限拦截、统一访问日志记…...

Go-变量

可以理解为一个昵称 以后这个昵称就代指这些信息 var sg string "czy" 声明赋值 package mainimport "fmt"func main() {var sg string "陈政洋"fmt.Println(sg)var age int 73fmt.Println(age)var flag bool truefmt.Println(flag) } …...

【CTF-Crypto】RSA-选择明密文攻击 一文通

RSA&#xff1a;选择明密文攻击 关于选择明/密文攻击&#xff0c;其实这一般是打一套组合拳的&#xff0c;在网上找到了利用的思路&#xff0c;感觉下面这个题目是真正将这个问题实现了&#xff0c;所以还是非常棒的一道题&#xff0c;下面先了解一下该知识点&#xff1a;(来自…...

Pytorch基础:torch.expand() 和 torch.repeat()

在torch中&#xff0c;如果要改变某一个tensor的维度&#xff0c;可以利用view、expand、repeat、transpose和permute等方法&#xff0c;这里对这些方法的一些容易混淆的地方做个总结。 expand和repeat函数是pytorch中常用于进行张量数据复制和维度扩展的函数&#xff0c;但其…...

如何正确安装Scrapy 2.6.1并解决常见的Python环境问题

在配置Python环境和安装包时&#xff0c;常常会遇到版本冲突和路径问题&#xff0c;特别是当系统中存在多个Python版本时。本文将指导你如何在CentOS系统中正确使用pip3安装Scrapy 2.6.1&#xff0c;并解决一些常见的环境问题。 步骤1: 确认和升级 pip3 确认 pip3 的版本&…...

阵痛中的乳业产业,何时才能成为下一个啤酒产业?

说起饮品&#xff0c;近年来中国啤酒业中各大品牌齐齐聚焦高端化的趋势绝对值得一提。然而&#xff0c;与之相反&#xff0c;国内乳业却是仍未进入高端化阶段&#xff0c;甚至陷入了周期底部中。 图源&#xff1a;中国圣牧财报 增收降利 牧企承受巨大的供需缺口压力 从产业链…...

关于模型参数融合的思考

模型参数融合通常指的是在训练过程中或训练完成后将不同模型的参数以某种方式结合起来&#xff0c;以期望得到更好的性能。这种融合可以在不同的层面上进行&#xff0c;例如在神经网络的不同层之间&#xff0c;或者是在完全不同的模型之间。模型参数融合的目的是结合不同模型的…...

Windows MySQL本地服务器设置并导入数据库和数据

文章目录 小结问题及解决导出数据库Windows MySQL本地服务器设置导入数据库和数据 参考 小结 最近需要在本地Windows环境中设置MySQL服务器&#xff0c;并导入数据库和数据&#xff0c;记录过程。 问题及解决 导出数据库 首先需要导出数据库&#xff1a; C:\mysql-8.0.37-…...

豪投巨资,澳大利亚在追逐海市蜃楼吗?

澳大利亚政府正在积极投资于量子计算领域。继2021年向量子技术投资逾1亿澳元后&#xff0c;2023年5月&#xff0c;该国发布了首个国家量子战略&#xff0c;详细阐述了如何把握量子技术的未来及保持全球领先地位。 澳大利亚的国家量子战略概述 原文链接&#xff1a; https://ww…...

面试集中营—Redis架构篇

一、Redis到底是多线程还是单线程 1、redis6.0版本之前的单线程&#xff0c;是指网络请求I/O与数据的读写是由一个线程完成的&#xff1b; 2、redis6.0版本升级成了多线程&#xff0c;指的是在网络请求I/O阶段应用的多线程技术&#xff1b;而键值对的读写还是由单线程完成的。所…...

05_kafka-整合springboot

文章目录 kafka 整合 springboot pom.xml <parent><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-parent</artifactId><version>2.1.5.RELEASE</version> </parent> <dependencies>&…...

论UML在学情精准测评系统中的应用

摘要简介 项目背景&#xff1a; 随着教育改革的不断深入&#xff0c;对学生学情的精准测评成为教育教学工作中的重要环节。为了解决传统学情测评方式主观性强、效率低、反馈不及时等问题&#xff0c;我们团队受教育主管部门委托&#xff0c;承担了中小学学情精准测评系统&…...

Day23 代码随想录打卡|字符串篇---重复的子字符串

题目&#xff08;leecode T459&#xff09;&#xff1a; 给定一个非空的字符串 s &#xff0c;检查是否可以通过由它的一个子串重复多次构成。给定的字符串只含有小写英文字母&#xff0c;并且长度不超过10000。fang 移动匹配。分析可以由自己的子串构成的字符串&#xff0c;肯…...

【win10 文件夹数量和看到不一致查看隐藏文件已经打开,Thumb文件作妖】

目录 任务介绍&#xff1a;重命名规则修改前修改后 实现思路VB代码实现BUG犯罪现场&#xff08;眼见不一定为实&#xff09;破案1&#xff1a;抓顶风作案的反贼&#xff01;&#xff01;&#xff01;破案2&#xff1a;破隐身抓刺客&#xff01;&#xff01;&#xff01;杀器&am…...

ctfshow web入门 sql注入 web224--web233

web224 扫描后台&#xff0c;发现robots.txt&#xff0c;访问发现/pwdreset.php &#xff0c;再访问可以重置密码 &#xff0c;登录之后发现上传文件 检查发现没有限制诶 上传txt,png,zip发现文件错误了 后面知道群里有个文件能上传 <? _$GET[1]_?>就是0x3c3f3d60245…...

「Java开发指南」如何用MyEclipse搭建GWT 2.1和Spring?(一)

本教程将指导您如何生成一个可运行的Google Web Toolkit (GWT) 2.1和Spring应用程序&#xff0c;该应用程序为域模型实现了CRUD应用程序模式。在本教程中&#xff0c;您将学习如何&#xff1a; 安装Google Eclipse插件为GWT配置一个项目搭建从数据库表到一个现有的项目GWT编译…...

python同时进行字符串的多种替换

一些常见的方法&#xff1a; 使用str.replace()方法&#xff1a;这是一种简单的方法&#xff0c;但是如果你有多个替换需要进行&#xff0c;可能会变得很繁琐。 text "This is a sample text with some words." text text.replace("sample", "exa…...

【Java基础题型】用筛法求之N内的素数(老题型)

输入格式 N输出格式 0&#xff5e;N的素数样例输入 100样例输出 2 3 5 7 11 13 17 19 23 29 31 37 老朋友素数了属于是&#xff01; 方法1&#xff1a;(穷举法) 通过遍历 i 的所有除数&#xff0c;如果除以除数后商变成了0&#xff0c;那么把布尔值变成假的。表示不是素数 【…...

Linux进程——Linux环境变量

前言&#xff1a;在结束完上一篇的命令行参数时&#xff0c;我们简单的了解了一下Linux中的环境变量PATH&#xff0c;而环境变量不只有PATH&#xff0c;关于更多环境变量的知识我们将在本篇展开&#xff01; 本篇主要内容&#xff1a; 常见的环境变量 获取环境变量的三种方式 本…...

SRM系统供应链库存协同提升企业服务水平

SRM系统供应链库存协同是一种以提高供应链整体效率和竞争力为目标的管理方法。它涉及到企业与供应商之间的紧密合作&#xff0c;以实现库存优化、成本降低、风险分担和灵活响应市场变化等目标。 一、SRM供应链库存协同的概念和特点 SRM供应链库存协同是指企业与供应商之间通过…...

swoole wordpress/seo优化怎么做

win7只能上qq不能上网怎么解决_网站服务器运行维护win7只能上qq不能上网解决方法&#xff1a;1、在开始菜单中打开运行&#xff0c;然后运行cmd&#xff0c;输入netsh winsock reset命令&#xff0c;然后按提示重启电脑。2、把DNS设置为自动。1、插入u盘&#xff0c;执行如下命…...

百度注册域名免费建站/如何做一个网页

1. ODBC 插件使用场景 在使用 DolphinDB 的业务场景中&#xff0c;我们有从其他数据源和 DolphinDB 之间同步数据的需求。比如从 Oracle 读数据写入到 DolphinDB&#xff0c;或者从 DolphinDB 写数据到 Oracle 数据库。 按照频率的不同&#xff0c;数据同步可以分为实时和离线…...

网站第三方评价如何做/想卖产品怎么推广宣传

最近在为公司实施做了一个工具,Silverlight部署早已是轻车熟路, 但对于非技术人员来说却很是头疼的一件事,当到现场实施碰到客户情况也各不相同, 急需一个类似系统备份的"一键工具"快速实现应用程序部署和数据库进行关联. 网上关于这方面资源也比较混乱,其中对于IIS的…...

wordpress 删除图片/重庆网络推广外包

简单的来说就是为了方便查找和导航代码用的。 下面举例如何快速的定位到我已经标识过的代码。#pragma mark 播放节拍器- (void) Run:(NSNumber *)tick{ //... } OK,那么如何查找呢&#xff0c;点击代码编辑器上面的导航栏即可&#xff1a;接着我修改一下代码&#xff1a;#pragm…...

品牌产品网站怎么做/app拉新平台

关于单元测试的一些问题 当我们Javaweb项目中编写单元测试的时候&#xff0c;通常会面临一个普遍的问题&#xff1a;需要测试的类会有很多依赖&#xff0c;而这些依赖的类或者对象又会有很多别的依赖&#xff0c;导致我们在写单元测试的时候几乎需要把完整的业务体系代码编写出…...

做网站要有什么团队/一键优化清理手机

源地址&#xff1a;http://mina.apache.org/mina-project/userguide/ch17-spring-integration/ch17-spring-integration.html 一个mina应用主要包括一下方面&#xff1a; One HandlerTwo Filter - Logging Filter and a ProtocolCodec FilterNioDatagram Socket一个mina 服务端…...