场景
在随机地让ChatGPT写了一些代码之后,我想用一个简单的、但隐约是 "真实 "的场景来测试它。
一个简单的Express服务器,有一系列与处理订单有关的端点。
以一个包含各种信息的订单对象为例,如购买的物品、总价、客户的详细信息等,端点将返回一个字符串作为运输标签,一个销售人员的佣金数额,以及客户的忠诚度积分数额。为了增加复杂性,还将有几个返回CSV的端点。
工作
首先,我创建一个新的Express服务器。我可以直接使用一个命令,所以我不需要ChatGPT来做这个。
npx express-generator --no-view
现在引入ChatGPT!首先,我要求它生成一个路由,为订单返回一个硬编码的JSON对象,包括账单地址、运输地址、行数、总额、支付方式、运输方式、客户、销售人员和订单号。这不会被应用程序直接使用,但以后会很有用。
它的返回结果是这样的,得到了它的要点,但稍有局限。我肯定需要地址包括国家,所以我手动添加了这个。
app.get('/order/:id', (req, res) => {
const orderId = req.params.id;
const order = {
billingAddress: {
street: '1 Main St',
city: 'Boston',
state: 'MA',
zip: '01234'
},
shippingAddress: {
street: '1 Main St',
city: 'Boston',
state: 'MA',
zip: '01234'
},
lineItems: [
{
productId: 1,
name: 'Product 1',
quantity: 2,
price: 9.99
},
{
productId: 2,
name: 'Product 2',
quantity: 1,
price: 14.99
}
],
total: 34.97,
paymentMethod: 'credit card',
shippingMethod: '2-day',
customer: {
id: 1,
name: 'John Doe'
},
salesperson: {
id: 2,
name: 'Jane Doe'
},
orderNumber: orderId
};
res.json(order);
});
我试着启动服务器,但它崩溃了,因为app 没有定义。由于我使用的是生成器中的Express Router,我需要把app.get 改为router.get 。我这样做了,它就成功了
我想用ChatGPT尝试一些测试驱动开发,所以我安装了Jest。
我要求ChatGPT为程序需要的主要功能生成一个Jest测试。
一个接收订单对象并以字符串形式返回送货地址的函数。
test('getShippingAddressFromOrder', () => {
const order = {
shippingAddress: {
street: '1 Main St',
city: 'Boston',
state: 'MA',
zip: '01234'
}
};
const expectedShippingAddress = '1 Main St, Boston, MA 01234';
const actualShippingAddress = getShippingAddressFromOrder(order);
expect(actualShippingAddress).toBe(expectedShippingAddress);
});
一个函数,接收一个订单对象,并返回销售人员的0.1%的佣金。
test('getSalespersonCommission', () => {
const order = {
total: 100,
salesperson: {
id: 1,
name: 'Jane Doe'
}
};
const expectedCommission = 0.1;
const actualCommission = getSalespersonCommission(order);
expect(actualCommission).toBe(expectedCommission);
});
一个接收订单对象并返回客户忠诚度积分的函数,每块钱2分。
test('getCustomerLoyaltyPoints', () => {
const order = {
total: 100,
customer: {
id: 1,
name: 'John Doe'
}
};
const expectedLoyaltyPoints = 200;
const actualLoyaltyPoints = getCustomerLoyaltyPoints(order);
expect(actualLoyaltyPoints).toBe(expectedLoyaltyPoints);
});
一个函数,接收一个订单对象,并为包装商返回一个带有行项目和订单号的csv。
test('getPackerCsv', () => {
const order = {
orderNumber: 123456,
lineItems: [
{
productId: 1,
name: 'Product 1',
quantity: 2
},
{
productId: 2,
name: 'Product 2',
quantity: 1
}
]
};
const expectedCsv = '123456,Product 1,2\n123456,Product 2,1';
const actualCsv = getPackerCsv(order);
expect(actualCsv).toBe(expectedCsv);
});
一个函数,接收一个订单对象,并为财务部门返回一个包含国家和收入的 csv 文件。
test('getFinanceCsv', () => {
const order = {
billingAddress: {
country: 'US',
},
total: 100,
};
const expectedCsv = 'US,100';
const actualCsv = getFinanceCsv(order);
expect(actualCsv).toBe(expectedCsv);
});
现在我有五个测试运行,它们都失败了,这很好,因为这些函数还没有写出来
接下来我要求ChatGPT编写我的函数。
一个接收订单对象并以字符串形式返回送货地址的函数。
function getShippingAddressFromOrder(order) {
const { shippingAddress } = order;
return `${shippingAddress.street}, ${shippingAddress.city}, ${shippingAddress.state} ${shippingAddress.zip}`;
}
一个函数,接收一个订单对象并返回销售人员的0.1%的佣金。
function calculateCommission(order) {
return order.total * 0.001;
}
一个接收订单对象并返回客户忠诚度积分的函数,每块钱2分。
function calculateLoyaltyPoints(order) {
return order.total * 2;
}
一个函数,接收一个订单对象,并为包装商返回一个带有行项目和订单号的csv。
function generatePackerCsv(order) {
let csv = `Order Number: ${order.number}\n`;
for (const item of order.items) {
csv += `${item.name}, ${item.quantity}\n`;
}
return csv;
}
一个函数,接收一个订单对象,并为财务部门返回一个包含国家和收入的 csv 文件。
function generateFinanceCsv(order) {
return `${order.country}, ${order.total}\n`;
}
它告诉我如何使用这些函数,并给出了调用它们的例子对象。
接下来我把它们导出并导入我的测试文件中。这一步我是手动完成的。
现在我再次运行我的测试,一个通过了(🎉),其他四个没有通过。我发现这是因为函数的名称与测试中的名称不一致,我手动纠正了这一点。
现在其中三个通过了。一个失败是因为函数返回错误的结果,另一个失败是因为函数期望有一个可迭代的东西,但没有得到。
我看了一下,发现其中一个使用了错误的属性名称,另一个则是期望有错误的嵌套。这些问题很容易手动解决。
它们仍然失败,这次是因为函数返回的CSV字符串与测试所期望的不一致。这是一个更大的错误。
经过一些手动调试,现在所有测试都通过了。
我想我可能需要另一个函数来接收CSV字符串并生成CSV文件,所以我将向ChatGPT要一个测试和一个函数来实现。
const fs = require("fs");
const { generateCsvFile } = require("./csv-utils");
describe("generateCsvFile", () => {
it("should generate a CSV file from a CSV string", () => {
const csvString = "A,B,C\n1,2,3\n4,5,6";
const filePath = "./test.csv";
generateCsvFile(csvString, filePath);
const csvFile = fs.readFileSync(filePath, "utf8");
expect(csvFile).toEqual(csvString);
fs.unlinkSync(filePath);
});
});
const fs = require("fs");
function generateCsvFile(csvString, filePath) {
fs.writeFileSync(filePath, csvString, "utf8");
}
module.exports = { generateCsvFile };
这次它也为我生成了导入和导出文件,所以我只需要改变路径以适应我的目录结构,就应该通过了。
它通过了!
最后一步是制作使用这些函数的路由,我要求ChatGPT也这样做。
一个接受订单对象并使用getShippingAddressFromOrder 函数来返回运输地址的路由。
app.get('/shipping-address', (req, res) => {
const { order } = req.body;
const shippingAddress = getShippingAddressFromOrder(order);
res.send({ shippingAddress });
});
一个接受订单对象并使用getSalespersonCommission 函数来返回销售人员的佣金的路由。
app.get('/salesperson-commission', (req, res) => {
const { order } = req.body;
const commission = getSalespersonCommission(order);
res.send({ commission });
});
一个接受订单对象并使用getCustomerLoyaltyPoints 函数返回客户忠诚度积分的途径。
app.get('/customer-loyalty-points', (req, res) => {
const { order } = req.body;
const loyaltyPoints = getCustomerLoyaltyPoints(order);
res.send({ loyaltyPoints });
});
一个接受订单对象并使用getPackerCsv 函数为包装商返回一个 CSV 文件的途径。
app.get('/packer-csv', (req, res) => {
const { order } = req.body;
const csv = getPackerCsv(order);
res.setHeader('Content-Type', 'text/csv');
res.setHeader('Content-Disposition', 'attachment; filename="packer.csv"');
res.send(csv);
});
一个接受订单对象并使用getFinanceCsv 函数为财务部门返回一个 CSV 文件的途径。
app.get('/finance-csv', (req, res) => {
const { order } = req.body;
const csv = getFinanceCsv(order);
res.setHeader('Content-Type', 'text/csv');
res.setHeader('Content-Disposition', 'attachment; filename="finance.csv"');
res.send(csv);
});
我必须再次将app.get 改为router.get 。我还得手动添加函数导入。
然后我启动服务器,尝试使用/order/:id 端点返回的订单对象来测试新路线。
发货地址端点工作了,我得到了这个响应:
{
"shippingAddress": "1 Main St, Boston, MA 01234"
}
销售人员的佣金端点可以工作,我得到这样的响应:
{
"commission": 0.03497
}
客户忠诚度积分端点工作,我得到这样的回应:
{
"loyaltyPoints": 69.94
}
我现在意识到,它应该把忠诚度积分四舍五入到一个整数。
Packer CSV端点可以工作,我得到这样的回应:
1,Product 1,2
1,Product 2,1
财务部的CSV端点可以工作,我得到这样的回应:
US,34.97
我现在有了一个可以工作的服务器,而且我几乎没有用手写过任何代码
回顾一下
现在要对GPTChat所产生的代码进行回顾。
虽然它确实做了一项令人印象深刻的工作,但它绝不是完美无缺的。
我对代码风格有一些评论,我更喜欢箭头函数和没有分号,但如果这个工具被集成到IDE中,让它从现有的代码风格或linter规则中理解这些可能是微不足道的,所以不用担心这个问题。
破坏测试的错误似乎大多是关于缺乏一致性,而不是任何根本性的缺乏理解,而且值得记住的是,我没有一次性完成所有这些,所以它没有之前写过的背景。同样,如果这个工具被集成到IDE中,这一点可能会得到解决。
我不喜欢它生成的地址对象--包括的属性不容易支持世界上某些地方的地址,而且它不包括国家,这在任何国际环境中都是必不可少的(让我们面对现实吧,大多数技术都是国际化的)。发货地址字符串函数假定它总是而且只具有它为样本地址生成的四个属性。这可能表明ChatGPT对世界各地使用的标准范围没有足够的了解,或者只是假设了美国的环境(而我不在美国)。它也没有对客户忠诚度积分进行四舍五入,而当我收到信用卡账单时,这些积分总是被四舍五入了。
总的来说,我认为有必要记住,这是一个工具,而不是一个真理的来源。它可以成为人类的一个有用的助手,但它的输出在用于生产之前应该始终由人类检查。