Confirmed: Code Coverage Is a Useless Management Metric
确认:代码覆盖率是无用的管理指标
Discover the simple proof that dismantles the code coverage metric
发现拆解代码覆盖率指标的简单证明
There is a strong belief that code coverage is a strong metric to measure the quality of a software product, a belief that’s been shared without question among tech leaders for many years. Its rationale seems sound on the surface: the more thorough the testing, the higher the code coverage, and consequently, the more robust and error-proof our software should be. That’s the idea that has been firmly planted in our minds. But what if I have proof that code coverage is fundamentally wrong? What if I could show you such a simple idea to get you out of doubt? So, get ready and brace yourself.
代码覆盖率是衡量软件产品质量的一个强有力的指标,多年来,技术领导者们对此深信不疑。从表面上看,其理由似乎很充分:测试越彻底,代码覆盖率就越高,因此,我们的软件就应该越健壮,越能防止错误。这就是我们脑海中根深蒂固的想法。但是,如果我有证据证明代码覆盖率从根本上就是错误的呢?如果我能向你展示这样一个简单的想法,让你不再怀疑呢?那么,请做好准备,振作起来。
Given this article shows which kind of metric are not useful for management —although it is very useful for developers— but does not show which ones you should follow, I recently wrote a followup explaining the four fundamental metrics you should use and why, backed by scientific evidence:
鉴于这篇文章说明了哪些指标对管理层没有用(尽管对开发人员非常有用),但没有说明你应该遵循哪些指标,我最近写了一篇后续文章,以科学证据为支撑,解释了你应该使用的四种基本指标及其原因:
The Code Coverage
代码覆盖范围
The code coverage, in its simplest form, measures how much of your code is being ‘touched’ or ‘covered’ by your tests. We assume that in our product, we have tested and run those tests at least before every release. When those tests execute, they perform operations with the product, making the code execute. Soon, we realize that if we track which code is executed by tests, we can start measuring how much code is executed. To the ratio of executed code to the total amount of code in our product, we give the name ‘code coverage’:
代码覆盖率的最简单形式就是衡量测试 "触及 "或 "覆盖 "了多少代码。我们假设,在我们的产品中,我们至少在每次发布之前都测试并运行了这些测试。当这些测试执行时,它们会对产品执行操作,使代码得以执行。很快,我们就会意识到,如果跟踪测试执行了哪些代码,我们就可以开始衡量执行了多少代码。对于已执行代码与产品中代码总量的比率,我们称之为 "代码覆盖率":
That is a very simple metric. If we have 100 lines of code but tests only execute 75 of them, we have code coverage of the 75%.
这是一个非常简单的指标。如果我们有 100 行代码,但测试只执行了其中的 75 行,那么我们的代码覆盖率就是 75%。
And soon, we realize something greater. If the code coverage is not 100%, we have code that is not executed by our tests, or in other words: we have untested code!
很快,我们就意识到了更大的问题。如果代码覆盖率不是 100%,那么我们的代码就不会被测试执行,或者换句话说:我们的代码未经测试!
Therefore, having untested code is dangerous because it can contain bugs. Furthermore, it may also contain business-critical functionalities that we can lose if we touch that code.
因此,未经测试的代码是危险的,因为它可能包含错误。此外,它还可能包含关键业务功能,如果我们触碰这些代码,就会失去这些功能。
So, having high code coverage is a must.
因此,代码覆盖率高是必须的。
The Code Coverage Fallacy
代码覆盖谬误
But, now we have a fallacy to face: we know that having uncovered code means that our tests are leaving important cases behind, but the opposite is not true.
但是,现在我们要面对一个谬误:我们知道,揭露代码意味着我们的测试会遗漏重要的情况,但事实并非如此。
For example, in the previous example, we had code coverage of 75%. Or in other words, the metric said that 25% of lines were not executed by any test, which points out an area of risk. We can say with certainty that any tests have not validated this 25% of the codebase and, therefore, could be a breeding ground for issues and maintenance problems.
例如,在前面的例子中,我们的代码覆盖率为 75%。换句话说,该指标显示有 25% 的代码行没有通过任何测试,这就指出了一个风险区域。我们可以肯定地说,任何测试都没有验证这 25% 的代码库,因此,这可能会成为问题和维护问题的温床。
Yet, here is when we risk into the fallacy: while we can confidently say that untested code hides potential errors and hindrances to future developments, we can believe that the opposite is true. We might believe that having the code covered means it has fewer bugs and fewer maintenance problems. But, that seemingly logical hunch may turn out to not be true.
然而,就在这时,我们有可能陷入谬误:虽然我们可以肯定地说,未经测试的代码隐藏着潜在的错误和对未来开发的阻碍,但我们也可以相信,事实恰恰相反。我们可能会认为,代码被覆盖就意味着它的错误和维护问题更少。但是,这种看似合乎逻辑的直觉可能会被证明是不正确的。
The reality is that we can have 100% of code coverage and still have code that’s full of bugs and is hard to maintain.
现实情况是,我们可以拥有 100% 的代码覆盖率,但仍然会有错误百出、难以维护的代码。
An Elementary Example
一个基本例子
Imagine a simple function that calculates the sum of two numbers:
试想一个计算两个数字之和的简单函数:
function addition(a, b) {
return a + b;
}
Which is the simplest test that will cover 100%? Just making one addition will make all the code execute:
最简单的测试是什么?只需增加一项,就能执行所有代码:
test('the addition function', () => {
addition(3, 4);
});
This test covers 100% of the code. Yet, it is useless. Why? If we change the addition implementation to this one:
该测试覆盖了 100% 的代码。然而,它却毫无用处。为什么呢?如果我们把加法的实现改成这样:
function addition(a, b) {
return a - b;
}
The test still passes!
测试仍然通过!
If you are a programmer, you probably already know where the problem is. The issue is not with the code coverage but with the test itself. The test covers 100% of the code but doesn’t assert or check for anything. That is why mistaking the implementation (subtracting instead of adding), the test still passes. So, it seems like a bad example… well, not.
如果您是程序员,您可能已经知道问题所在。问题不在于代码覆盖率,而在于测试本身。测试覆盖了 100% 的代码,但并没有断言或检查任何东西。这就是为什么错误的实现(减法而不是加法)仍能通过测试的原因。所以,这似乎是个坏例子......其实不然。
It turns out that for this very simple small example, we can easily see the problem with the test. But what will happen if the code base has hundreds of thousands of lines? Will anyone be able to easily pinpoint a test that does not properly validate its outcomes? It is highly unlikely.
事实证明,对于这个非常简单的小例子,我们很容易就能发现测试中存在的问题。但是,如果代码库有成千上万行代码,情况会怎样呢?会有人能轻易找出没有正确验证结果的测试吗?这种可能性很小。
So, tests can be faulty, assertions wrong, scenarios overlooked, and we could still boast a 100% code coverage metric. Here is precisely where the problem lies.
因此,测试可能有问题,断言可能有错误,场景可能被忽略,但我们仍然可以夸耀 100% 的代码覆盖率。这正是问题所在。
The Root Cause
根本原因
The root cause of this problem is that code coverage is a metric about code, not about business.
造成这一问题的根本原因在于,代码覆盖率是一个关于代码而非业务的指标。
Although it is a good indicator to uncover parts of the code that might be untested, it says very little about the business and how well the project satisfies the business objectives.
虽然这是一个很好的指标,可以发现代码中可能未经测试的部分,但它对业务以及项目在多大程度上满足业务目标的说明却很少。
Code coverage focuses on the technical aspect of software testing without necessarily factoring in the broader business objectives and requirements that the software is built to address. It measures the extent of the code that is tested but does not give insight into whether the software meets its intended purpose, fulfills user needs, or aligns with the broader business strategy.
代码覆盖率侧重于软件测试的技术方面,而不一定考虑软件所要实现的更广泛的业务目标和要求。它衡量的是已测试代码的范围,但无法深入了解软件是否达到了预期目的、满足了用户需求或符合更广泛的业务战略。
The only thing that code coverage does is to evaluate if you execute all the code during tests. And that is very easy to achieve:
代码覆盖所做的唯一一件事就是评估在测试过程中是否执行了所有代码。而这是很容易实现的:
Rule 1: Run all the methods. For each function, write a test that executes it. That will cover all methods. So, if you have two functions, write two tests.
规则 1:运行所有方法。为每个函数编写一个执行它的测试。这将涵盖所有方法。因此,如果有两个函数,就写两个测试。
function one() {
// ...
}
test('function one', () => {
one();
});
function two() {
// ...
}
test('function two', () => {
two();
});
Rule 2: Run all the branches. Create an additional test for each conditional to ensure it satisfies the condition. That will cover all the code inside any branch.
规则 2:运行所有分支。为每个条件创建一个额外的测试,以确保它满足条件。这将涵盖任何分支内的所有代码。
function conditional(condition) {
if (condition) {
// ...
} else {
// ...
}
}
test('condition true', () => {
conditional(true);
});
test('condition false', () => {
conditional(false);
});
Please note it is not always necessary to write additional tests to achieve 100% of code coverage:
请注意,并非一定要编写额外的测试才能实现 100% 的代码覆盖率:
function conditional(condition) {
if (condition) {
// ...
}
// ...
}
test('conditional', () => {
conditional(true);
});
There is no need for more rules. I have shown the ‘if’ statement, but the same applies to ‘while’ and ‘switch.’ Calls to other functions are already covered by rule 1, so that’s all.
不需要更多的规则。我已经展示了 "if "语句,但 "while "和 "switch "也是如此。对其他函数的调用已经包含在规则 1 中,所以就到此为止。
And what do these rules say about the business? Nothing. And that is the problem.
这些规则说明了什么?什么也没说。这就是问题所在。
Real Experiences
真实经历
I want to discuss two cases in which code coverage has been deceitful.
我想讨论两个代码覆盖有欺骗性的案例。
Some years ago, in a meetup, I met a fellow developer who was working in a software developer company, and he explained his experience in making a product ready for the FDA (Food and Drug Administration, a federal agency of the United States Department of Health and Human Services).
几年前,在一次聚会上,我遇到了一位在软件开发公司工作的开发人员,他向我介绍了他为 FDA(美国食品和药物管理局,隶属于美国卫生与公众服务部的一个联邦机构)准备产品的经历。
The situation was the following: the FDA required 60% code coverage, and their product had no tests—so 0% code coverage.
情况如下:FDA 要求 60% 的代码覆盖率,而他们的产品没有测试,因此代码覆盖率为 0%。
When the FDA requires 60% code coverage, they want to see that at least 60% of the software code is exercised during testing. This is a way to provide some assurance that the software works correctly under different conditions. Or at least, that is what they intended.
当 FDA 要求 60% 的代码覆盖率时,他们希望看到至少 60% 的软件代码在测试过程中得到验证。这是一种保证软件在不同条件下正确运行的方法。至少,这就是他们的初衷。
What really happened?
到底发生了什么?
Because they had no tests, they started to create tests. Initially, they tried to create meaningful tests, thoroughly examining the most critical functionalities and validating correct behavior under various conditions. But as time passed, creating those tests was hard, and the code coverage hardly increased. Soon, they realized they were racing against the clock.
由于没有测试,他们开始创建测试。起初,他们试图创建有意义的测试,彻底检查最关键的功能,验证各种条件下的正确行为。但随着时间的推移,创建这些测试变得越来越困难,代码覆盖率也几乎没有提高。很快,他们意识到自己在与时间赛跑。
Desperate times called for desperate measures. They shifted their focus from creating valuable tests to increasing the code coverage percentage. They executed tests, looked at the code coverage reports, and tweaked tests to pass through the largest portions of code to quickly increase the code coverage. They abandoned any consideration for useful tests, prioritizing quantity over quality.
绝境需要绝招。他们将重点从创建有价值的测试转移到提高代码覆盖率上。他们执行测试,查看代码覆盖率报告,调整测试以通过代码的最大部分,从而快速提高代码覆盖率。他们放弃了对有用测试的任何考虑,将数量置于质量之上。
It took three months, and he described it as the worst experience in his entire developer career.
历时三个月,他形容这是他整个开发人员生涯中最糟糕的经历。
Now, you are probably thinking that it was an extreme situation, that their acts were at least questionable, and that, surely, this is not a common practice in the software industry. Well, think again.
现在,你可能会想,这是一种极端的情况,他们的行为至少是有问题的,而且,这肯定不是软件行业的普遍做法。那么,请再想一想。
It turns out that every developer experiences the same ticking bomb for each delivery.
事实证明,每个开发人员在每次交付时都会遇到同样的定时炸弹。
So, if a developer is forced to deliver code with tests, have a certain minimal code coverage, and meet an arbitrary deadline (even if they have estimated it), the lessons from the previous experience also apply.
因此,如果开发人员被迫交付带有测试的代码,具有一定的最小代码覆盖率,并满足任意的截止日期要求(即使他们已经估计了截止日期),那么之前的经验也同样适用。
And that is where my second experience led me. Some time ago, one of my customers requested my assistance with one of his team’s tests. There were many discussions of tests, and a feel that tests were costly and time-consuming. The company required at least 80% code coverage, and the overall situation reminded me of my previous experience.
这就是我的第二次经历。前段时间,我的一位客户要求我协助他的团队进行一项测试。当时有很多关于测试的讨论,大家都觉得测试既费钱又费时。公司要求至少有 80% 的代码覆盖率,整个情况让我想起了之前的经历。
So, I did the only logical thing to do: I downloaded the code, I looked at the tests, and after one hour, I realized I could not make sense of any of them.
于是,我做了唯一合乎逻辑的事情:我下载了代码,查看了测试,一个小时后,我发现自己无法理解其中的任何内容。
I ran the tests, they passed, and I started experimenting. Because I did not understand how the tests were actually working, I got the code, and I broke it intentionally. The result surprised me: tests still passed, although the code was broken.
我进行了测试,测试通过了,于是我开始做实验。因为我不明白测试到底是如何进行的,所以我拿到了代码,并故意把它弄坏了。结果让我大吃一惊:虽然代码被破坏了,但测试仍然通过了。
The code coverage was achieved not because tests were thorough in their job but because they ran the code accidentally.
之所以能实现代码覆盖率,不是因为测试工作做得很彻底,而是因为他们不小心运行了代码。
Both experiences gave me a clear hint that forcing code coverage could not be a good management practice.
这两次经历都给了我一个明确的提示:强迫代码覆盖率并不是一种好的管理方法。
The Experiment
实验
As promised, I will present an experiment that’s so simple yet so effective that it will prove that code coverage as a management metric is useless without any doubt.
按照约定,我将介绍一个简单而有效的实验,它将证明代码覆盖率作为管理指标毫无疑问是无用的。
It is based on the following observation from Allen Holub:
它是基于艾伦-霍卢布的以下观点:
The idea is simple, right? As I mentioned earlier, there are only two rules that we need to satisfy to achieve a 100% code coverage: 1) execute all the functions, 2) execute all the branches. Well, it turns out that the Allen Holub proposal is exactly that: 1) make the tests execute all functions/methods, 2) cover branches by using random arguments.
这个想法很简单,对吗?正如我前面提到的,要实现 100% 的代码覆盖率,我们只需满足两条规则:1) 执行所有函数,2) 执行所有分支。事实证明,Allen Holub 的建议正是如此:1)让测试执行所有函数/方法;2)使用随机参数覆盖分支。
If we do that, what will this kind of test say about our business objectives? Nothing! It will just run mercilessly through all the code without considering our business. It will be the ultimate lazy developer.
如果我们这样做了,那么这种测试会对我们的业务目标产生什么影响呢?什么都没有!它只会无情地运行所有代码,而不会考虑我们的业务。这将是懒惰的开发人员的终极表现。
So, the question is: was Allen Holub right?
那么,问题来了:艾伦-霍卢布说得对吗?
Creating the automated code coverage may be challenging, but if we restrict to the premise of random inputs without the need to analyze the code branches, its complexity greatly reduces. So, let’s do it!
创建自动代码覆盖可能很有挑战性,但如果我们仅限于随机输入,而不需要分析代码分支,其复杂性就会大大降低。那么,让我们开始吧!
In my first approach, I picked Java. It is a fairly easy language to automate testing thanks to its reflection capacities, and I already had some public code repositories I could use to check the generator. So, I did a first proof of concept here:
在第一种方法中,我选择了 Java。由于具有反射功能,Java 是一种相当容易实现自动测试的语言,而且我已经有了一些公共代码库,可以用来检查生成器。因此,我在这里做了第一个概念验证:
This simple code only creates all instances of classes with a public constructor and no arguments and executes all the methods without arguments.
这段简单的代码只创建所有带有公共构造函数且不带参数的类实例,并执行所有不带参数的方法。
Although it’s simple, it has already achieved 11% code coverage. This is far below 80%, but it was expected.
虽然它很简单,但已经实现了 11% 的代码覆盖率。虽然远低于 80%,但这是意料之中的。
At this point, I would need to start executing constructors and methods with arguments. And also, I could execute directly private methods by ‘cheating,’ using the same mechanism that Spring or JPA depends on. That opened a new rabbit hole. So, at that point, with one first proof of concept in the right direction, and my opportunity to convert this experiment into a final degree project as a teacher at the university, I decided to list this experiment inside the final degree projects offered.
此时,我需要开始执行带有参数的构造函数和方法。此外,我还可以通过 "作弊 "的方式直接执行私有方法,使用 Spring 或 JPA 所依赖的相同机制。这就打开了一个新的兔子洞。因此,在这一点上,有了正确方向上的第一个概念证明,以及作为大学老师将此实验转化为最终学位项目的机会,我决定将此实验列入提供的最终学位项目中。
Here, I have to say that I am very grateful to Gerard Torrent (link to his LinkedIn here). He took the challenge, and although they had almost nothing about compiler theory in their degree, he created a different approach that enabled better understanding.
在此,我不得不说,我非常感谢杰拉德-托伦特(Gerard Torrent)(此处链接到他的 LinkedIn)。他接受了挑战,虽然他们的学位几乎没有编译器理论方面的知识,但他创造了一种不同的方法,让我们更好地理解了编译器理论。
Instead of doing one single test that walks through all the code, he built a code generator that created a test for each method and possible arguments. He continued adding functionalities, like when methods required other objects, creating them, and iteration by iteration, he raised the overall code coverage. Sometimes, he worked alone. Other times, we joined forces to improve the coverage one step further. And, oh boy, we did it.
他建立了一个代码生成器,为每个方法和可能的参数创建一个测试,而不是做一个走遍所有代码的测试。他不断增加功能,比如当方法需要其他对象时,他就创建这些对象,一次又一次地迭代,从而提高了代码的整体覆盖率。有时,他独自工作。有时,他独自工作;有时,我们联手进一步提高代码覆盖率。天啊,我们做到了。
The Results
结果
Yep, we did it. We achieved 80% code coverage and more.
是的,我们做到了。我们实现了 80% 的代码覆盖率,甚至更高。
I asked Gerard to iterate and take the results step by step to get more insights into how code coverage works.
我请 Gerard 进行迭代,一步一步地得出结果,以便更深入地了解代码覆盖的工作原理。
So, step by step, the code coverage achieved was:
因此,一步步实现的代码覆盖率是
- My first reference implementation: 11%
我的首次参考实施:11% - Executing all constructors with null values as arguments: 20%
执行所有以空值作为参数的构造函数:20% - Execute only public void methods: 23%
只执行公共无效方法:23 - Execute all public methods: 50%
执行所有公共方法: 50% - Execute all methods, public and private: 50%
执行所有方法,包括公共方法和私人方法:50% - Create instances of required arguments (no more null values): 65%
创建所需参数的实例(不再有空值):65% - Create instances for instances required (nested): 69%
为所需实例创建实例(嵌套):69% - Test three different values for each argument: 69%
测试每个参数的三个不同值:69% - Use Spring to instance classes when possible: 85%
尽可能使用 Spring 对类进行实例化:85%
Please note that testing private methods is an anti-pattern, don’t do that, but it was part of this demonstration because it can help to artificially increase the code coverage.
请注意,测试私有方法是一种反模式,不要这样做,但它是本演示的一部分,因为它有助于人为地增加代码覆盖率。
So, the final result was:
因此,最终的结果是
85% code coverage
85% 的代码覆盖率
That is generating code without any business consideration. So, now what?
这就是在不考虑任何业务因素的情况下生成代码。那么,现在怎么办?
The Conclusion
结论
The reason why Allen Holub targeted 80% in his comment was not because he thought that it was a reasonable target —he might–but because 80% is a common requirement for most companies. He was looking for a way to debunk the need for mandatory code coverage minimal.
Allen Holub 之所以在评论中将 80% 作为目标,并不是因为他认为这是一个合理的目标--他可能会这么认为--而是因为 80% 是大多数公司的共同要求。他在寻找一种方法来驳斥强制代码覆盖率最低的必要性。
So, now we know that we can build a simple library that, regardless of which is your business, can execute most of the code and artificially increase the code coverage. We need no AI, no fancy LLM, no code complexity analysis, execute random functions, and you will comply with most of the required minimum code coverage by any company.
因此,现在我们知道,我们可以构建一个简单的库,无论您的业务是什么,它都可以执行大部分代码,并人为地提高代码覆盖率。我们不需要人工智能,不需要花哨的 LLM,不需要代码复杂性分析,只需执行随机函数,就能满足任何公司对最低代码覆盖率的大部分要求。
Even in companies where the code coverage could be slightly higher, you could throw a couple of handcrafted manual tests to achieve that extra required coverage.
即使在代码覆盖率可以稍高一些的公司,也可以通过手工测试来达到额外的覆盖率要求。
So, what does using code coverage as a management metric result in? Nothing.
那么,将代码覆盖率作为管理指标的结果是什么呢?一无所获。
Previously, we knew developers could fake it to achieve higher code coverage without testing. Now, we also know that a quick automatic tool can increase it quickly.
以前,我们知道开发人员可以通过伪造代码来提高代码覆盖率,而无需进行测试。现在,我们也知道快速自动工具可以迅速提高覆盖率。
So, if merely executing code randomly can achieve a high degree of code coverage, it renders this metric useless.
因此,如果仅仅随机执行代码就能达到很高的代码覆盖率,那么这个指标就失去了作用。
The Next Step
下一步
Which is the next step? What can we do now, knowing that code coverage is useless for management?
下一步该怎么做?既然代码覆盖率对管理毫无用处,我们现在能做什么?
First and most important: code coverage is still important for developers. Many, including Martin Fowler, have said that for a long time. He explains in this article that the only purpose of code coverage is finding the untested code. That helps the developer discover his mistakes while creating the code and wrong assumptions. Correctly applied, code coverage failures can spark important business conversations that can uncover new functionalities or misunderstandings.
首先,也是最重要的一点:代码覆盖率对开发人员来说仍然很重要。马丁-福勒(Martin Fowler)等许多人早就说过这一点。他在这篇文章中解释说,代码覆盖的唯一目的是找到未经测试的代码。这有助于开发人员发现自己在创建代码时的错误和错误的假设。如果应用得当,代码覆盖失败可以引发重要的业务对话,从而发现新的功能或误解。
Second, there is TDD or BDD. Probably, without any doubt, is the only reasonable way to create tests. The main problem when developers are forced to create tests, and they create them after the code, is that no one can ensure that those tests work correctly. We need to watch them fail and see how the new code corrects those failures, and only that will reassure us that we created those tests correctly.
其次是 TDD 或 BDD。毫无疑问,这可能是创建测试的唯一合理方法。当开发人员被迫创建测试,而且是在代码之后创建测试时,主要的问题是没有人能确保这些测试能正确工作。我们需要观察它们的失败,看看新代码是如何纠正这些失败的,只有这样才能让我们确信我们创建的测试是正确的。
And finally, we should focus on business. Period. A test only makes sense if it helps verify that a business proposition is working as expected. So, instead of relying on an obscure metric focused only on code, we can choose other metrics more focused on business. One example is business rules coverage:
最后,我们应该专注于业务。期间。只有当测试有助于验证业务主张是否按预期运行时,测试才有意义。因此,与其依赖只关注代码的晦涩指标,我们可以选择其他更关注业务的指标。业务规则覆盖率就是一个例子:
It is a fairly simple metric, similar to code coverage, sharing a few of its problems, and yet more effective because it focuses on business. I have more details in some of my articles.
这是一个相当简单的指标,类似于代码覆盖率,但也存在一些问题。我在一些文章中介绍了更多细节。
I wanted to keep this article short, but I believed it necessary to explain some of the concepts here. Yet, I have several more resources you can use to better understand this issue, including my earlier Medium stories. Some of them are:
我想让这篇文章简短一些,但我认为有必要在这里解释一些概念。不过,我还有其他一些资源,包括我之前在 Medium 上的故事,你可以用来更好地理解这个问题。其中一些是
“Working software is the primary measure of progress.”
— Principles of the Agile Manifesto
"工作软件是衡量进步的主要标准"--《敏捷宣言》的原则
Thanks for the read. I usually like to write stories to think about how we understand and apply software engineering and to make us think about what we could improve. If you liked the article, check my most successful stories on Medium to read more.
感谢您的阅读。我通常喜欢通过写故事来思考我们是如何理解和应用软件工程的,并让我们思考可以改进的地方。如果你喜欢这篇文章,请在 Medium 上查看我最成功的故事,阅读更多内容。