您现在的位置是:网站首页 > 单元测试与集成测试文章详情

单元测试与集成测试

单元测试与集成测试的基本概念

单元测试是针对软件中最小的可测试单元进行的测试,通常是函数或方法。集成测试则是将多个单元组合在一起进行测试,验证它们之间的交互是否正确。在Express框架中,单元测试可能针对单个路由处理函数,而集成测试则可能测试整个路由与中间件的配合。

// 单元测试示例:测试一个简单的工具函数
function add(a, b) {
  return a + b;
}

// 集成测试示例:测试Express路由
const express = require('express');
const app = express();

app.get('/sum', (req, res) => {
  const result = add(Number(req.query.a), Number(req.query.b));
  res.json({ result });
});

Express中的单元测试实践

在Express应用中,单元测试通常关注以下几个方面:

  1. 路由处理函数的独立测试
  2. 中间件函数的测试
  3. 工具类和辅助函数的测试

使用Jest进行单元测试的典型示例:

// userController.js
exports.getUser = (req, res) => {
  const user = { id: req.params.id, name: 'Test User' };
  res.status(200).json(user);
};

// userController.test.js
const { getUser } = require('./userController');
const mockRequest = (params) => ({ params });
const mockResponse = () => {
  const res = {};
  res.status = jest.fn().mockReturnValue(res);
  res.json = jest.fn().mockReturnValue(res);
  return res;
};

test('getUser should return user data', () => {
  const req = mockRequest({ id: '123' });
  const res = mockResponse();
  
  getUser(req, res);
  
  expect(res.status).toHaveBeenCalledWith(200);
  expect(res.json).toHaveBeenCalledWith({
    id: '123',
    name: 'Test User'
  });
});

Express中的集成测试方法

集成测试需要启动Express应用并模拟真实请求。Supertest是专门为HTTP服务器测试设计的库:

const request = require('supertest');
const app = require('../app');

describe('GET /api/users', () => {
  it('should return list of users', async () => {
    const response = await request(app)
      .get('/api/users')
      .expect('Content-Type', /json/)
      .expect(200);
    
    expect(response.body).toHaveLength(3);
    expect(response.body[0]).toHaveProperty('id');
    expect(response.body[0]).toHaveProperty('name');
  });

  it('should create new user', async () => {
    const newUser = { name: 'New User' };
    const response = await request(app)
      .post('/api/users')
      .send(newUser)
      .expect(201);
    
    expect(response.body).toMatchObject({
      id: expect.any(String),
      name: 'New User'
    });
  });
});

测试数据库交互

真实应用通常需要测试数据库操作,可以使用内存数据库或模拟:

// 使用Jest模拟Mongoose模型
jest.mock('../models/User');
const User = require('../models/User');
const { getUsers } = require('./userController');

test('getUsers should fetch from database', async () => {
  const mockUsers = [{ name: 'User1' }, { name: 'User2' }];
  User.find.mockResolvedValue(mockUsers);
  
  const req = {};
  const res = mockResponse();
  
  await getUsers(req, res);
  
  expect(User.find).toHaveBeenCalled();
  expect(res.json).toHaveBeenCalledWith(mockUsers);
});

// 集成测试中使用真实数据库连接
const mongoose = require('mongoose');
const { MongoMemoryServer } = require('mongodb-memory-server');

let mongoServer;
beforeAll(async () => {
  mongoServer = await MongoMemoryServer.create();
  await mongoose.connect(mongoServer.getUri());
});

afterAll(async () => {
  await mongoose.disconnect();
  await mongoServer.stop();
});

test('should save user to database', async () => {
  const User = require('../models/User');
  const user = new User({ name: 'Test' });
  const savedUser = await user.save();
  
  expect(savedUser._id).toBeDefined();
  expect(savedUser.name).toBe('Test');
});

测试中间件

中间件测试需要特别注意next函数的调用:

// authMiddleware.js
module.exports = (req, res, next) => {
  if (!req.headers.authorization) {
    return res.status(401).json({ error: 'Unauthorized' });
  }
  next();
};

// authMiddleware.test.js
const authMiddleware = require('./authMiddleware');

test('should block unauthorized requests', () => {
  const req = { headers: {} };
  const res = mockResponse();
  
  authMiddleware(req, res, jest.fn());
  
  expect(res.status).toHaveBeenCalledWith(401);
});

test('should allow authorized requests', () => {
  const req = { headers: { authorization: 'Bearer token' } };
  const res = mockResponse();
  const next = jest.fn();
  
  authMiddleware(req, res, next);
  
  expect(next).toHaveBeenCalled();
  expect(res.status).not.toHaveBeenCalled();
});

测试错误处理

验证应用对错误情况的处理:

// errorHandler.js
module.exports = (err, req, res, next) => {
  console.error(err.stack);
  res.status(500).json({ error: 'Something went wrong!' });
};

// 测试路由抛出错误的情况
app.get('/error', (req, res, next) => {
  next(new Error('Test error'));
});

