原文:https://martinfowler.com/articles/mocksArentStubs.html

这篇是Martin Fowler 大神对于单元测试的概念的分析,收获颇多。

该篇文章解释mock对象如何工作,如何鼓励基于行为的测试,社区如何围绕他们发展出不同风格的测试。

第一次听说术语“mock object” 是一些年前在极限编程社区,但人们没有很好的描述它,而且经常与stub混淆,差异主要在两方面,一方面是测试结果的教研不同:状态校验和行为校验。另一方面,与测试和设计共同发挥作用的方式截然不同,这里我描述为classical和mockist TDD风格。

常规测试

订单包含商品和数量,仓库保存不同的商品和数量,然后用仓库填充订单,2种不同的响应,商品足够填充订单,订单填充,仓库商品减少,商品不足,订单失败,仓库不变。XUnit 测试遵循典型4个阶段顺序,setup,exercise,verify,teardown。例子中订单是测试对象,我使用System Under Test,仓库是一个配合对象。这个风格是状态校验(state verification)

Mock对象的测试

setup阶段有些不同,分成2部分,data和expectations
关键不同点是我们通过订单的反馈来校验它是否做了对的事情,状态校验通过仓库的状态判断。Mock使用行为校验。

使用EasyMock

EasyMock使用 记录/回放 (record/replay)隐喻去设置期望。每一个对象创建一个control和mock对象。可以添加额外的特性,可以调用实际的方法,不用传方法名。

Mocks与Stubs的不同

当你专注于一个软件元素时,也就是我们通常的单元测试,做一个单一的单元测试的问题时,我们经常需要其他的单元,因此我们的例子中需要仓库。

在之前我们展示的2种测试风格中,第一个例子使用真实的仓库对象,第二个使用模拟的仓库对象,使用mocks是一种不适用真实仓库的方法,但还有其他形式不使用真实对象的方法。
在讨论这些的时候词汇容易混乱,stub,mock,fake,dummy。这里我们使用Gerard Meszaros的书。Mezaros使用术语Test Double代表任何类型的假装对象,来替换真实对象。
Dummy对象是传递,但不会实际使用。通常他们知识填充参数列表。
Fake对象实际工作与实现,但通常使用一些捷径,不满足生产(比如内存数据库)
Stubs提供一个调用的罐头类回答,通常不回答任何外界的要求。
Spies当他们被调用的记录一些信息,一种形式是email服务记录有多少消息发送
Mocks编程前给予期望,特定调用的响应。

差异的选择

文章中已经讨论了不同:/状态或行为校验 /classic or mockist TDD。选择行为状态 vs 行为呢?
第一个是考虑上下文。如果时间简单的配合,没有什么选择。mockist就是mock对象,行为校验。classicist确实需要做选择,但不是特别大,通常是一个个的解决。
cache是一个比较难classical的方式去解决,mocj是一个好方式。

Driving TDD

Mock对象是从XP社区出来的,而且重点发展TDD是XP的特性原则之一,那里系统设计通过写测试迭代发展。
因此不用惊讶mockists,尤其是mockist testing 在设计上的效果的谈论,尤其他们贡献了一个叫need-driven development 的方式。通过这种方式,你通过写一个测试开始你的用户故事,给你的SUT制定一些接口,通过合作者的一些期望,你开放SUT与关联对象的关系-有效设计SUT的关联接口。

一旦你有了一个测试在运行,在mocks对象上的期望提供下一步的规范和测试的起点。你一个添加测试上的期望在合作者上,然后重复过程在SUT上,这种方式在分层系统中工作得很好。你第一步写UImock,然后写耕更低层次的测试用例,逐渐写每一层,这是一种结构化和可控的。很多人相信对于新的面相对象和TDD新人是有帮助的。

Classic TDD没有提供这种指导,你可以做相似的步骤,使用stub方法替代mocks。当你需要合作者的时候,你写死代码来响应SUT的工作。

但是classic TDD可以做另外的事情,一种通常的风格是middle-out。在这种风格,你做一个特性,决定你需要在domain中。
我应该强调mockists和classicists做了同一件事情,有这么一个学校教大家,一层层去构建系统,不是开始一层指导另外一层完成。classicist和mockist都趋向于敏捷背景和更容易获得的迭代循环,结果他们逐个特性地工作而不是逐层工作。

