后端联调前自动化对比本地mock接口和后端实际接口的返回数据

171 阅读5分钟

要在前后端联调前自动化对比本地mock接口和后端实际接口的返回数据结构和字段,可以采用以下步骤来实现:

  1. 定义接口规范:使用OpenAPI(Swagger)或其他API文档工具定义前端和后端的接口规范。
  2. 编写Mock数据:在前端项目中准备与接口规范一致的Mock数据。
  3. 获取后端实际数据:通过HTTP请求获取后端接口的实际返回数据。
  4. 比较数据结构:编写脚本自动化比较Mock数据和后端数据的结构和字段,确保一致性。
  5. 生成报告:将比较结果生成报告,便于开发人员查看和修复不一致的问题。

下面将详细介绍如何使用Node.js和Jest测试框架来实现上述功能。


项目结构

首先,规划项目结构如下:

project-root/
│
├── mocks/
│   ├── businessCouponMock.json
│   └── ... (其他Mock数据)
│
├── schemas/
│   ├── businessCouponSchema.json
│   └── ... (其他接口规范)
│
├── tests/
│   ├── compareSchemas.test.js
│   └── ... (其他测试文件)
│
├── utils/
│   ├── fetchData.js
│   └── compareSchemas.js
│
├── package.json
└── jest.config.js

步骤详解

1. 初始化项目

首先,初始化一个Node.js项目并安装必要的依赖。

npm init -y
npm install axios jest jest-extended ajv
  • axios:用于发送HTTP请求。
  • jest:测试框架。
  • jest-extended:扩展Jest的断言库。
  • ajv:JSON Schema验证器。

更新package.json,添加测试脚本:

{
  "scripts": {
    "test": "jest"
  }
}

2. 定义接口规范(Schemas)

schemas/目录下,为每个接口定义JSON Schema。例如,businessCouponSchema.json

{
  "$schema": "http://json-schema.org/draft-07/schema#",
  "title": "Business Coupon Response",
  "type": "object",
  "properties": {
    "bg": { "type": "string" },
    "logo": { "type": "string" },
    "title": { "type": "string" },
    "couponNum": { "type": "string" },
    "couponTitle": { "type": "string" },
    "businessCoupon": { "type": "string" },
    "couponBtn": {
      "type": "object",
      "properties": {
        "text": { "type": "string" },
        "color": { "type": "string" },
        "fontSize": { "type": "number" },
        "fontWeight": { "type": "number" },
        "width": { "type": "number" },
        "height": { "type": "number" },
        "bg": { "type": "string" }
      },
      "required": ["text", "color", "fontSize", "fontWeight", "width", "height", "bg"]
    },
    "businessBtn": {
      "type": "object",
      "properties": {
        "text": { "type": "string" },
        "color": { "type": "string" },
        "fontSize": { "type": "number" },
        "fontWeight": { "type": "number" },
        "width": { "type": "number" },
        "height": { "type": "number" },
        "bg": { "type": "string" }
      },
      "required": ["text", "color", "fontSize", "fontWeight", "width", "height", "bg"]
    },
    "isBackClose": { "type": "boolean" },
    "afxUrlNoFish": { "type": "string" }
  },
  "required": ["bg", "logo", "title", "couponNum", "couponTitle", "businessCoupon", "couponBtn", "businessBtn", "isBackClose", "afxUrlNoFish"]
}

3. 编写Mock数据

mocks/目录下,为每个接口编写符合JSON Schema的Mock数据。例如,businessCouponMock.json

{
  "bg": "background-image-url",
  "logo": "logo-url",
  "title": "度小满送你一张优惠券",
  "couponNum": "50",
  "couponTitle": "叠加满500减50",
  "businessCoupon": "coupon-background-url",
  "couponBtn": {
    "text": "立即领券",
    "color": "#FDF4E7",
    "fontSize": 36,
    "fontWeight": 500,
    "width": 193,
    "height": 72,
    "bg": "coupon-btn-bg-url"
  },
  "businessBtn": {
    "text": "去使用",
    "color": "#FDF4E7",
    "fontSize": 68,
    "fontWeight": 600,
    "width": 600,
    "height": 186,
    "bg": "business-btn-bg-url"
  },
  "isBackClose": true,
  "afxUrlNoFish": "afx-url"
}

4. 编写获取后端数据的工具函数

utils/fetchData.js中编写函数,用于从后端获取实际数据。

// utils/fetchData.js

const axios = require('axios');

/**
 * 发送GET请求获取后端数据
 * @param {string} url - 接口URL
 * @returns {Promise<Object>} 返回数据
 */
async function getBackendData(url) {
  try {
    const response = await axios.get(url);
    return response.data;
  } catch (error) {
    console.error(`Error fetching backend data from ${url}:`, error);
    throw error;
  }
}

