前端测试与Next.js项目开发全解析
1. 前端测试相关内容
1.1 模拟API请求测试
在前端开发中,模拟API请求测试是很重要的。例如,在测试加入时事通讯功能时,我们可以使用Mock Service Worker库来模拟API请求。以下是相关代码:
it('Should show an error if the request to join the newsletter fails.', async () => {
server.use(
rest.post(JOIN_NEWSLETTER_URL, (req, res, ctx) => {
return res(ctx.status(500))
})
)
const { nameInput, emailInput, submitBtn } = renderNewsletterForm()
fireEvent.change(nameInput, {
target: {
value: 'Thomas',
},
})
fireEvent.change(emailInput, {
target: {
value: 'myemail@test.com',
},
})
fireEvent.click(submitBtn)
await waitFor(() => screen.getByText('Joining...'))
await waitFor(() =>
screen.getByText(
'There was a problem while signing you up for the newsletter. Please try again.'
)
)
})
这里通过
ctx.status(500)
模拟请求失败的情况,当表单提交时,时事通讯组件会显示错误信息。
1.2 Cypress端到端测试
Cypress是一个优秀的端到端测试工具,适合处理大量API请求的动态应用。以下是使用Cypress测试用户注册表单的详细步骤:
1.
环境准备
- 切换到指定分支:
chapter/testing/cypress-start
。
- 安装依赖:
npm install
。
- 安装Cypress:
npm install --save-dev cypress
。
- 安装Cypress Testing Library:
npm install --save-dev @testing-library/cypress
。
- 在
cypress/support/commands.ts
文件中导入测试库命令:
import '@testing-library/cypress/add-commands'
- 更新`cypress/tsconfig.json`文件:
{
"extends": "../tsconfig.json",
"compilerOptions": {
"noEmit": true,
"types": ["cypress", "@testing-library/cypress"],
"isolatedModules": false
},
"include": [
"../node_modules/cypress",
"../node_modules/@testing-library/cypress",
"./**/*.ts"
]
}
-
创建测试组件
-
创建
useStepper钩子:
-
创建
import { useCallback, useState } from 'react'
export const useStepper = (initialStep = 1) => {
const [step, setStep] = useState(initialStep)
const goToNextStep = useCallback(() => setStep((step) => step + 1), [])
const goToPrevStep = useCallback(() => setStep((step) => step - 1), [])
return {
step,
setStep,
goToNextStep,
goToPrevStep,
}
}
- 创建`RegistrationForm`组件:
import React, { useState } from 'react'
import { useStepper } from '@/hooks/useStepper'
import styles from './registrationForm.module.css'
type RegistrationFormProps = {}
const RegistrationForm = (props: RegistrationFormProps) => {
const { step, goToNextStep, goToPrevStep } = useStepper()
const [form, setForm] = useState({
name: '',
surname: '',
address: '',
city: '',
email: '',
password: '',
})
const [registerApiStatus, setRegisterApiStatus] = useState<
'IDLE' | 'PENDING' | 'SUCCESS' | 'ERROR'
>('IDLE')
const onFieldChange = (e: React.ChangeEvent<HTMLInputElement>) => {
const { name, value } = e.target
setForm((formState) => ({ ...formState, [name]: value }))
}
const onSubmit = async (e: React.FormEvent<HTMLFormElement>) => {
e.preventDefault()
try {
setRegisterApiStatus('PENDING')
await fetch('/post-user', {
method: 'post',
body: JSON.stringify(form),
})
setRegisterApiStatus('SUCCESS')
} catch (error) {
setRegisterApiStatus('ERROR')
}
}
return (
<div>
<div className="container mx-auto py-8">
<div className="w-2/3 mx-auto shadow p-5">
{registerApiStatus === 'SUCCESS' ? (
<div>Welcome new user!</div>
) : (
<form className="" onSubmit={onSubmit}>
<h2 className="mb-6 text-2xl font-semibold">Register form</h2>
<div className="mb-4">
{step === 1 ? (
<>
<div className={styles.formBlock}>
<label htmlFor="name">Name</label>
<input
className={styles.inputField}
value={form.name}
id="name"
onChange={onFieldChange}
/>
</div>
<div className={styles.formBlock}>
<label htmlFor="surname">Surname</label>
<input
className={styles.inputField}
value={form.surname}
id="surname"
onChange={onFieldChange}
/>
</div>
</>
) : null}
{step === 2 ? (
<>
<div className={styles.formBlock}>
<label htmlFor="address">Address</label>
<input
className={styles.inputField}
value={form.address}
id="address"
onChange={onFieldChange}
/>
</div>
<div className={styles.formBlock}>
<label htmlFor="city">City</label>
<input
className={styles.inputField}
value={form.city}
id="city"
onChange={onFieldChange}
/>
</div>
</>
) : null}
{step === 3 ? (
<>
<div className={styles.formBlock}>
<label htmlFor="email">Email</label>
<input
className={styles.inputField}
value={form.email}
id="email"
onChange={onFieldChange}
/>
</div>
<div className={styles.formBlock}>
<label htmlFor="password">Password</label>
<input
className={styles.inputField}
value={form.password}
id="password"
onChange={onFieldChange}
/>
</div>
</>
) : null}
</div>
<div className="flex justify-between mt-8">
<div>
{step > 1 ? (
<button
type="button"
className={styles.stepBtn}
onClick={goToPrevStep}
>
Previous
</button>
) : null}
</div>
<div>
{step < 3 ? (
<button
type="button"
className={styles.stepBtn}
onClick={goToNextStep}
>
Next
</button>
) : null}
{step === 3 ? (
<button type="submit" className={styles.submitBtn}>
Submit
</button>
) : null}
</div>
</div>
</form>
)}
</div>
</div>
</div>
)
}
export default RegistrationForm
- 表单样式:
.formBlock {
@apply flex flex-col justify-between items-start mb-4 space-y-2;
}
.inputField {
@apply border w-full px-3 py-2;
}
.stepBtn {
@apply px-4 py-3 bg-teal-100 text-teal-900 font-semibold cursor-pointer hover:bg-teal-200;
}
.submitBtn {
@apply px-4 py-3 text-teal-100 bg-teal-900 font-semibold cursor-pointer hover:bg-teal-800;
}
-
编写测试用例
-
创建测试数据文件
cypress/fixtures/userRegistrationData.json:
-
创建测试数据文件
{
"name": "John",
"surname": "Smith",
"address": "15 Oakland Street",
"city": "London",
"email": "johnsmith@gmail.com",
"password": "qwerty"
}
- 编写测试代码`cypress/integration/registerForm.ts`:
// Helper to go to the next step
const goNext = () => cy.findByText('Next').click()
type UserData = {
name: string
surname: string
address: string
city: string
email: string
password: string
}
describe('User Registration', () => {
beforeEach(() => {
// Load fixture for each test
cy.fixture('userRegistrationData.json').as('userData')
})
it('Visits the page', () => {
cy.visit('http://localhost:3000')
cy.findByText('Register form').should('exist')
})
it('Fill in the form', () => {
cy.get<UserData>('@userData').then((user) => {
cy.findByLabelText('Name').type(user.name)
cy.findByLabelText('Surname').type(user.surname)
goNext()
cy.findByLabelText('Address').type(user.address)
cy.findByLabelText('City').type(user.city)
goNext()
cy.findByLabelText('Email').type(user.email)
cy.findByLabelText('Password').type(user.password)
})
})
it('Submit the form', () => {
// Intercep post-user request so we can check the body
cy.intercept('POST', '/post-user').as('postUser')
// Submit the form
cy.findByText('Submit').click()
// Wait for the post request and get the request object
cy.wait('@postUser').then(({ request }) => {
// Get user
cy.get<UserData>('@userData').then((user) => {
// Check if request body matches with user fixture
expect(JSON.parse(request.body)).to.eql(user)
})
})
// Welcome message should be displayed
cy.findByText('Welcome new user!').should('exist')
})
})
1.3 测试小贴士
以下是一些有用的测试小贴士:
- 浅挂载时要格外小心,因为浅挂载会存根所有子组件,查询子组件渲染的元素可能会失败。
- 编写测试时,专注于与DOM输出交互,避免测试实现细节。
- 根据项目需求选择测试类型,尽量避免测试实现细节。
- 100%测试覆盖率并非总是必要的,要权衡利弊。
- 编写测试时模仿用户行为,避免让测试成为应用的第三种消费者。
- 使用
data-testid
一致地抓取元素。
- 不要测试第三方库,它们应有自己的测试套件。
- 单元测试可模拟API请求,端到端测试尽量避免模拟API请求。
2. Next.js项目开发
2.1 Next.js项目搭建
Next.js提供了CLI工具
Create Next App
来搭建项目。可以使用以下命令创建项目:
npx create-next-app@latest --typescript
# or
yarn create next-app --typescript
创建项目后,会生成一些文件和目录:
| 文件/目录 | 说明 |
| ---- | ---- |
|
.next
| 存储配置和缓存文件 |
|
pages
| 创建页面组件 |
|
public
| 存放公共文件 |
|
styles
| 存放CSS样式 |
|
next-env.d.ts
| 自动生成的Next.js类型文件 |
|
next.config.js
| Next.js应用配置文件 |
package.json
文件会新增四个脚本:
| 脚本 | 说明 |
| ---- | ---- |
|
"dev": "next dev"
| 启动开发环境 |
|
"build": "next build"
| 构建生产包并生成HTML文件 |
|
"start": "next start"
| 启动生产服务器 |
|
"lint": "next lint"
| 运行内置ESLint配置 |
2.2 Next.js页面与路由
Next.js使用基于文件系统的路由,通过遍历
pages
目录为每个文件创建路由。例如:
-
pages/index.tsx
-
localhost:3000
-
pages/about.tsx
-
localhost:3000/about
-
pages/user/profile.tsx
-
localhost:3000/user/profile
以下是页面文件示例:
// pages/index.tsx
import type { NextPage } from "next";
const Home: NextPage = () => {
return <div>Home</div>;
};
export default Home;
// pages/about.tsx
import type { NextPage } from "next";
type AboutProps = {};
const About: NextPage = (props: AboutProps) => {
return <div>About</div>;
};
export default About;
// pages/user/profile.tsx
import type { NextPage } from "next";
type ProfileProps = {};
const Profile: NextPage = (props: ProfileProps) => {
return <div>Profile</div>;
};
export default Profile;
为了实现页面导航,我们可以创建一个头部组件:
// components/header/Header.tsx
import Link from "next/link";
import styles from "./Header.module.css";
type HeaderProps = {};
const Header = (props: HeaderProps) => {
return (
<header className={styles.header}>
<div>The Road To Enterprise</div>
<nav className={styles.nav}>
<Link href="/">Home</Link>
<Link href="/about">About</Link>
<Link href="/user/profile">Profile</Link>
</nav>
</header>
);
};
export default Header;
头部组件样式:
.header {
max-width: 1180px;
margin: 0 auto;
padding: 1rem 2rem;
display: flex;
justify-content: space-between;
}
.nav {
display: flex;
justify-content: space-around;
width: 12rem;
}
将头部组件添加到
pages/_app.tsx
文件中:
import "../styles/globals.css";
import type { AppProps } from "next/app";
import Header from "../components/header/Header";
function MyApp({ Component, pageProps }: AppProps) {
return (
<div>
<Header />
<Component {...pageProps} />
</div>
);
}
2.3 开发流程总结
以下是Next.js项目开发的流程:
graph LR
A[创建Next.js项目] --> B[配置项目环境]
B --> C[创建页面组件]
C --> D[实现页面路由]
D --> E[添加页面导航]
E --> F[开发功能并测试]
综上所述,前端测试和Next.js项目开发是前端开发中重要的部分。通过合理运用测试工具和Next.js框架,可以提高项目的质量和可维护性。
2.4 静态站点生成(SSG)、增量静态再生(ISR)和服务器端渲染(SSR)
2.4.1 问题背景
单页应用(SPA)在用户访问时,最初会返回几乎为空的HTML文件,这对于搜索引擎优化和社交媒体爬虫来说是个问题。例如,社交媒体分享链接时可能无法获取到有效信息,或者当网站内容过大、加载过慢时,爬虫可能无法获取完整的预览内容。
2.4.2 解决方案
为了解决这些问题,可以使用静态站点生成(SSG)和服务器端渲染(SSR)技术来预渲染页面,让用户或爬虫收到完整的HTML文件。Next.js是一个流行的React框架,提供了SSG和SSR功能。
2.4.3 SSG和SSR的应用
-
SSG
:在构建时生成每个页面的HTML文件。在Next.js中,运行
"build": "next build"命令会进行SSG,为每个页面生成HTML文件。 -
SSR
:在请求时动态渲染页面。运行
"start": "next start"命令会启动生产服务器进行SSR。
2.4.4 增量静态再生(ISR)
ISR允许在构建后更新静态生成的页面。可以通过设置重新验证时间来定期更新页面内容,这样既可以享受SSG的性能优势,又能保持页面内容的时效性。
2.5 服务器less函数和API端点
2.5.1 服务器less函数
Next.js支持服务器less函数,这些函数可以在服务器端运行,处理复杂的逻辑,而无需管理服务器基础设施。
2.5.2 添加API端点
可以在
pages/api
目录下创建API路由。例如,创建一个
pages/api/hello.ts
文件:
// pages/api/hello.ts
import type { NextApiRequest, NextApiResponse } from 'next'
export default function handler(req: NextApiRequest, res: NextApiResponse) {
res.status(200).json({ name: 'John Doe' })
}
访问
localhost:3000/api/hello
即可获取JSON响应。
2.6 中间件限制页面访问
2.6.1 中间件的作用
中间件可以用于限制对特定页面的访问,例如进行身份验证、权限检查等。
2.6.2 添加中间件
在Next.js中,可以在
pages/_middleware.ts
文件中添加中间件。以下是一个简单的示例:
// pages/_middleware.ts
import { NextResponse } from 'next/server'
import type { NextRequest } from 'next/server'
export function middleware(request: NextRequest) {
// 简单的身份验证示例
const isAuthenticated = request.cookies.get('isAuthenticated')
if (!isAuthenticated && request.nextUrl.pathname.startsWith('/protected')) {
return NextResponse.redirect(new URL('/login', request.url))
}
return NextResponse.next()
}
export const config = {
matcher: ['/protected/:path*'],
}
这个中间件会检查用户是否已认证,如果未认证且访问
/protected
路径下的页面,会重定向到登录页面。
2.7 总结与展望
2.7.1 总结
- 前端测试 :通过模拟API请求和使用Cypress进行端到端测试,可以确保代码的正确性和稳定性。遵循测试小贴士可以提高测试的质量和可维护性。
- Next.js开发 :Next.js提供了便捷的项目搭建、页面路由、SSG、SSR、ISR、服务器less函数和中间件等功能,有助于构建高性能、可维护的React应用。
2.7.2 展望
随着前端技术的不断发展,测试工具和框架也会不断更新。例如,Vitest是一个快速的单元测试框架,值得关注。同时,Next.js也在不断发展,未来可能会提供更多强大的功能。
以下是Next.js开发中不同功能的对比表格:
| 功能 | 描述 | 实现方式 |
| ---- | ---- | ---- |
| 页面路由 | 根据文件系统创建路由 | 在
pages
目录下创建文件 |
| SSG | 构建时生成HTML文件 | 运行
next build
|
| SSR | 请求时动态渲染页面 | 运行
next start
|
| ISR | 构建后更新静态页面 | 设置重新验证时间 |
| 服务器less函数 | 处理复杂逻辑 | 在
pages/api
目录下创建文件 |
| 中间件 | 限制页面访问 | 在
pages/_middleware.ts
文件中编写逻辑 |
Next.js项目开发的整体流程可以用以下mermaid流程图表示:
graph LR
A[创建Next.js项目] --> B[配置项目环境]
B --> C[创建页面组件]
C --> D[实现页面路由]
D --> E[选择渲染方式(SSG/SSR/ISR)]
E --> F[添加API端点和服务器less函数]
F --> G[设置中间件]
G --> H[开发功能并测试]
通过掌握前端测试和Next.js项目开发的相关知识和技能,可以更好地应对前端开发中的各种挑战,开发出高质量、高性能的Web应用。
超级会员免费看

被折叠的 条评论
为什么被折叠?