// 在测试中
it('should handle errors properly', async () => {
  const response = await request(app)
    .get('/error')
    .expect(500);
  
  expect(response.body).toEqual({
    error: 'Something went wrong!'
  });
});

测试文件上传

测试文件上传等复杂请求:

const path = require('path');
const fs = require('fs');

it('should upload file', async () => {
  const filePath = path.join(__dirname, 'test-file.txt');
  fs.writeFileSync(filePath, 'test content');
  
  const response = await request(app)
    .post('/upload')
    .attach('file', filePath)
    .expect(200);
  
  expect(response.body).toHaveProperty('filename');
  fs.unlinkSync(filePath);
});

测试认证和会话

测试需要认证的路由:

// 先登录获取token
let authToken;
beforeAll(async () => {
  const loginRes = await request(app)
    .post('/login')
    .send({ username: 'test', password: 'test' });
  authToken = loginRes.body.token;
});

it('should access protected route with valid token', async () => {
  const response = await request(app)
    .get('/protected')
    .set('Authorization', `Bearer ${authToken}`)
    .expect(200);
  
  expect(response.body).toHaveProperty('secretData');
});

测试性能与负载

虽然不是传统测试,但可以验证中间件的性能影响:

const slowMiddleware = (req, res, next) => {
  // 模拟耗时操作
  for (let i = 0; i < 1000000; i++) {}
  next();
};

test('should not add significant latency', () => {
  const start = Date.now();
  const req = {};
  const res = {};
  const next = jest.fn();
  
  slowMiddleware(req, res, next);
  
  const duration = Date.now() - start;
  expect(duration).toBeLessThan(100); // 毫秒
  expect(next).toHaveBeenCalled();
});

测试配置和环境变量

验证不同环境下的配置:

// config.js
module.exports = {
  dbUrl: process.env.DB_URL || 'mongodb://localhost:27017/test',
  port: process.env.PORT || 3000
};

// config.test.js
process.env.DB_URL = 'mongodb://memory';
process.env.PORT = '0';

const config = require('./config');

test('should use test environment variables', () => {
  expect(config.dbUrl).toBe('mongodb://memory');
  expect(config.port).toBe('0');
});

测试WebSocket集成

如果应用使用WebSocket:

const WebSocket = require('ws');
const { createServer } = require('http');

test('should handle WebSocket connections', (done) => {
  const server = createServer();
  const wss = new WebSocket.Server({ server });
  
  wss.on('connection', (ws) => {
    ws.on('message', (message) => {
      ws.send(`Echo: ${message}`);
    });
  });
  
  server.listen(() => {
    const { port } = server.address();
    const ws = new WebSocket(`ws://localhost:${port}`);
    
    ws.on('open', () => {
      ws.send('test');
    });
    
    ws.on('message', (message) => {
      expect(message).toBe('Echo: test');
      ws.close();
      server.close();
      done();
    });
  });
});

测试定时任务

对于定时执行的代码:

// taskScheduler.js
let executionCount = 0;
module.exports = {
  run: () => { executionCount++ },
  getCount: () => executionCount
};

// 使用Jest的定时器模拟
jest.useFakeTimers();

test('should run task periodically', () => {
  const { run, getCount } = require('./taskScheduler');
  
  setInterval(run, 1000);
  
  jest.advanceTimersByTime(3000);
  
  expect(getCount()).toBe(3);
});

测试第三方API调用

模拟外部API请求:

// weatherService.js
const axios = require('axios');
module.exports = {
  getWeather: async (city) => {
    const response = await axios.get(`https://api.weather.com/${city}`);
    return response.data;
  }
};

// 测试中使用Jest模拟axios
jest.mock('axios');
const axios = require('axios');
const { getWeather } = require('./weatherService');

test('should return weather data', async () => {
  const mockData = { temp: 25, condition: 'Sunny' };
  axios.get.mockResolvedValue({ data: mockData });
  
  const weather = await getWeather('london');
  
  expect(axios.get).toHaveBeenCalledWith('https://api.weather.com/london');
  expect(weather).toEqual(mockData);
});

测试覆盖率与持续集成

配置Jest收集覆盖率:

// package.json
{
  "jest": {
    "collectCoverage": true,
    "coverageThreshold": {
      "global": {
        "branches": 80,
        "functions": 80,
        "lines": 80,
        "statements": 80
      }
    }
  }
}

在CI管道中运行测试:

# .github/workflows/test.yml
name: Test
on: [push, pull_request]
jobs:
  test:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v2
      - uses: actions/setup-node@v2
        with:
          node-version: '16'
      - run: npm ci
      - run: npm test
      - run: npm run test:coverage

我的名片

网名:~川~

岗位:console.log 调试员

坐标:重庆市-九龙坡区

邮箱:cc@qdcc.cn

沙漏人生

站点信息

  • 建站时间:2013/03/16
  • 本站运行
  • 文章数量
  • 总访问量
微信公众号
每次关注
都是向财富自由迈进的一步