module.exports = { getBackendData };

5. 编写比较数据结构的工具函数

utils/compareSchemas.js中编写函数,使用ajv验证Mock数据和后端数据是否符合JSON Schema。

// utils/compareSchemas.js

const Ajv = require('ajv');
const ajv = new Ajv({ allErrors: true });

/**
 * 比较数据是否符合Schema
 * @param {Object} schema - JSON Schema
 * @param {Object} data - 数据对象
 * @returns {Object} 返回验证结果
 */
function validateSchema(schema, data) {
  const validate = ajv.compile(schema);
  const valid = validate(data);
  return { valid, errors: validate.errors };
}

/**
 * 深度比较两个对象的结构和字段
 * @param {Object} obj1 - 第一个对象
 * @param {Object} obj2 - 第二个对象
 * @returns {boolean} 是否相同
 */
function deepCompare(obj1, obj2) {
  if (typeof obj1 !== typeof obj2) return false;
  if (typeof obj1 !== 'object' || obj1 === null || obj2 === null) {
    return obj1 === obj2;
  }
  const keys1 = Object.keys(obj1);
  const keys2 = Object.keys(obj2);
  if (keys1.length !== keys2.length) return false;
  for (let key of keys1) {
    if (!keys2.includes(key)) return false;
    if (!deepCompare(obj1[key], obj2[key])) return false;
  }
  return true;
}

module.exports = { validateSchema, deepCompare };

6. 编写测试脚本

tests/compareSchemas.test.js中编写测试用例,自动化对比Mock数据和后端数据。

// tests/compareSchemas.test.js

const fs = require('fs');
const path = require('path');
const { getBackendData } = require('../utils/fetchData');
const { validateSchema, deepCompare } = require('../utils/compareSchemas');

const backendBaseUrl = 'https://api.backend.com'; // 替换为实际后端地址

// 列出需要测试的接口
const apiTests = [
  {
    name: 'businessCoupon',
    endpoint: '/api/business/coupon',
    mockFile: 'businessCouponMock.json',
    schemaFile: 'businessCouponSchema.json',
  },
  // 可以在这里添加更多接口测试
];

describe('API Schema Comparison Tests', () => {
  apiTests.forEach(({ name, endpoint, mockFile, schemaFile }) => {
    test(`${name} API should match the schema and mock data`, async () => {
      // 读取Mock数据
      const mockDataPath = path.join(__dirname, '../mocks', mockFile);
      const mockData = JSON.parse(fs.readFileSync(mockDataPath, 'utf-8'));

      // 读取Schema
      const schemaPath = path.join(__dirname, '../schemas', schemaFile);
      const schema = JSON.parse(fs.readFileSync(schemaPath, 'utf-8'));

      // 获取后端数据
      const backendData = await getBackendData(`${backendBaseUrl}${endpoint}`);

      // 验证Mock数据是否符合Schema
      const mockValidation = validateSchema(schema, mockData);
      expect(mockValidation.valid).toBe(true);
      if (!mockValidation.valid) {
        console.error(`Mock Data Validation Errors for ${name}:`, mockValidation.errors);
      }

      // 验证后端数据是否符合Schema
      const backendValidation = validateSchema(schema, backendData);
      expect(backendValidation.valid).toBe(true);
      if (!backendValidation.valid) {
        console.error(`Backend Data Validation Errors for ${name}:`, backendValidation.errors);
      }

      // 深度比较Mock数据和后端数据
      const isSameStructure = deepCompare(mockData, backendData);
      expect(isSameStructure).toBe(true);
      if (!isSameStructure) {
        console.error(`Data structure mismatch for ${name} API`);
      }
    });
  });
});

7. 配置Jest

在项目根目录下创建jest.config.js,配置Jest。

// jest.config.js

module.exports = {
  testEnvironment: 'node',
  verbose: true,
  setupFilesAfterEnv: ['jest-extended'],
};

8. 运行测试

在命令行中运行以下命令启动测试:

npm test

代码详解

utils/fetchData.js

该模块使用axios发送HTTP GET请求,以获取后端接口的实际数据。

const axios = require('axios');

/**
 * 发送GET请求获取后端数据
 * @param {string} url - 接口URL
 * @returns {Promise<Object>} 返回数据
 */
async function getBackendData(url) {
  try {
    const response = await axios.get(url);
    return response.data;
  } catch (error) {
    console.error(`Error fetching backend data from ${url}:`, error);
    throw error;
  }
}

module.exports = { getBackendData };
  • 功能:封装了发送GET请求的逻辑,捕捉并输出错误信息,方便调试。

utils/compareSchemas.js

该模块提供两个函数:

  1. validateSchema:使用ajv验证数据是否符合给定的JSON Schema。
  2. deepCompare:深度比较两个对象的结构和字段是否一致。