夹具设置

Classic TDD 你需要创建的不仅仅是SUT,同时也要创建所有的合作者来响应SUT测试,测试通常会调用一大堆合作对象,通常这些对象每次测试被创建和销毁。
Mockistest只要创建SUT和模拟相关的邻居,者可以避免创建复杂的夹具。
实际中,classic测试者趋向于重用复杂的夹具。最简单的方式是将夹具放在xUnit setup方法中,更复杂的夹具,几个测试类都需要用的,需要生成类,我通常称他们是Object Mother
结果我听到双方都控诉对方做了太多的工作,mockists说创建夹具做了太多的努力,但classicist说这个可以重用,但你不得不每次都创建mocks。

测试隔离

如果你通过mockist测试引入一个bug,它通常只引起SUT包含bug的失败。通过classic的方式,很多相关的可能都失败,如果一个重用的对象,那会导致很多的失败。
Mockist测试者因为这是一个主要的问题,它导致很多debugging,为了发现错误的根源与修复它。但classicist看起来不是什么问题,因为一般是最后编辑的代码有问题,你很快会发现。

测试粒度是一个重要的因素。因为classic测试执行多个真实对象,你经常会发现单个测试有一簇对象,很难发现真实bug的源码,测试粒度太粗会发生。
本质上xunit测试不仅仅是单元测试,也包括最小化的集成测试。

实现与测试耦合

当你写mockist测试,你需要调用SUT,确保它提供的正确的,classic测试只关心最终状态,不关心状态如何传递,Mockist测试更多与实现耦合,改变合作方法会导致mockist测试损坏。
这些耦合导致一些关注,最重要的是对TDD的影响。mockist测试,书写测试要确保实现的行为,相反mockist测试者把这个看成优点。Classicists,认为只关心外部的接口,留下所有实现关联直到测试写完。

设计方式

这些测试方式最让我着迷的方面是他们如何影响设计决定。
我已经提到一些不同层面的踪迹。Mockist测试者支持从外到内的实现,classic测试更倾向于domain model的方法。

该选择classicist或mockist ,我发现这是一个难回答的问题。

例子

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
public class Main {
public static void main(String[] args) {
InvocationHandler handler = new InvocationHandler() {
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
System.out.println(method);
if (method.getName().equals("morning")) {
System.out.println("Good morning, " + args[0]);
}
return null;
}
};
Hello hello = (Hello) Proxy.newProxyInstance(
Hello.class.getClassLoader(), // 传入ClassLoader
new Class[] { Hello.class }, // 传入要实现的接口
handler); // 传入处理调用方法的InvocationHandler
hello.morning("Bob");
}
}


interface Hello {
void morning(String name);
}

在运行期动态创建一个interface实例的方法如下:

  1. 定义一个InvocationHandler实例,它负责实现接口的方法调用;
  2. 通过Proxy.newProxyInstance()创建interface实例,它需要3个参数:
    1. 使用的ClassLoader,通常就是接口类的ClassLoader;
    2. 需要实现的接口数组,至少需要传入一个接口进去;
    3. 用来处理接口方法调用的InvocationHandler实例。
  3. 将返回的Object强制转型为接口。

本质上运行期创建字节码并加载

题目

给定两个以字符串形式表示的非负整数 num1 和 num2,返回 num1 和 num2 的乘积,它们的乘积也表示为字符串形式。

示例 1:

输入: num1 = “2”, num2 = “3”
输出: “6”
示例 2:

输入: num1 = “123”, num2 = “456”
输出: “56088”
说明:

num1 和 num2 的长度小于110。
num1 和 num2 只包含数字 0-9。
num1 和 num2 均不以零开头,除非是数字 0 本身。
不能使用任何标准库的大数类型(比如 BigInteger)或直接将输入转换为整数来处理。

来源:力扣(LeetCode)
链接:https://leetcode-cn.com/problems/multiply-strings
著作权归领扣网络所有。商业转载请联系官方授权,非商业转载请注明出处。

难点

不能直接转整数,那就需要去模拟乘法,
然后就是容易超出整数的范围
我只想到了一位数字乘以被乘数,但还是会超出位数限制,long也没有110位

解题思路

看到一个评论,一位一位计算,放入数组

