23、前端测试与Next.js项目开发全解析

前端测试与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"
  ]
}
  1. 创建测试组件
    • 创建 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;
}
  1. 编写测试用例
    • 创建测试数据文件 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应用。

评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符  | 博主筛选后可见
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值