const Ajv = require('ajv');
const ajv = new Ajv({ allErrors: true });

/**
 * 比较数据是否符合Schema
 * @param {Object} schema - JSON Schema
 * @param {Object} data - 数据对象
 * @returns {Object} 返回验证结果
 */
function validateSchema(schema, data) {
  const validate = ajv.compile(schema);
  const valid = validate(data);
  return { valid, errors: validate.errors };
}

/**
 * 深度比较两个对象的结构和字段
 * @param {Object} obj1 - 第一个对象
 * @param {Object} obj2 - 第二个对象
 * @returns {boolean} 是否相同
 */
function deepCompare(obj1, obj2) {
  if (typeof obj1 !== typeof obj2) return false;
  if (typeof obj1 !== 'object' || obj1 === null || obj2 === null) {
    return obj1 === obj2;
  }
  const keys1 = Object.keys(obj1);
  const keys2 = Object.keys(obj2);
  if (keys1.length !== keys2.length) return false;
  for (let key of keys1) {
    if (!keys2.includes(key)) return false;
    if (!deepCompare(obj1[key], obj2[key])) return false;
  }
  return true;
}

module.exports = { validateSchema, deepCompare };
  • validateSchema

    • 参数
      • schema:JSON Schema定义。
      • data:需要验证的数据对象。
    • 返回
      • valid:布尔值,表示数据是否符合Schema。
      • errors:如果不符合,包含详细的错误信息。
  • deepCompare

    • 功能:递归比较两个对象的类型和值,确保结构和字段完全一致。
    • 返回:布尔值,表示两个对象是否相同。

tests/compareSchemas.test.js

该测试文件负责读取Mock数据、后端数据和Schema,进行验证和比较。

const fs = require('fs');
const path = require('path');
const { getBackendData } = require('../utils/fetchData');
const { validateSchema, deepCompare } = require('../utils/compareSchemas');

const backendBaseUrl = 'https://api.backend.com'; // 替换为实际后端地址

// 列出需要测试的接口
const apiTests = [
  {
    name: 'businessCoupon',
    endpoint: '/api/business/coupon',
    mockFile: 'businessCouponMock.json',
    schemaFile: 'businessCouponSchema.json',
  },
  // 可以在这里添加更多接口测试
];

describe('API Schema Comparison Tests', () => {
  apiTests.forEach(({ name, endpoint, mockFile, schemaFile }) => {
    test(`${name} API should match the schema and mock data`, async () => {
      // 读取Mock数据
      const mockDataPath = path.join(__dirname, '../mocks', mockFile);
      const mockData = JSON.parse(fs.readFileSync(mockDataPath, 'utf-8'));

      // 读取Schema
      const schemaPath = path.join(__dirname, '../schemas', schemaFile);
      const schema = JSON.parse(fs.readFileSync(schemaPath, 'utf-8'));

      // 获取后端数据
      const backendData = await getBackendData(`${backendBaseUrl}${endpoint}`);

      // 验证Mock数据是否符合Schema
      const mockValidation = validateSchema(schema, mockData);
      expect(mockValidation.valid).toBe(true);
      if (!mockValidation.valid) {
        console.error(`Mock Data Validation Errors for ${name}:`, mockValidation.errors);
      }

      // 验证后端数据是否符合Schema
      const backendValidation = validateSchema(schema, backendData);
      expect(backendValidation.valid).toBe(true);
      if (!backendValidation.valid) {
        console.error(`Backend Data Validation Errors for ${name}:`, backendValidation.errors);
      }

      // 深度比较Mock数据和后端数据
      const isSameStructure = deepCompare(mockData, backendData);
      expect(isSameStructure).toBe(true);
      if (!isSameStructure) {
        console.error(`Data structure mismatch for ${name} API`);
      }
    });
  });
});
  • apiTests:列出需要测试的接口,每个接口包含名称、端点、Mock文件和Schema文件。

  • 测试流程

    1. 读取Mock数据:从mocks/目录读取对应的Mock JSON文件。
    2. 读取Schema:从schemas/目录读取对应的Schema JSON文件。
    3. 获取后端数据:调用后端接口,获取实际数据。
    4. 验证Mock数据:使用validateSchema函数验证Mock数据是否符合Schema。
    5. 验证后端数据:同样验证后端数据是否符合Schema。
    6. 深度比较:使用deepCompare函数比较Mock数据和后端数据的结构和字段是否一致。
  • 断言

    • expect(mockValidation.valid).toBe(true);:确保Mock数据符合Schema。
    • expect(backendValidation.valid).toBe(true);:确保后端数据符合Schema。
    • expect(isSameStructure).toBe(true);:确保Mock数据和后端数据结构一致。

运行测试

确保所有配置和数据文件正确无误后,运行以下命令进行测试:

npm test