错误1 要添加以前位,不光是最后一位
0 * 0 返回 “”,因为去0导致

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
class Solution {
public String multiply(String num1, String num2) {
int n1 = num1.length();
int n2 = num2.length();
if(n1 < 0 || n2 < 0)return "";
int[] result = new int[n1 + n2];
for(int i = n2 -1; i >= 0 ; i--){
for(int j = n1-1; j >=0 ; j--){
int one = num2.charAt(i) - '0';
int two = num1.charAt(j) - '0';
int sum = one * two + result[i+j+1];
result[i+j] += sum/10;
result[i+j+1] = sum%10;
}
}
StringBuilder sb = new StringBuilder();
boolean zero = true;
for(int k = 0 ; k < n1+n2 ; k++){
if(result[k]!=0){
zero = false;
}
if(!zero){
sb.append(String.valueOf(result[k]));
}

}


return sb.toString().equals("")?"0":sb.toString();
}
}

最近加班有点多,长时间盯着屏幕,一天去星巴克,突然感觉头一动就晕,那种感觉真的不好,我一回家就躺在床上,稍微一动就天旋地转,那一刻感觉健康真的很重要,有些病人求死的心理我多少也能理解了,之前我还一直抱有不管怎么样都要活下去。我发现这是我的理想化,没有健康的身体,其他真的很难实现,年纪逐渐变大,也许哪天就不再是有什么病,睡一觉就好了,到那一天就晚了,要主动关心常见病,做好预防,预防才是治病的根本,得了病就太晚了。

年底去参加公司的年会,新公司年会的中奖率很高,我也中了3等奖,但在大家参与狂欢中,我发现狂欢的是员工,真正的领导不靠运气赚钱,领导都是发,靠运气挣钱肯定不如踏踏实实工作一年,所以年会上都是小奖,真正的大奖也不会靠运气,领导的年终奖可能要好几倍。所以挣钱不能靠运气,要靠脚踏实地的努力。将大目标拆解成小目标,一步步完成还是很重要的,不能有暴富心理,搏一搏,一下上位。

而且年龄越来越大,恐慌感是真的,怕被裁员,要给别人打工都是一样,随时可能被裁员,重要的是你有什么能力,可以自己去干,销售能力我觉得就很重要,把我市场的能力,是经营的第一步。

题目

编写一个函数,其作用是将输入的字符串反转过来。输入字符串以字符数组 char[] 的形式给出。

不要给另外的数组分配额外的空间,你必须原地修改输入数组、使用 O(1) 的额外空间解决这一问题。

你可以假设数组中的所有字符都是 ASCII 码表中的可打印字符。

示例 1:

输入:[“h”,”e”,”l”,”l”,”o”]
输出:[“o”,”l”,”l”,”e”,”h”]
示例 2:

输入:[“H”,”a”,”n”,”n”,”a”,”h”]
输出:[“h”,”a”,”n”,”n”,”a”,”H”]

来源:力扣(LeetCode)
链接:https://leetcode-cn.com/problems/reverse-string
著作权归领扣网络所有。商业转载请联系官方授权,非商业转载请注明出处。

解题思路

方法1
双指针

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
class Solution {
public void reverseString(char[] s) {
int first = 0;
if(s.length <= 1){
return;
}
int last = s.length-1;
while(first < last){
char temp = s[first];
s[first] = s[last];
s[last] = temp;
first++;
last--;
}
}
}

执行用时 :1 ms, 在所有 java 提交中击败了100.00%的用户
内存消耗 :51.6 MB, 在所有 java 提交中击败了80.05%的用户

之前在hcrm就专门花了很多时间去看如何写单元测试,但当时想通过他解决所有问题,直接保证质量。但单元测试并不能完全解决问题,可以解决一部分问题,而且受限于当时的场景,发现单元测试不解决不了太多问题,就没实际尝试。时隔3-4年又重新拿起来这个问题,但场景变了,环境变了,一些又那么理所当然,正好有人写了单元测试,我就趁机入门一下,发现原来也不难。

