重新思考软件开发中的单元测试
When it comes to unit testing, there is always two sides for the coin. Does unit testing pay off? Is it worth it?
对于单元测试,硬币总是有两个方面。 单元测试能带来回报吗? 这值得么?
Be humble about what your unit tests can achieve, unless you have an extrinsic requirements oracle for the unit under test. Unit tests are unlikely to test more than one trillionth of the functionality of any given method in a reasonable testing cycle.
对单元测试可以达到的目标保持谦逊,除非您对被测单元有外部要求。 单元测试不太可能在合理的测试周期内测试超过给定方法任何万亿分之一的功能。
— James O Coplien on “Why Most Unit Testing is a Waste”
— James O Coplien谈“为什么大多数单元测试是浪费的”
This article touches up on my thoughts on unit testing. If you’re looking for a hands-on, I will soon have a Go language tutorial on “Gorm for SQL in Go”. If you are new to unit testing, I suggest you read up some of these then try two different exercises:
本文修饰了我对单元测试的想法。 如果您需要动手操作,我将很快获得有关“ Gorm for SQL in Go ”的Go语言教程。 如果您不熟悉单元测试,建议您阅读其中的一些内容,然后尝试两个不同的练习:
-
Try building a dead simple project and add unit tests whenever testable. e.g. Building a Parking Lot system. Here is one simple problem, overkilled solution + unit test in my github. No third-party library allowed here.
尝试构建无效的简单项目,并在可测试时添加单元测试。 例如,建立停车场系统。 这是一个简单的问题, 我的github中的解决方案+单元测试过度。 此处不允许第三方库。
- Try building applications. For example, build a web application using a minimalist web framework + ORM / database connector library. Now remember, unit test is all about isolated test but your code now depends on other components. How do you add a unit test? And what benefits does it give you here? 尝试构建应用程序。 例如,使用简约的Web框架+ ORM /数据库连接器库构建Web应用程序。 现在请记住,单元测试是关于隔离测试的,但是您的代码现在取决于其他组件。 如何添加单元测试? 它给您带来什么好处?
表中的内容 (Table of Content)
I. Unit Testing — What is and what is NOT Unit Testing?
I.单元测试-什么是单元测试 ,什么不是单元测试?
II. Advantage of Unit Testing — How valuable is a well-writen Unit Tests?
二。 单元测试的优势 -编写良好的单元测试有多有价值?
III. Misbelief on Advantage of Unit Testing — What it won’t give you, ever.
三, 对单元测试优势的误解-永远不会给您带来什么。
IV. The Cost of Unit Testing — Cost of unit testing and Risk for writing a bad one.
IV。 单元测试的成本-单元测试的成本和编写不良代码的风险。
V. When is Unit Testing Valuable then? — Should you then do other kind of testing?
V.那么什么时候单元测试有价值? —然后您应该进行其他类型的测试吗?
这篇文章是关于什么的? (What this article about?)
I’m going to talk a bit about unit testing because I think it’s worth to pause and think:
我将讨论单元测试,因为我认为值得停下来思考:
- Does Unit Testing a must? I have seen many cases where it is not worth doing and its much worth to skip towards functional tests instead. Its not a norm either, despite many books and resources encourage unit testing and even TDD. As a software engineer, you must not blindly follow what the book, tutors, or what your tech lead says. 是否必须进行单元测试? 我已经看到很多情况下不值得做的事情,而值得去进行功能测试。 尽管有很多书籍和资源鼓励单元测试,甚至TDD,但这也不是一种规范。 作为软件工程师,您一定不能盲目地遵循书,导师或技术负责人所说的。
-
But it’s just unit testing, right? Isn’t it easy and quick? The answer is a big NO but of course it depends. For most of my backend application, it is a big NO and it is tedious to write a well written unit test. I’m talking just the unit tests, not functional or integration tests yet.
但这只是单元测试,对不对? 是不是容易又快捷? 答案是“ 否”,但当然要视情况而定。 对于大多数我的后端的应用,这是一个大NO,这是繁琐写一个写得很好的单元测试。 我说的只是单元测试,还不是功能或集成测试。
这不是什么意思? (What this is not about?)
-
This is NOT an anti unit testing neither I’m suggesting you to delete you testing code.
这不是反单元测试,也不建议您删除测试代码。
-
This is NOT a tutorial on how to write a test.
这不是有关如何编写测试的教程。
单元测试 (Unit Testing)
I think sooner or later we will have to talk about unit testing and why it is very difficult. Unit testing is not as easy as I thought would be back then. It requires one module to be isolated from its external dependencies such as database layer or API client.
我认为迟早我们将不得不谈论单元测试以及为什么它非常困难。 单元测试并不像我当时想象的那么容易。 它要求一个模块与其外部依赖项(例如数据库层或API客户端)隔离开。
Many developers that I spoke to misunderstand what is and what is NOT a unit test. For some of those, unit test is described as “just testing a single function” which is far from truth and it not easy once you created a fairly complex application.
我说过的许多开发人员会误解什么是单元测试,什么不是单元测试。 对于某些工具,单元测试被描述为“仅测试单个功能”,这与事实相去甚远,一旦创建了相当复杂的应用程序,这并不容易。
There are many formal definitions and articles greatly explaining what is Integration Test vs Unit Test. Here is a recap in my own language:
有许多正式的定义和文章极大地解释了什么是集成测试与单元测试。 这是我的母语回顾:
-
Integration Test: test more than one component and how they function together. For instance, how another system interacts with your system or the database interacts with your data abstraction layer. Usually this requires a fully installed system, although in its purest forms it does not.
集成测试:测试多个组件以及它们如何一起工作。 例如,另一个系统如何与您的系统交互或数据库如何与您的数据抽象层交互。 通常,这需要一个完全安装的系统,尽管不是最纯粹的形式。
-
Unit Test: is a level of software testing where individual units/ components of a software are tested. The purpose is to validate that each unit of the software performs as designed. A unit is the smallest part of any software. It usually has one or a few inputs and usually a single output which we call a function or method.
单元测试 :是软件测试的级别,其中测试软件的各个单元 /组件。 目的是验证软件的每个单元是否按设计执行。 单元是所有软件中最小的部分。 它通常只有一个或几个输入,通常只有一个输出,我们称之为函数或方法。
In a nutshell, unit testing requires three key points:
简而言之,单元测试需要三个关键点:
-
White Box testing. This means the code is executed as expected for known inputs and edge cases. For example, if you are testing your applications’ data layer, you need to test for SQL or Query that is generated by your ORM. Then see if the query is exactly like what you wanted when you use the ORM library.
白盒测试 。 这意味着代码将按已知输入和边缘情况的预期执行。 例如,如果要测试应用程序的数据层,则需要测试ORM生成SQL或Query。 然后查看查询是否与您使用ORM库时想要的查询完全相同。
-
Extreme Modularity and Code splitting. This one goes hand by hand with doing White box testing. Unit testing is about testing the smallest unit possible in any software. A modular code improves testability when you can build bottom up from the smallest and reusable units possible up to more complex function. However, code splitting could possibly destroy your software architecture you have designed earlier.
极端模块化与代码分割 。 这与白盒测试并驾齐驱。 单元测试旨在测试任何软件中可能的最小单元。 当您可以从最小且可重用的单元开始逐步构建自下而上的功能,再到更复杂的功能时,模块化代码可提高可测试性。 但是,代码拆分可能会破坏您先前设计的软件体系结构。
-
Isolation and Mock. Any dependency on other components, external or internal should be mocked. That is the hardline of TDD developer. Mocking in my view is the most difficult part. Your code must be well structured and adhere to SOLID principle. The scope of your classes and modules must be well defined so that when they are tested, you can mock the dependencies and vice versa.
隔离和模拟 。 对外部或内部其他组件的任何依赖都应被模拟。 这是TDD开发人员的强硬路线。 在我看来,模拟是最困难的部分。 您的代码必须结构合理并且遵守SOLID原则。 您必须很好地定义类和模块的范围 ,以便在测试它们时可以模拟依赖关系,反之亦然。
完善的单元测试的优势 (Advantage of Well-Implemented Unit Testing)
Hard work must pay off doesn’t it? Well, it depends but what I know for sure, testing is less but arguably quite a brain puzzle and more of hard work. Then what do you get from doing these unit testing?
努力工作一定会得到回报,不是吗? 好吧,这取决于但我可以确定的是,测试虽然少但可以说是脑力激荡,而更多的是艰苦的工作。 那么您从这些单元测试中得到什么呢?
In my view, there are two major advantages that a unit testing provides in long future. How long is long? Depends on your sprints but for me, starting from 2nd sprints cycle onward, I have already felt the benefit as follows:
我认为,单元测试在很长的将来会提供两个主要优点。 多长时间? 取决于您的冲刺,但对我来说,从第二个冲刺开始,我已经感受到了以下好处:
-
Automation for Catching Bugs. I don’t think this is a surprise and need explaination. Team collaboration in code sprints means lot of code being pushed, lots of changes, and hence requires automated testing before merging new features, bug fixing, refactoring, configs changes, etc.
自动捕获错误 。 我认为这并不奇怪,不需要解释。 代码冲刺中的团队协作意味着需要推送大量代码,进行大量更改,因此需要在合并新功能,错误修复,重构,配置更改等之前进行自动化测试。
-
Explainable, Interpretable Error Debugging. This is the part that I think most valuable. For me, the cost of doing unit test only worth if it saves me from days of debugging. Well written code can immediately pinpoint the exact reason for a bug to occur, in which specific function/method in which module. This feature, however, requires you to design your unit tests and develop the testing code well, including the result logging and test labelling.
可解释的,可解释的错误调试 。 这是我认为最有价值的部分。 对我来说,进行单元测试的成本只有在它使我免于调试的日子里才值得。 编写良好的代码可以立即查明错误发生的确切原因,其中包括哪个模块中的特定功能/方法。 但是,此功能要求您设计单元测试并很好地开发测试代码,包括结果记录和测试标签。
-
Unofficial Guide or Docs. Good tests can also serve as an unofficial guide to using the interface. This benefit actually applies to any kind of testing. The tests that are well written demonstrate how to use your API and what are the expected outcome in clear manner.
非官方指南或文档 。 好的测试也可以作为使用该接口的非正式指南。 此好处实际上适用于任何类型的测试。 编写良好的测试以清晰的方式演示了如何使用您的API以及预期的结果。
对单元测试优势的误解 (Misbelief on Advantage of Unit Testing)
-
More Coverage != Improving Code Quality. A well designed testing, at most, provides an insight on what does the current implementation is lacking to achieve ideal system. On the other hand, good system designs and programming do.
覆盖更多!=提高代码质量 。 精心设计的测试最多可以提供有关当前实现实现理想系统所缺乏的见解。 另一方面,良好的系统设计和编程也可以。
-
Write Test First makes one think more clearly. On the contary, performing TDD or writing test before added complexity in mind when writing an implementation. As if somehow there is more information in a test than in code. I have practiced TDD for 1 year and I am a Software Engineer at Test Automation before being a backend engineer, writing 100 unit tests and 67 end-end integration test. I can confirm that TDD discipline does not make one a better programmer. Practice on good designs and programming does!
首先编写测试可以使您的思路更加清晰。 在竞争中,在编写实现时要考虑增加复杂性之前执行TDD或编写测试。 好像测试中比代码中有更多信息。 我从事TDD已有1年的经验,在成为一名后端工程师之前,我是Test Automation的一名软件工程师,编写了100个单元测试和67个终端集成测试。 我可以证实,TDD学科并不能使一个程序员成为更好的程序员。 实践良好的设计和编程!
-
Less Failures and Safer. If you have comprehensive unit tests but still have a high failure rate in system tests or low quality in the field. Probably its time to rethink on your design solution rather than adding more unit tests and bug fixes. I can’t stress how important a good design is rather than having tests all over the place.
减少故障,更安全 。 如果您具有全面的单元测试,但系统测试中的故障率仍然很高,或者现场质量较低。 也许是时候重新考虑您的设计解决方案,而不是添加更多的单元测试和错误修复了。 我不能强调一个好的设计有多重要,而不是在整个地方进行测试。
单元测试的成本和弊端 (Cost and Darkside of Unit Testing)
… And what the program can do is somehow related to the number of bits on that tape at the start of execution. If you want to thoroughly test that program, you need a test with at least the same amount of information …
…该程序可以执行的操作与执行开始时该磁带上的位数有关。 如果要彻底测试该程序,则需要使用至少相同数量的信息进行测试……
— James O Coplien on “Why Most Unit Testing is a Waste”
— James O Coplien谈“为什么大多数单元测试是浪费的”
-
Time. Unit testing in general takes more time than actual implementation. In my case as backend developer, it took me twice the time I need to implement an application on average, regardless that I have familiarised with the testing suite or the web application framework I’m using. For some, it could take
时间 。 通常,单元测试比实际实施花费更多的时间。 就我作为后端开发人员而言,无论我是否熟悉测试套件或正在使用的Web应用程序框架,平均花了我两倍的时间来实现一个应用程序。 对于某些人来说,可能需要
-
Enforced refactoring and code splitting. Unit testing most of the time requires splitting up your algorithms simply to satisfy unit testing compliance, not for modularity sake. This could mean destroying your system architecture and code comprehension along with it to test at a better granularity.
强制重构和代码拆分 。 大多数时候,单元测试只需要拆分算法即可满足单元测试的要求,而不是出于模块化的考虑。 这可能意味着破坏您的系统体系结构和代码理解,以进行更好的粒度测试。
-
Badly written tests confuses. Badly written tests leads to confusion and unnecessarily create an obstacle to further changes. Bad tests risk introducing bugs in your software as well, and possibly pollute testing result if the tests (logic, result logging, etc) is not designed well. How many times have you been into performing unit test, but when tests fails you still need to perform heroic debugging?
写不好的测试会造成混乱 。 写得不好的测试会导致混乱,并不必要地造成进一步变化的障碍。 不良测试也可能会在您的软件中引入错误,并且如果测试(逻辑,结果记录等)设计不当,可能会污染测试结果。 您进行了几次单元测试,但是当测试失败时,您仍然需要执行英勇的调试?
-
Another Stack of Hays. If written badly, your unit test will only tell if your code is broken somewhere but that somewhere is a needle in the stack of hays plus another stack of hays that your testing code introduces. You will see stack of traced errors without any light on what brokes your code.
另一堆干草 。 如果写得不好,您的单元测试将只告诉您代码是否在某个地方坏了,但是某个地方是一堆干草,再加上测试代码引入的另一堆干草。 您将看到堆栈的跟踪错误,而不会弄清楚破坏代码的原因。
单元测试何时达到最大收益? (When Unit Testing Reach Maximum Benefit Then?)
… some systems have key algorithms — like network routing algorithms — that are testable against a single API. There is a formal oracle for deriving the tests for such APIs, as I said above. So those unit tests have value.
…某些系统具有可针对单个API进行测试的关键算法(例如网络路由算法)。 正如我上文所述,有一个正式的预言程序可以推导此类API的测试。 因此,这些单元测试具有价值。
— James O Coplien on “Why Most Unit Testing is a Waste” (p. 11)
— James O Coplien谈“为什么大多数单元测试都是浪费” (第11页)
Complex algorithms benefit greatly from unit testing. Algorithmic programs have well defined steps and input output, but at the same time they might have complex edge cases to handle. You want to ensure as much as possible that your implementation is exactly as the designed algorithm on paper and handles all expected edge cases.
复杂的算法从单元测试中受益匪浅。 算法程序具有明确定义的步骤和输入输出,但同时可能要处理复杂的边缘情况。 您希望尽可能确保您的实现与纸上设计的算法完全相同,并能处理所有预期的边缘情况。
给我举个例子! (Give me example!)
Now, let say have understood well the algorithm and you are ready for implementation part. The algorithm can be broken down into parts and the implementation of each part can start by adding a simple test to test against, then the implementation.
现在,假设您已经很好地了解了算法,您就准备好实现部分了。 该算法可以分解为多个部分,每个部分的实现都可以从添加一个简单的测试开始,然后再实现。
For example, if you are implementing Dijkstra algorithm, you this algorithm can be broken down into five steps:
例如,如果您要实现Dijkstra算法,则可以将该算法分解为五个步骤:
// minHeap is used in Dijkstra algorithm
// to ensure the node with minimum distance
// from source is popped first, hence the
// nearest path to target will be discovered
// first.
// Each node is represented by tuple of 2 ints:
// - nodeID: unique node identifier
// - dist: distance from source node
type minHeap [][2]int // nodeID, dist
func (h minHeap) Len() int { return len(h) }
func (h minHeap) Less(i, j int) bool { return h[i][1] < h[j][1] }
func (h minHeap) Swap(i, j int) { h[i], h[j] = h[j], h[i] }
func (h *minHeap) Push(x interface{}) {
// Push and Pop use pointer receivers because they modify the slice's length,
// not just its contents.
*h = append(*h, x.(int))
}
func (h *minHeap) Pop() interface{} {
old := *h
n := len(old)
x := old[n-1]
*h = old[0 : n-1]
return x
}
func dijkstra (src, dest int, graph map[int][][2]int) bool {
// Step 1: Initialise Data structure here ...
queue = new(minHeap)
heap.Push(&queue, src)
// Step 2: Initialise Associative Distance here ...
distance := make(map[int]int)
for node := range graph {
distance[node] = math.MaxInt32
}
distance[src] = 0
// Step 3: BFS with tweak: append = append(queue, src) node when new path
// from src-node dist is found to be less than current src-node dist
for len(queue) > 0 {
// deque next state/node
// it should be a minheap
node, dist = heap.Pop(queue).([2]int)
// Step 4: Finish and found dest
if node == dest { return true } // add whatever you need to yield
for idx, state := range graph[node] { // get neighbours
nextNode, weight = state[0], state[1]
// Step 5. Enqueue path which node distance from src
// is less than current known node distance from src
if distance[nextNode] > dist + weight {
distance[nextNode] = dist + weight
heap.Push(&queue, [2]{nextNode, dist+weight}
}
}
}
// when dest not found
return false
}
- Getting Started: Initializing Relevant Data Structure: PriorityQueue (min heap) and Distance Table 入门:初始化相关数据结构:PriorityQueue(最小堆)和距离表
- Initializing the Distance Associative Table, StartNode’s distance to Zero, others to infinity (e.g. MAX_INT) 初始化距离关联表,StartNode的距离为零,其他距离为无穷大(例如MAX_INT)
- Breadth-First Search, but with a bit of twist: using MinHeap (Priority Queue) instead of ordinary queue structure 广度优先搜索,但有一点曲折:使用MinHeap(优先级队列)代替普通队列结构
4. Check If Destination Node reached, return and exit
4.检查是否到达目标节点,返回并退出
5. While Destination Node isn’t reached, push next node to priority queue if we found a shorter distance.
5.在未到达“目标节点”的情况下,如果发现较短的距离,则将下一个节点推入优先级队列。
我们如何对Dijkstra进行TDD? (How we do TDD that Dijkstra?)
You can implement TDD for these steps and added a fairly simple test case to ensure their behavior meet your algorithmic design, handles all edge cases well. I recommend performing parameterised tests. Elliot Chance write a good blog on parameterised test in go.
您可以为这些步骤实现TDD,并添加一个相当简单的测试用例,以确保其行为符合您的算法设计,并很好地处理了所有边缘情况。 我建议执行参数化测试。 Elliot Chance 在go中撰写了有关参数化测试的不错的博客。
-
TestMinHeapPop()
: Test that ensuresminHeap.Pop()
always enqueue the last element in the array. The heap push ops ensures the last element to be element with minimum distance among all nodes in the heap.TestMinHeapPop()
:确保minHeap.Pop()
始终排入数组中最后一个元素的测试。 堆推操作确保最后一个元素成为堆中所有节点之间具有最小距离的元素。 -
TestMinHeapPush()
: Test that ensuresminHeap.Push()
added the item and restructure the heap such that the last item in the heap array is the one that contains minimum distance from node.TestMinHeapPush()
:确保minHeap.Push()
添加项并重组堆的测试,以使堆数组中的最后一项是包含距节点最小距离的项。 -
TestEnqueueNextNode()
: TheTestEnqueueNextNode()
is independent and isolated. If you happen to use implementation from other library for implementing theMinHeap
, you might want to mock it as wellTestEnqueueNextNode()
:TestEnqueueNextNode()
是独立且隔离的。 如果您碰巧使用其他库中的实现来实现MinHeap
,则可能也要对其进行模拟
As you finish an implementation, add edge cases tests into your software and build up more complex parts of the algorithm until everything is accomplished. You might want to make sure some important steps to be covered with your unit testing.
完成实现后,将边缘用例测试添加到软件中,并构建算法的更复杂部分,直到完成所有工作。 您可能要确保单元测试涵盖一些重要步骤。
Then functional tests can be added on top after you are done. Your functional tests can act like the sphere online judge in any CP or LeetCode-like practice. They tested against the whole algorithm as one component.
完成后,可以在顶部添加功能测试。 在任何CP或类似LeetCode的实践中,您的功能测试都可以像是在线领域的裁判一样。 他们将整个算法作为一个组成部分进行了测试。
TestDijkstra()
: This is a component test but good to add into your testing. Test your dijkstra algorithm with parameterised test. Put edge cases that you can think of as well.
TestDijkstra()
:这是一个组件测试,但是可以添加到您的测试中。 使用参数化测试来测试您的dijkstra算法。 放一些您也可以想到的案例。
Tips: I would also recommend marking the TestDijkstra()
as dependent test to TestEnqueueNextNode()
during development such that if the TestEnqueueNextNode()
fails, this test will be skipped until you fixed the dependen test first. The purpose is to make your test results less clutter and easier for you to look for the culprit bug.
温馨提示:我也建议标志着TestDijkstra()
作为因测试TestEnqueueNextNode()
在开发过程中,例如,如果TestEnqueueNextNode()
失败,本次测试将被跳过,直到你第一次固定的dependen测试。 目的是使您的测试结果更整洁,并使您更容易查找罪魁祸首。
翻译自: https://levelup.gitconnected.com/rethinking-unit-testing-in-software-development-11b948483ed