测试结果将显示每个接口的验证和比较结果,如果存在不一致,将输出详细的错误信息,便于开发人员进行修复。


扩展功能

1. 自动生成Mock数据

可以使用工具如json-schema-faker根据Schema自动生成Mock数据,确保Mock数据和Schema的一致性。

npm install json-schema-faker
// utils/generateMock.js

const fs = require('fs');
const path = require('path');
const jsf = require('json-schema-faker');

/**
 * 根据Schema生成Mock数据并保存到指定路径
 * @param {string} schemaFile - Schema文件名
 * @param {string} outputFile - 输出Mock文件名
 */
function generateMock(schemaFile, outputFile) {
  const schemaPath = path.join(__dirname, '../schemas', schemaFile);
  const schema = JSON.parse(fs.readFileSync(schemaPath, 'utf-8'));
  const mockData = jsf.generate(schema);
  const outputPath = path.join(__dirname, '../mocks', outputFile);
  fs.writeFileSync(outputPath, JSON.stringify(mockData, null, 2), 'utf-8');
  console.log(`Mock data generated and saved to ${outputPath}`);
}

// 示例:生成businessCoupon的Mock数据
generateMock('businessCouponSchema.json', 'businessCouponMock.json');

运行脚本:

node utils/generateMock.js

2. 集成到CI/CD流程

将自动化对比测试集成到CI/CD流程中,确保每次代码提交或部署前,接口数据结构的一致性得到验证。

例如,在使用GitHub Actions时,可以添加以下工作流文件:

# .github/workflows/api-schema-compare.yml

name: API Schema Comparison

on:
  push:
    branches: [ main ]
  pull_request:
    branches: [ main ]

jobs:
  compare-schemas:
    runs-on: ubuntu-latest

    steps:
      - uses: actions/checkout@v2

      - name: Set up Node.js
        uses: actions/setup-node@v2
        with:
          node-version: '14'

      - name: Install dependencies
        run: npm install

      - name: Run schema comparison tests
        run: npm test

3. 支持多种HTTP方法和参数

扩展apiTests数组,支持不同的HTTP方法(GET、POST等)和传递必要的参数,以适应更多类型的接口测试需求。

const apiTests = [
  {
    name: 'businessCoupon',
    endpoint: '/api/business/coupon',
    method: 'GET',
    headers: { Authorization: 'Bearer token' },
    params: { userId: 123 },
    mockFile: 'businessCouponMock.json',
    schemaFile: 'businessCouponSchema.json',
  },
  // 其他接口测试
];

修改fetchData.js以支持不同的HTTP方法和参数。

// utils/fetchData.js

const axios = require('axios');

/**
 * 发送HTTP请求获取后端数据
 * @param {string} method - HTTP方法
 * @param {string} url - 接口URL
 * @param {Object} options - 其他选项,如headers、params、data
 * @returns {Promise<Object>} 返回数据
 */
async function sendRequest(method, url, options = {}) {
  try {
    const response = await axios({
      method,
      url,
      ...options,
    });
    return response.data;
  } catch (error) {
    console.error(`Error fetching backend data from ${url}:`, error);
    throw error;
  }
}

module.exports = { sendRequest };

在测试文件中调用sendRequest

const { sendRequest } = require('../utils/fetchData');

// ...

const backendData = await sendRequest(method, `${backendBaseUrl}${endpoint}`, {
  headers,
  params,
  data,
});

4. 增加字段、类型和枚举值的验证

在JSON Schema中,可以定义字段的类型、枚举值等更详细的验证规则,确保数据的准确性和一致性。

{
  "type": "object",
  "properties": {
    "status": {
      "type": "string",
      "enum": ["success", "error"]
    },
    "data": {
      "type": "object",
      "properties": {
        "id": { "type": "integer" },
        "name": { "type": "string" },
        "email": { "type": "string", "format": "email" }
      },
      "required": ["id", "name", "email"]
    }
  },
  "required": ["status", "data"]
}

5. 处理动态数据

对于返回数据中包含动态值(如时间戳、随机数)的字段,可以在Schema中使用正则表达式或自定义验证逻辑进行处理,以免影响结构对比。

{
  "type": "object",
  "properties": {
    "timestamp": {
      "type": "string",
      "pattern": "^\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}Z$"
    }
  },
  "required": ["timestamp"]
}

总结

通过以上步骤,可以实现前端在前后端联调前,自动化对比本地Mock接口和后端实际接口的数据结构和字段。该方法通过定义明确的接口规范、编写符合规范的Mock数据、自动获取后端数据,并利用JSON Schema验证和深度比较,确保接口的一致性和数据的准确性。这不仅提高了开发效率,还降低了因接口不一致引发的潜在问题。

为了进一步扩展和优化,可以集成自动化工具链、支持更多类型的接口测试,并不断完善Schema定义,以适应项目需求的变化。