项目使用的是spring cloud这套东西,但首先要先弄清楚什么是单元测试,什么是集成测试,什么是端到端测试。单元测试应该不依赖其他系统,自己就可以快速的执行,马上得到结果。如果与数据库关联,与其他系统关联就是集成测试,需要准备其他的环境。第一个条件是定义,那第二个条件就是弄清楚项目的情况,比如我的项目都是spring boot的,那spring 的依赖注入如何解决,servlet容器如何解决,这是第二步看关联。
比如我之前就很迷惑,Aservice里注入Bservice,Bservice注入Cservice,Cservice里注入Dservice,那我要使用Aservice,要不断new B、C、Dservice这个好麻烦,如果有一些不熟悉的复杂的service那就头大了。一种方式就是将整个spring容器启动起来,这样就是所有bean都有,直接取注入或者取都行,但这样就不算单元测试了,启动容器要加载所有bean,可能会很慢,而且很多我并不需要,之前就是卡在这里没过去。最近看了别人写的代码,发现通过代理的方式很好的实现了,先将要代理的对象注入,启动的时候自动代理这些对象,将要执行的方法的返回值,提前输入,等到触发代理条件,自动返回,这样就不用去实际创建这些对象,理解了这个用途,突然觉得代理强大,这个地方用的恰如其分。

然后我写了一些业务代码,去stub这些service也有些烦,mockito不支持私有方法stub,需要找到公开的方法一点带你的mock,这点有点烦,也许是我用法不对,而且一些看起来很简单的判断逻辑要想每种条件都测试,也是很麻烦的事情。
这次算是单元测试入门,什么事情都是入门难,先快速入门,其他都好办了。

你不能保证你代码是正确的,只能证明他是错的。
如果没有测试,它就是坏的。
因为java是静态语言,程序员对于语言的安全性太多舒服,想“编译通过了,那没问题了”。但是静态类型校验,有很大的限制。它只代表满足语法和基本类型规则。不意味着到达你编程的目的。

单元测试
这个在你创造的代码中建立集成测试的过程,每次构建系统的时候运行他们。这样构建过程不仅仅检查语法错误。你教它检查语义上的错误。
“单元”涉及最小块代码测试的想法。每个类都测试方法的行为。“系统”测试不同,检查完成的程序是否满足要求。
C风格语言,尤其c++,注重性能甚于安全编程。开发java比c++更快的原因是因为java更安全。特性比如:垃圾收集,类型校验。集成单元测试在编译的时候,将扩展你的安全网,结果是更快的开发。你可以更简单和健壮的重构你的代码,当你发现设计和实现的缺陷时,一般情况下会生产更好的产品,更快。
我的测试经验开始于,保证书中代码的正确性,书中的每个程序必须能自动提取,和编译在一个合适的构建系统。这本书的构建系统是gradle,你安装jdk后, 简单的gradlew compilejava,来编译整本书的所有代码。。。。
JUnit
最初的junit发布于2000年,基于java1.0,没用到java反射机制,所以写单元测试是一个很麻烦和啰嗦的活动。我发现了设计的缺陷,写了自己的单元测试框架,这个框架走向了另一个极端,“尝试可能可行的最简单的方法”。自此Junit使用发射和注解提升了很多。最新版本添加了lambda的支持,在junit5中。
在最简单的junit使用中,每个方法代表一个测试,使用@Test注解。Junit确定这些方法为分立的测试,设定和运行一次只有一个,采取措施避免测试间的副作用。

覆盖率的错觉
测试覆盖率,或者叫代码覆盖率。更改的百分比,更好测试覆盖率。很多方法去计算覆盖率,一个有用的文章描述是java code coverage tools
太容易决定100%覆盖率是可接受的值。数值不太是测试有效性的一个好的测量值。你可能测试了所有,只有65%的覆盖率。为了100%你可能要浪费很多时间保证其他。
测试覆盖率是一个初略的数据是有效的,当你分析一个未知的代码。如果测试工具报告很低的数值,比如40%,告诉你覆盖率可能是无效。然后一个非常高数值也是可疑的,可能是一个知识不足的人下团队决定。
最好的覆盖率工具是发现未测试的代码库。但不依赖它告诉你测试的质量。

今天从地铁里听到一句话开始,地铁里的安全防范教育中的一句话,引起了我的注意,当时的情景是宾馆里一个妇女给前台递上一个字条求助报警,后面一个好心人发现这是一个骗局,警察到了也一下就醒悟的,原来是这样。然后节目点题,知骗才能防骗。

