Skip to content

Commit f8e2c85

Browse files
committed
add tdd & refactor
1 parent 5d6af08 commit f8e2c85

5 files changed

Lines changed: 919 additions & 4 deletions

File tree

chapters/10-tdd-with-autotest.md

Lines changed: 78 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1,78 @@
1-
#测试
1+
#测试
2+
3+
##一次测试驱动开发
4+
5+
虽然接触的TDD时间不算短,然而真正在实践TDD上的时候少之又少。除去怎么教人TDD,就是与人结对编程时的switch,或许是受限于当前的开发流程。
6+
7+
偶然间在开发一个物联网相关的开源项目——[Lan](https://github.com/phodal/lan)的时候,重拾了这个过程。不得不说提到的一点是,在我们的开发流程中**测试是由相关功能开发人员写的**,有时候测试是一种很具挑战性的工作。久而久之,为自己的开源项目写测试变成一种自然而然的事。有时没有测试,反而变得**没有安全感**
8+
9+
###故事
10+
11+
之前正在重写一个[物联网](http://www.phodal.com/iot)的服务端,主要便是结合CoAP、MQTT、HTTP等协议构成一个物联网的云服务。现在,主要的任务是集中于协议与授权。由于,不同协议间的授权是不一样的,最开始的时候我先写了一个http put授权的功能,而在起先的时候是如何测试的呢?
12+
13+
curl --user root:root -X PUT -d '{ "dream": 1 }' -H "Content-Type: application/json" http://localhost:8899/topics/test
14+
15+
我只要顺利在request中看有无``req.headers.authorization``,我便可以继续往下,接着给个判断。毕竟,我们对HTTP协议还是蛮清楚的。
16+
17+
```javascript
18+
if (!req.headers.authorization) {
19+
res.statusCode = 401;
20+
res.setHeader('WWW-Authenticate', 'Basic realm="Secure Area"');
21+
return res.end('Unauthorized');
22+
}
23+
```
24+
25+
可是除了HTTP协议,还有MQTT和CoAP。对于MQTT协议来说,那还算好,毕竟自带授权,如:
26+
27+
```bash
28+
mosquitto_pub -u root -P root -h localhost -d -t lettuce -m "Hello, MQTT. This is my first message."
29+
```
30+
31+
便可以让我们简单地完成这个功能,然而有的协议是没有这样的功能如CoAP协议中是用Option来进行授权的。现在的工具如libcoap只能有如下的简单功能
32+
33+
```bash
34+
coap-client -m get coap://127.0.0.1:5683/topics/zero -T
35+
```
36+
37+
于是,先写了个测试脚本来验证功能。
38+
39+
```javascript
40+
var coap = require('coap');
41+
var request = coap.request;
42+
var req = request({hostname: 'localhost',port:5683,pathname: '',method: 'POST'});
43+
44+
...
45+
46+
req.setHeader("Accept", "application/json");
47+
req.setOption('Block2', [new Buffer('phodal'), new Buffer('phodal')]);
48+
49+
...
50+
51+
req.end();
52+
```
53+
54+
写完测试脚本后发现不对了,这个不应该是测试的代码吗? 于是将其放到了spec中,接着发现了上面的全部功能的实现过程为什么不用TDD实现呢?
55+
56+
###说说测试驱动开发
57+
58+
测试驱动开发是一个很"古老"的程序开发方法,然而由于国内的开发流程的问题——即开发人员负责功能的测试,导致这么好的一项技术没有在国内推广。
59+
60+
测试驱动开发的主要过程是:
61+
62+
1. 先写功能的测试
63+
2. 实现功能代码
64+
3. 提交代码(commit -> 保证功能正常)
65+
4. 重构功能代码
66+
67+
而对于这样的一个物联网项目来说,我已经有了几个有利的前提:
68+
69+
1. 已经有了原型
70+
2. 框架设计
71+
72+
###思考
73+
74+
通常在我的理解下,TDD是可有可无的。既然我知道了我要实现的大部分功能,而且我也知道如何实现。与此同时,对Code Smell也保持着警惕、要保证功能被测试覆盖。那么,总的来说TDD带来的价值并不大。
75+
76+
然而,在当前这种情况下,我知道我想要的功能,但是我并不理解其深层次的功能。我需要花费大量的时候来理解,它为什么是这样的,需要先有一些脚本来知道它是怎么工作的。TDD变显得很有价值,换句话来说,在现有的情况下,TDD对于我们不了解的一些事情,可以驱动出更多的开发。毕竟在我们完成测试脚本之后,我们也会发现这些测试脚本成为了代码的一部分。
77+
78+
在这种理想的情况下,我们为什么不TDD呢?

chapters/11-refactor-project.md

Lines changed: 260 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -151,4 +151,263 @@ str = tableHandler(str, execStr, strict);
151151
✓ should return correctly class name
152152
```
153153

154-
快来试试吧, [https://github.com/artisanstack/js-refactor](https://github.com/artisanstack/js-refactor)
154+
快来试试吧, [https://github.com/artisanstack/js-refactor](https://github.com/artisanstack/js-refactor)
155+
156+
是时候讨论这个Refactor利器了,最初看到这个重构的过程是从ThoughtWorks郑大晔校开始的,只是之前对于Java的另外一个编辑器Eclipse的坏感。。这些在目前已经不是很重要了,试试这个公司里面应用广泛的编辑器。
157+
158+
##Interllij Idea重构
159+
160+
开发的流程大致就是这样子的,测试先行算是推荐的。
161+
162+
编写测试->功能代码->修改测试->重构
163+
164+
上次在和buddy聊天的时候,才知道测试在功能简单的时候是后行的,在功能复杂不知道怎么手手的时候是先行的。
165+
166+
167+
开始之前请原谅我对于Java语言的一些无知,然后,看一下我写的Main函数:
168+
169+
```java
170+
package com.phodal.learing;
171+
172+
public class Main {
173+
174+
public static void main(String[] args) {
175+
int c=new Cal().add(1,2);
176+
int d=new Cal2().sub(2,1);
177+
System.out.println("Hello,s");
178+
System.out.println(c);
179+
System.out.println(d);
180+
}
181+
}
182+
```
183+
184+
代码写得还好(自我感觉),先不管Cal和Cal2两个类。大部分都能看懂,除了c,d不知道他们表达的是什么意思,于是。
185+
186+
###Rename
187+
188+
**快捷键:Shift+F6**
189+
190+
**作用:重命名**
191+
192+
- 把光标丢到int c中的c,按下shift+f6,输入result_add
193+
- 把光标移到int d中的d,按下shift+f6,输入result_sub
194+
195+
于是就有
196+
197+
```java
198+
package com.phodal.learing;
199+
200+
public class Main {
201+
202+
public static void main(String[] args) {
203+
int result_add=new Cal().add(1,2);
204+
int result_sub=new Cal2().sub(2,1);
205+
System.out.println("Hello,s");
206+
System.out.println(result_add);
207+
System.out.println(result_sub);
208+
}
209+
}
210+
```
211+
212+
###Extract Method
213+
214+
**快捷键:alt+command+m**
215+
216+
**作用:扩展方法**
217+
218+
- 选中System.out.println(result_add);
219+
- 按下alt+command+m
220+
- 在弹出的窗口中输入mprint
221+
222+
于是有了
223+
224+
```java
225+
public static void main(String[] args) {
226+
int result_add=new Cal().add(1,2);
227+
int result_sub=new Cal2().sub(2,1);
228+
System.out.println("Hello,s");
229+
mprint(result_add);
230+
mprint(result_sub);
231+
}
232+
233+
private static void mprint(int result_sub) {
234+
System.out.println(result_sub);
235+
}
236+
```
237+
238+
似乎我们不应该这样对待System.out.println,那么让我们内联回去
239+
240+
###Inline Method
241+
242+
**快捷键:alt+command+n**
243+
244+
**作用:内联方法**
245+
246+
- 选中main中的mprint
247+
- alt+command+n
248+
- 选中Inline all invocations and remove the method(2 occurrences) 点确定
249+
250+
然后我们等于什么也没有做了~~:
251+
252+
```java
253+
public static void main(String[] args) {
254+
int result_add=new Cal().add(1,2);
255+
int result_sub=new Cal2().sub(2,1);
256+
System.out.println("Hello,s");
257+
System.out.println(result_add);
258+
System.out.println(result_sub);
259+
}
260+
```
261+
262+
似乎这个例子不是很好,但是够用来说明了。
263+
264+
###Pull Members Up
265+
266+
开始之前让我们先看看Cal2类:
267+
268+
```java
269+
public class Cal2 extends Cal {
270+
271+
public int sub(int a,int b){
272+
return a-b;
273+
}
274+
}
275+
```
276+
277+
以及Cal2的父类Cal
278+
279+
```java
280+
public class Cal {
281+
282+
public int add(int a,int b){
283+
return a+b;
284+
}
285+
286+
}
287+
```
288+
289+
最后的结果,就是将Cal2类中的sub方法,提到父类:
290+
291+
```java
292+
public class Cal {
293+
294+
public int add(int a,int b){
295+
return a+b;
296+
}
297+
298+
public int sub(int a,int b){
299+
return a-b;
300+
}
301+
}
302+
```
303+
304+
而我们所要做的就是鼠标右键
305+
306+
###重构之以查询取代临时变量
307+
308+
快捷键
309+
310+
Mac: 木有
311+
312+
Windows/Linux: 木有
313+
314+
或者: ``Shift``+``alt``+``command``+``T`` 再选择 ``Replace Temp with Query``
315+
316+
鼠标: **Refactor** | ``Replace Temp with Query``
317+
318+
####重构之前
319+
320+
过多的临时变量会让我们写出更长的函数,函数不应该太多,以便使功能单一。这也是重构的另外的目的所在,只有函数专注于其功能,才会更容易读懂。
321+
322+
以书中的代码为例
323+
324+
```java
325+
import java.lang.System;
326+
327+
public class replaceTemp {
328+
public void count() {
329+
double basePrice = _quantity * _itemPrice;
330+
if (basePrice > 1000) {
331+
return basePrice * 0.95;
332+
} else {
333+
return basePrice * 0.98;
334+
}
335+
}
336+
}
337+
```
338+
339+
####重构
340+
341+
选中``basePrice``很愉快地拿鼠标点上面的重构
342+
343+
![Replace Temp With Query](./img/replace.jpg)
344+
345+
便会返回
346+
347+
```java
348+
import java.lang.System;
349+
350+
public class replaceTemp {
351+
public void count() {
352+
if (basePrice() > 1000) {
353+
return basePrice() * 0.95;
354+
} else {
355+
return basePrice() * 0.98;
356+
}
357+
}
358+
359+
private double basePrice() {
360+
return _quantity * _itemPrice;
361+
}
362+
}
363+
```
364+
365+
而实际上我们也可以
366+
367+
1. 选中
368+
369+
_quantity * _itemPrice
370+
371+
2. 对其进行``Extrace Method``
372+
373+
3. 选择``basePrice````Inline Method``
374+
375+
####Intellij IDEA重构
376+
377+
在Intellij IDEA的文档中对此是这样的例子
378+
379+
```java
380+
public class replaceTemp {
381+
382+
public void method() {
383+
String str = "str";
384+
String aString = returnString().concat(str);
385+
System.out.println(aString);
386+
}
387+
388+
}
389+
```
390+
391+
接着我们选中``aString``,再打开重构菜单,或者
392+
393+
``Command``+``Alt``+``Shift``+``T`` 再选中Replace Temp with Query
394+
395+
便会有下面的结果:
396+
397+
398+
```javas
399+
import java.lang.String;
400+
401+
public class replaceTemp {
402+
403+
public void method() {
404+
String str = "str";
405+
System.out.println(aString(str));
406+
}
407+
408+
private String aString(String str) {
409+
return returnString().concat(str);
410+
}
411+
412+
}
413+
```

0 commit comments

Comments
 (0)