像黑客一样进行单元测试🧪
灵感
在我的一门计算机科学课程中,老师教我们编写代码实现只需满足单元测试的要求即可。这叫做测试驱动开发(TDD),其核心在于确保代码在特定用例中能够按预期运行。具体来说,你需要先编写单元测试,然后再编写实现代码以通过这些测试。
编写单元测试并对其进行良好的维护,起初并不是一个容易理解的概念,但这是我作为初级开发人员学到的关键技能之一。
HacktoberTest——哈哈,这是个双关语……
每天反复进行单元测试和处理实验/作业,实际上帮助我在 Hacktoberfest 期间找到了合适的任务来做贡献!
我最初的两个 PR(第一个和第二个)都比较容易被接受newbie level,所以我想要贡献一些更有挑战性的东西meaningful,一些能激发我智力潜能的东西。我花了一周左右的时间在issues 页面上纠结接下来该贡献什么,结果反而更加焦虑。我逐渐意识到,如果你只是不停地在 issues 页面上滚动查找符合你需求的 issue,那你就注定要失败……因为还有两万多人也在做同样的事情,只是为了获得一些小礼物。这样一来,你花在搜索上的精力可能比从头开始编写一个新功能或修复一个棘手的 bug 还要多。
幸运的是,需要编写单元测试的问题需求量不如其他问题那么高,所以我找到了一些这样的问题。这个问题和这个问题让我有机会编写单元测试,并让我能够为一个真正有意义且具有挑战性的项目提交 PR。
黑客攻击
我参与的项目是一个名为pr-approve-generator 的Web 应用,它可以为 PR 生成鼓励信息。它旨在供 GitHub 上的项目维护者使用,以鼓励他们的贡献者。
每次点击Refresh按钮,都会收到一条新消息,欢迎 PR 并鼓励贡献者继续努力。这些消息存储在一个数组中,应用程序会随机选择其中一条显示。以下是处理此逻辑的函数:
getRandomMessage() {
const { messagesState } = state;
const index = Math.floor(Math.random() * messagesState.length);
let newMessage;
let newMessagesState;
if (messagesState.length !== 0) {
newMessage = messagesState[index];
newMessagesState = [
...messagesState.slice(0, index),
...messagesState.slice(index + 1),
];
} else {
newMessage = messages[index];
newMessagesState = [
...messages.slice(0, index),
...messages.slice(index + 1),
];
}
state.messagesState = newMessagesState;
return { newMessage, newMessagesState };
},
您可以查看randomizer.js以更好地理解
我首先着手解决的问题是为该getRandomMessage函数编写单元测试。该函数负责从数组中随机选择一条消息并返回,同时还会将该消息从数组中移除,以避免重复选择相同的消息。如果数组中没有剩余消息,则会将空数组重新填充为消息数组,依此类推。每次Refresh点击按钮时都会调用此函数。(此外,该应用最近还新增了一个功能,允许用户使用函数更改表情符号getRandomEmoji,其工作原理与上述非常相似。我也提交了一个 PR 来为该功能编写测试,链接在此。)
单元测试框架已经实现,使用的是Vitest,所以我开始进行一些修改,比如设置一个代码覆盖率提供程序来显式地识别代码行,并在评论covered/uncovered中向维护者提到了这一点。我为此使用了Istanbul 🇹🇷 。
单元测试既有疗愈作用又令人痛苦。
我开始模拟函数messages & emojis array,并getRandomMessage()使用模拟数组调用函数。根据选取的索引,我断言返回的消息不等于新消息状态,因为消息已从数组中移除(即它们必须是唯一的)。
it("should always return unique message", () => {
const messages = ["1", "2", "3", "4", "5"];
const emojis = ["1", "2", "3", "4", "5"];
const randomizer = buildRandomizer(messages, emojis);
const { newMessage, newMessagesState } = randomizer.getRandomMessage();
expect(newMessagesState).not.toContain(newMessage);
});
请注意,此测试遵循一定的AAA (Arrange-Act-Assert)模式。这Arrange部分仅用于设置测试中要操作的数据。
const messages = ["1", "2", "3", "4", "5"];
const emojis = ["1", "2", "3", "4", "5"];
这Act部分用于调用你要测试的函数。
const randomizer = buildRandomizer(messages, emojis);
const { newMessage, newMessagesState } = randomizer.getRandomMessage();
这Assert部分是预期结果,它取决于Act该功能会引发需要断言的潜在反应。
expect(newMessagesState).not.toContain(newMessage);
基于这种模式,我编写了完整的randomizer.test.js 文件并提交了一个PR。我的第二个 PR是关于为messages.test.js 文件编写单元测试,以确保以下几点:
- 无论格式和使用的表情符号如何,每条信息都应该是独一无二的。
- 重复消息测试失败
- 无论格式如何,LGTM 消息在测试中都会失败。
为了满足这些要求,我使用正则表达式来匹配消息的格式,并断言消息是唯一的。
it("should have unique messages regardless of the emojis", () => {
const regex = /([a-zA-Z0-9 ])/g;
const uniqueMessages = messages.map((message) =>
message.match(regex).join("").toLowerCase()
);
expect(uniqueMessages).toEqual([...new Set(uniqueMessages)]);
expect(uniqueMessages.length).toBe(messages.length);
});
哦,我还特意确认了一下LGTM,他不受欢迎 :P
it("should never contain the message LGTM", () => {
const lgtmMessages = messages.filter(
(message) => message.toLowerCase() === "lgtm"
);
expect(lgtmMessages).toEqual([]);
});
现在,覆盖率报告骄傲地显示着100%覆盖率的绿色线条!这份报告很有价值,因为它以可视化的方式展现并记录了测试的可行性。看到它按预期运行,我感到很有成就感,也觉得很欣慰。
Git 问题
不过,在我第二次提交 PR 时,遇到了一些问题git branches。最初,我在为randomizer.test.js文件编写单元测试时,创建了一个名为 `test` 的分支tests-for-randomizer来专门实现该文件所需的测试。在将我的工作合并到该分支后,我创建了一个名为 `test` 的新分支,用于使用 `test` 命令tests-for-messages为文件 `test` 编写测试。显然,我为该文件编写的所有测试工作都合并到了这个新分支中。messages.test.jsgit checkout -b tests-for-messagesrandomizer.test.jsmessages.test.js
我首先需要更新master分支,然后在当前分支上使用 `rebase` 命令(使用 `-i` 参数进行交互式操作tests-for-messages)将分支变基到 master 分支,以移除与文件无关的提交。问题在于,我只在自己练习过变基,在开源项目中操作让我感到畏惧。我害怕搞砸项目,丢失我的工作成果。我向维护者寻求帮助,他指导我完成了整个过程。所以最终,为了解决这个问题,我需要将分支变基到 master 分支,移除与文件无关的提交,然后使用 `force push` 命令强制推送到远程分支。git rebase master -imessages.test.jstests-for-messagesmastermessages.test.jstests-for-messagesgit push -f origin tests-for-messages
单元测试的痛点
确保一切按预期运行在原则上是合理的。例如,我目前正在用 C++ 开发一个名为palpatine 的静态网站生成器,在开发过程中,我非常重视编写单元测试。很快,每当出现 bug 时,我都会先编写单元测试,然后再进行调试。然而,在编写单元测试时,我需要记住它们不会永远存在。我的静态网站生成器工具正在快速迭代:重构、添加新功能、修复 bug 并发布新版本,每天都在进行。也就是说,单元测试很快就会过时,我最终可能会花费更多的时间维护单元测试,而不是真正开发工具本身。因此,我编写单元测试的理念是:只在真正需要的时候才编写,例如当代码出错的后果很严重,或者当它们能够解决特定问题时。
结论
Hacktoberfest 对我来说是一个完美的契机,让我开始为开源项目做贡献。与社区保持联系,并向资深开发者或经验丰富的维护者学习,是我这个月迄今为止收获最大的部分。
文章来源:https://dev.to/batunpc/unit-testing-like-a-hacker-1e3m
