Supertest是Node.js生态里常用的HTTP接口测试库,它基于superagent封装,提供了链式断言和异步调用体验。实际项目中,测试脚本跑完后往往只在控制台输出绿点或红色错误堆栈,这些信息对开发者足够,但对产品、运维或需要留档的验收场景就不太友好。如果把每次接口测试的请求和响应信息渲染成图片,就可以像API调试工具那样生成一张测试快照,方便粘贴到需求单或缺陷报告里。本文实现的Supertest2Image工具正是围绕这个目标展开,核心工作分为三部分:捕获Supertest的响应数据、设计图片布局、集成到现有测试流程。

捕获Supertest的响应数据
Supertest在执行请求后会返回一个响应对象,该对象包含status、headers、body、text等属性。如果只是想获取状态码和响应体,直接读取即可。但要生成完整的请求快照,还需要记录请求方法、路径、查询参数和响应头中的关键字段,例如content-type、content-length等。建议封装一个统一的执行函数,避免在每个测试用例里重复编写提取逻辑。
下面是一个基础示例,它启动一个Express应用并使用Supertest发起GET请求,然后收集需要绘制的内容。其中res.header是一个对象,可以通过键名取值;res.body可能是对象、数组或字符串,需要根据类型做摘要处理。
const request = require('supertest');
const express = require('express');
const app = express();
app.get('/api/user', (req, res) => {
res.json({ id: 1, name: 'Alice', email: 'alice@ipipp.com' });
});
async function captureTestResult() {
const method = 'GET';
const url = '/api/user';
const start = Date.now();
const res = await request(app).get(url);
const duration = Date.now() - start;
const result = {
method,
url,
status: res.status,
duration,
headers: {
'content-type': res.headers['content-type'] || '',
'content-length': res.headers['content-length'] || ''
},
body: typeof res.body === 'object' ? JSON.stringify(res.body) : String(res.body)
};
return result;
}
这段代码中,耗时通过请求前后的时间差计算,响应体如果是对象则序列化为字符串,避免在绘制阶段处理嵌套结构。需要注意的是,如果接口返回的是Buffer或大体积文本,直接序列化可能产生很长的字符串,后续绘制时必须做截断和换行处理。
Supertest本身不提供中间件机制来拦截所有请求,因此最佳实践是在测试用例的公共封装层里记录数据。例如把测试步骤写成一个函数,接收app和请求配置,内部执行Supertest并返回格式化后的结果对象。这样既能保证数据一致性,也方便后续接入图片生成逻辑。
生成图片的两种技术路线
把结构化数据变成图片,在Node.js里主要有两类做法。第一类是使用node-canvas这类原生Canvas绑定,直接调用绘图API绘制文字、矩形和线条;第二类是使用Puppeteer控制无头浏览器渲染HTML,再对页面截图。两者各有适用场景:node-canvas启动快、依赖相对轻量,但排版和字体处理需要手动计算坐标;Puppeteer可以复用CSS布局能力,适合复杂模板,但每次生成都要启动浏览器,资源开销较大。
如果只需要固定格式的测试报告卡片,node-canvas是更合适的选择。下面的代码展示了如何创建一个800乘400的画布,绘制背景色和状态码区域。为了保持代码简洁,这里省略了响应头绘制,但保留了基本结构。
const { createCanvas, registerFont } = require('canvas');
function createReportImage(result) {
const width = 800;
const height = 400;
const canvas = createCanvas(width, height);
const ctx = canvas.getContext('2d');
// 背景
ctx.fillStyle = '#1e1e2e';
ctx.fillRect(0, 0, width, height);
// 状态码色块
const isSuccess = result.status >= 200 && result.status < 300;
ctx.fillStyle = isSuccess ? '#2ecc71' : '#e74c3c';
ctx.fillRect(0, 0, 200, height);
ctx.fillStyle = '#ffffff';
ctx.font = 'bold 48px sans-serif';
ctx.fillText(String(result.status), 40, 120);
// 请求信息
ctx.fillStyle = '#ffffff';
ctx.font = '24px sans-serif';
ctx.fillText(result.method + ' ' + result.url, 240, 80);
ctx.font = '18px sans-serif';
ctx.fillText('耗时: ' + result.duration + 'ms', 240, 130);
ctx.fillText('Content-Type: ' + result.headers['content-type'], 240, 180);
// 响应体摘要
const bodyText = result.body.length > 60 ? result.body.slice(0, 60) + '...' : result.body;
ctx.font = '16px sans-serif';
ctx.fillText(bodyText, 240, 240, 500);
return canvas.toBuffer('image/png');
}
上面的代码中比较运算符<和>在源码里被转义,实际运行时会正确比较状态码。node-canvas默认字体不支持中文,如果响应体或请求路径包含中文,会显示为方块。解决方法是调用registerFont注册系统中文字体,例如Windows下可以使用微软雅黑字体文件。注册后,在ctx.font中指定字体族名称即可正常显示中文。
Puppeteer方案则更适合生成带有表格、圆角和复杂样式的图片。可以先用模板字符串拼出HTML,再调用Puppeteer加载页面并截图。但需要注意,Puppeteer安装时会下载Chromium,体积较大,在CI环境中可能需要额外缓存。对于Supertest2Image这类轻量工具,优先推荐node-canvas,除非后续需要对图片做大量自定义排版。
封装完整的Supertest2Image工具
将前面两节的内容组合起来,可以得到一个可复用的函数。它的输入是Supertest执行后收集到的result对象,输出是PNG文件的Buffer。为了让图片信息更完整,除了状态码、耗时和响应体摘要,还可以把请求头里的authorization字段隐藏掉,避免敏感信息泄漏。同时建议在绘制阶段对文本进行宽度测量,超出可用区域时自动截断并添加省略号。
下面是一个更完整的实现,它包含了字体注册、多行文本绘制和颜色区分。实际使用时,可以把这个模块保存为supertest2image.js,在测试用例的afterEach钩子里调用,把Buffer写入文件。
const fs = require('fs');
const path = require('path');
const { createCanvas, registerFont } = require('canvas');
// 注册中文字体,根据操作系统调整路径
try {
registerFont('C:\\Windows\\Fonts\\msyh.ttc', { family: 'MSYaHei' });
} catch (e) {
console.warn('中文字体注册失败,使用默认字体');
}
function wrapText(ctx, text, x, y, maxWidth, lineHeight) {
const chars = Array.from(text);
let line = '';
let currentY = y;
for (const ch of chars) {
const testLine = line + ch;
if (ctx.measureText(testLine).width > maxWidth && line.length > 0) {
ctx.fillText(line, x, currentY);
line = ch;
currentY += lineHeight;
} else {
line = testLine;
}
}
if (line.length > 0) {
ctx.fillText(line, x, currentY);
}
return currentY;
}
function createReportImage(result) {
const width = 900;
const height = 500;
const canvas = createCanvas(width, height);
const ctx = canvas.getContext('2d');
ctx.fillStyle = '#0f172a';
ctx.fillRect(0, 0, width, height);
const isSuccess = result.status >= 200 && result.status < 300;
ctx.fillStyle = isSuccess ? '#16a34a' : '#dc2626';
ctx.fillRect(0, 0, 220, height);
ctx.fillStyle = '#ffffff';
ctx.font = 'bold 56px MSYaHei, sans-serif';
ctx.fillText(String(result.status), 50, 140);
ctx.font = '20px MSYaHei, sans-serif';
ctx.fillText(result.method, 50, 210);
ctx.fillText(result.duration + ' ms', 50, 250);
ctx.fillStyle = '#f8fafc';
ctx.font = 'bold 26px MSYaHei, sans-serif';
ctx.fillText(result.method + ' ' + result.url, 260, 80);
ctx.font = '18px MSYaHei, sans-serif';
ctx.fillStyle = '#cbd5e1';
ctx.fillText('Content-Type: ' + result.headers['content-type'], 260, 140);
ctx.fillText('Content-Length: ' + result.headers['content-length'], 260, 180);
ctx.fillStyle = '#e2e8f0';
ctx.font = '16px MSYaHei, sans-serif';
wrapText(ctx, '响应体摘要: ' + result.body, 260, 240, 600, 28);
return canvas.toBuffer('image/png');
}
async function supertest2image(result, outputDir = './reports') {
const buffer = createReportImage(result);
if (!fs.existsSync(outputDir)) {
fs.mkdirSync(outputDir, { recursive: true });
}
const filename = 'test-' + Date.now() + '.png';
const filepath = path.join(outputDir, filename);
fs.writeFileSync(filepath, buffer);
return filepath;
}
module.exports = { supertest2image };
该实现中的wrapText函数按字符逐字测量宽度,超过最大宽度就换行,适用于中文响应体。如果响应体包含很长的英文单词,没有空格,换行逻辑可能失效,可以在此基础上增加按字符强制截断的策略。另外,图片尺寸较大,实际生成时可以根据内容动态计算高度,比如根据响应体行数增加画布高度,避免截断重要信息。
对于敏感头部,建议在生成图片前过滤掉authorization、cookie、set-cookie等字段。可以在收集result对象时删除这些键,或者在绘制阶段跳过,只展示content-type、content-length、server等安全字段。这样即使图片被分享到外部群聊,也不会泄露认证信息。
错误处理与测试流程集成
接口测试不可能永远通过,超时、连接拒绝、证书错误都会导致Supertest抛出异常。Supertest2Image工具需要区分测试失败和请求失败两种情况:测试断言失败时response对象仍然存在,可以正常生成图片;网络层错误则没有response,需要捕获异常并构造一个错误结果对象。下面的代码演示了如何安全地执行请求并收集数据。
async function runWithCapture(app, method, url) {
try {
const start = Date.now();
const res = await request(app)[method.toLowerCase()](url);
const duration = Date.now() - start;
return {
method: method.toUpperCase(),
url,
status: res.status,
duration,
headers: {
'content-type': res.headers['content-type'] || '',
'content-length': res.headers['content-length'] || ''
},
body: typeof res.body === 'object' ? JSON.stringify(res.body) : String(res.body)
};
} catch (err) {
return {
method: method.toUpperCase(),
url,
status: 0,
duration: 0,
headers: { 'content-type': '', 'content-length': '' },
body: '请求失败: ' + err.message
};
}
}
这个函数把status为0作为网络层失败的标记,绘制时会将状态码色块变为灰色或红色。在Mocha或Jest中,可以把它放在测试用例内部,断言通过或失败后都调用supertest2image生成图片。对于Jest,可以在afterEach钩子中访问当前测试结果,但需要自行传递result对象;更简单的做法是在每个用例结尾显式调用生成函数。
CI环境通常运行在Linux容器中,没有Windows字体,此时需要额外安装字体包,例如在Dockerfile中添加fonts-noto-cjk。node-canvas依赖Cairo等底层库,在Alpine镜像中可能需要安装build依赖。如果不想处理这些兼容性问题,可以改用Puppeteer方案,因为它使用系统字体,只要容器安装了中文字体即可。无论采用哪种方案,图片输出目录都应该配置为CI artifact路径,方便在测试报告页面查看。
Supertest2Image并不是要替代现有的测试报告工具,而是为接口测试结果提供一种轻量、直观的分享方式。通过本文的实现,你可以把每次Supertest请求的请求方法、路径、状态码、耗时和响应摘要保存为PNG图片,也可以基于node-canvas的绘图能力继续扩展更多字段,比如请求头、响应头全量展示,或者添加二维码指向测试报告详情页。把测试结果从终端搬到图片上,往往能让技术沟通更高效。