这让我回想中,确实是这样, 如果一点概念都没有很容易陷入骗子的陷阱,这些陷阱都是利用人性的弱点,所以你没有防备就会入坑。但你如果有个意识,哪里不太对,这样就很大概率不会受骗。这一点跟犯错有点像,比如芒格就收集别人的错误,好让自己不犯大错误。我觉得这个也可以说成知错才能防错,放在任何事情上好像都合理,你只有了解它才能会使用它。
对于领会了这个道理,你要怎么在日常去做呢?
1.去关注一些教人防骗的公众号和微博,比如警察的微博,但不是推送,要我有时间才去看,比如rss方式
2.收集人犯错的方式,比如批判性思维里就有总结,平常把这些犯错归总,自己犯的,别人犯的,都要去总结,之前看的搞笑新闻也可以去关注,但不能简单的总结,要想明白合理处。

最近在看批判性思维,第一遍看有一些收获,但已经记不清了,第二遍看,越发惊奇,原来事实竟然是这样。
之前从来没有思考过如何思考,虽然听过笑来老师的元认知,是思考思考的过程,感觉也是对的。也没有实际操作过,很久就忘记。这可能就是我们知道了那么多道理还是过不好一生的原因,很多事情都是停留在虚幻的概念的层次上,没有到实际的生活层面,没有可以操作的地方,所以没有内化,很快就忘了,道路都是对的,但没有知行合一。所以一个道理如果懂了,马上落实到生活中如何去做。然后不断检查,养成习惯。

人的感知是有屏蔽性的,对于自己认为不对的东西进行屏蔽。这样有一些好处,就是过滤有效信息,防止信息过载。但坏处就是我们要主动去调整这种天生的行为。直觉是千万年形成的,可能与现在的环境不符合,所以要做一些违背直觉的行为,每一次大脑进入默认模式,要提醒自己多审视自己的思考过程。说到这里,我想到了古人每日三省吾身。当时觉得很有道理,但这都是古人的事,我天生就比你们强,然后就过去了。一个人的思维模式,决定他见到的世界是什么样的。我们经常有这样的感觉,原来这个竟然是这样的,原来我虽然“看到”,但却一点意识都没有。那个东西好像在你的世界里压根没存在过,但当你思维扩展之后,好像生活处处都是。

人的默认行为被科学不断研究,有一定原因,但不那么符合现在的社会,可能被很多人利用,所以我们要克服直觉,默认行为,多思考我们为什么那么做。一方面要思考自己思考的过程,一方面要思考别人的思考过程,这也是了解世界的一种方式。比如最近我做签名合成单证的逻辑,开始看代码觉得就是一坨屎,这里面逻辑好乱,好像封装了一些方法,但也不那么顺畅。后面跟他一点点的询问,发现他的封装也有他固有的逻辑,在他的逻辑里,那种方式也是很合理的,只是他知道的我不知道,所以我开始不理解,甚至误解。虽然他做的不能算好,但绝不是我开始看的一无是处。我突然眼界开阔了,从原有的思维中成长了。不能有拒绝的态度,拒绝得知别人的思考,注定缩小你的视野,看不到一些事实。

以事实为基础,《原则》中的精准事实,最近也听到好多人这么说,想要获得事实也不那么容易,人的感知会蒙蔽你获得事实的途径。要不断反思。
以后每天反思一个自己的思考行为看是否合理。每天记录反思日记。

第一遍看源码分析流程

入口时dispatchServlet的doDispatch方法,processDispatchResult是请求结束的统一处理,不管是不是异常。
如果传入exception是不是空,说明发生了异常,判断是不是网页的异常。调用processHandlerException处理异常。

其中有一个handlerExceptionResolvers数组,是initStrategies方法在dodispatch初始化的时候初始化的。
找到可以处理这个异常的HandlerExceptionResolver,调用resolveException方法去处理异常,
首先shouldApplyTo方法,resolver是不是支持这个handler,如果支持调用doResolveException,然后调用doResolveHandlerMethodException
getExceptionHandlerMethod获取具体的调用方法,然后invokeAndHandle去实际调用异常处理方法。

只能说过了一个大致的流程,不能说完全懂了,里面很多细节还不清楚,我准备看一遍文档,梳理一下mvc的大致组件结构来再过一遍,看看会不会更清晰。