第三部分:理论二
第三部分:理论二
如何保证重构不出错呢?
- 需要熟练掌握各种设计原则、思想、模式,还需要对所重构的业务和代码有足够的了解。
- 除了这些个人能力因素之外,最可落地执行、最有效的保证重构不出错的手段应该就是单元测试(Unit Testing)了。
什么是单元测试?
- 文中举例,测试 Text 类中的 toNumber() 函数的正确性。
- 写单元测试本身不需要什么高深技术,它更多的是考验程序员思维的缜密程度,看能否设计出覆盖各种正常及异常情况的测试用例:
- 如果字符串只包含数字:“123”,toNumber() 函数输出对应的整数:123。
- 如果字符串是空或者 null,toNumber() 函数返回:null。
- 如果字符串包含首尾空格:“ 123”,“123 ”,“ 123 ”,toNumber() 返回对应的整数:123。
- 如果字符串包含多个首尾空格:“ 123 ”,toNumber() 返回对应的整数:123 。
- 如果字符串包含非数字字符:“123a4”,“123 4”,toNumber() 返回 null。
- 将其翻译成代码:
- Assert 类中有两个方法:assertEquals() 返回数字的断言,assertNull() 返回NULL的断言。
- TextTest 类中有五个方法,分别是翻译上文的五个测试用例,输出根据情况调用 Assert 类中的两个方法。
- TestCaseRunner 类中的 main() 方法分别调用 TextTest 类中的五个测试用例方法。
toNumber() 函数的测试:
public class Text {
private String content;
public Text(String content) {
this.content = content;
}
/**
* 将字符串转化成数字,忽略字符串中的首尾空格;
* 如果字符串中包含除首尾空格之外的非数字字符,则返回 null。
*/
public Integer toNumber() {
if (content == null || content.isEmpty()) {
return null;
}
//... 省略代码实现...
return null;
}
}
测试用例的实现:
public class Assert {
public static void assertEquals(Integer expectedValue, Integer actualValue) {
if (actualValue != expectedValue) {
String message = String.format(
"Test failed, expected: %d, actual: %d.", expectedValue, actualValue);
System.out.println(message);
} else {
System.out.println("Test succeeded.");
}
}
public static boolean assertNull(Integer actualValue) {
boolean isNull = actualValue == null;
if (isNull) {
System.out.println("Test succeeded.");
} else {
System.out.println("Test failed, the value is not null:" + actualValue);
}
return isNull;
}
}
public class TestCaseRunner {
public static void main(String[] args) {
System.out.println("Run testToNumber()");
new TextTest().testToNumber();
System.out.println("Run testToNumber_nullorEmpty()");
new TextTest().testToNumber_nullorEmpty();
System.out.println("Run testToNumber_containsLeadingAndTrailingSpaces()");
new TextTest().testToNumber_containsLeadingAndTrailingSpaces();
System.out.println("Run testToNumber_containsMultiLeadingAndTrailingSpaces()");
new TextTest().testToNumber_containsMultiLeadingAndTrailingSpaces();
System.out.println("Run testToNumber_containsInvalidCharaters()");
new TextTest().testToNumber_containsInvalidCharaters();
}
}
public class TextTest {
public void testToNumber() {
Text text = new Text("123");
Assert.assertEquals(123, text.toNumber());
}
public void testToNumber_nullorEmpty() {
Text text1 = new Text(null);
Assert.assertNull(text1.toNumber());
Text text2 = new Text("");
Assert.assertNull(text2.toNumber());
}
public void testToNumber_containsLeadingAndTrailingSpaces() {
Text text1 = new Text(" 123");
Assert.assertEquals(123, text1.toNumber());
Text text2 = new Text("123 ");
Assert.assertEquals(123, text2.toNumber());
Text text3 = new Text(" 123 ");
Assert.assertEquals(123, text3.toNumber());
}
public void testToNumber_containsMultiLeadingAndTrailingSpaces() {
Text text1 = new Text(" 123");
Assert.assertEquals(123, text1.toNumber());
Text text2 = new Text("123 ");
Assert.assertEquals(123, text2.toNumber());
Text text3 = new Text(" 123 ");
Assert.assertEquals(123, text3.toNumber());
}
public void testToNumber_containsInvalidCharaters() {
Text text1 = new Text("123a4");
Assert.assertNull(text1.toNumber());
Text text2 = new Text("123 4");
Assert.assertNull(text2.toNumber());
}
}
为什么要写单元测试?
1、单元测试能有效地帮你发现代码中的 bug
- 通过单元测试也常常会发现代码中的很多考虑不全面的地方。
- 写出 bug free 的代码,节省很多 fix 低级 bug 的时间。
2、写单元测试能帮你发现代码设计上的问题
- 对于一段代码,如果很难为其编写单元测试,那往往就意味着代码设计得不够合理,比如,没有使用依赖注入、大量使用静态函数、全局变量、代码高度耦合等。
3、单元测试是对集成测试的有力补充
- 程序运行的 bug 往往出现在一些边界条件、异常情况下,而大部分异常情况都比较难在测试环境中模拟。
- 对于一些复杂系统来说,集成测试也无法覆盖得很全面。
4、写单元测试的过程本身就是代码重构的过程
- 设计和实现代码的时候,我们很难把所有的问题都想清楚。而编写单元测试就相当于对代码的一次自我 Code Review。
5、阅读单元测试能帮助你快速熟悉代码
- 在没有文档和注释的情况下,单元测试就起了替代性作用。
- 单元测试用例实际上就是用户用例,反映了代码的功能和如何使用。
6、单元测试是 TDD 可落地执行的改进方案
- 测试驱动开发(Test-Driven Development,简称 TDD)是一个经常被提及但很少被执行的开发模式。毕竟很多程序员连单元测试都懒得写,更何况在编写代码之前先写好测试用例了。
- 单元测试正好是对 TDD 的一种改进方案,先写代码,紧接着写单元测试,最后根据单元测试反馈出来问题,再回过头去重构代码。
如何编写单元测试?
- 针对上文 toNumber() 函数的测试用例,利用 Junit 单元测试框架重新实现一下。
- 首先导入了 org.junit.Assert 和 org.junit.Test ,这样就省略了上文的 Assert 类和 TestCaseRunner 类。
- 只写一个 TextTest 类即可,功能与上文中一致。
1、写单元测试真的是件很耗时的事情吗?
- 尽管单元测试的代码量可能是被测代码本身的 1~2 倍,写的过程很繁琐,但并不是很耗时。
- 毕竟我们不需要考虑太多代码设计上的问题,测试代码实现起来也比较简单。
- 不同测试用例之间的代码差别可能并不是很大,简单 copy-paste 改改就行。
2、对单元测试的代码质量有什么要求吗?
- 单元测试毕竟不会在产线上运行,而且每个类的测试代码也比较独立,基本不互相依赖。
- 我们对单元测试代码的质量可以放低一些要求,命名稍微有些不规范,代码稍微有些重复,也都是没有问题的。
3、单元测试只要覆盖率高就够了吗?
- 单元测试覆盖率是比较容易量化的指标,常常作为单元测试写得好坏的判断标准。
- 不管覆盖率的计算方式如何高级,将覆盖率作为衡量单元测试质量的唯一标准是不合理的。
- 更重要的是要看测试用例是否覆盖了所有可能的情况,特别是一些 corner case。
- 过度关注单元测试的覆盖率会导致开发人员为了提高覆盖率,写很多没有必要的测试代码。
- 从过往的经验上来讲,一个项目的单元测试覆盖率在 60~70% 即可上线。
4、写单元测试需要了解代码的实现逻辑吗?
- 单元测试不要依赖被测试函数的具体实现逻辑,它只关心被测函数实现了什么功能。
5、如何选择单元测试框架?
- 写单元测试本身不需要太复杂的技术,大部分单元测试框架都能满足。
- 在公司内部,起码团队内部需要统一单元测试框架。
Junit 单元测试框架的使用:
import org.junit.Assert;
import org.junit.Test;
public class TextTest {
@Test
public void testToNumber() {
Text text = new Text("123");
Assert.assertEquals(new Integer(123), text.toNumber());
}
@Test
public void testToNumber_nullorEmpty() {
Text text1 = new Text(null);
Assert.assertNull(text1.toNumber());
Text text2 = new Text("");
Assert.assertNull(text2.toNumber());
}
@Test
public void testToNumber_containsLeadingAndTrailingSpaces() {
Text text1 = new Text(" 123");
Assert.assertEquals(new Integer(123), text1.toNumber());
Text text2 = new Text("123 ");
Assert.assertEquals(new Integer(123), text2.toNumber());
Text text3 = new Text(" 123 ");
Assert.assertEquals(new Integer(123), text3.toNumber());
}
@Test
public void testToNumber_containsMultiLeadingAndTrailingSpaces() {
Text text1 = new Text(" 123");
Assert.assertEquals(new Integer(123), text1.toNumber());
Text text2 = new Text("123 ");
Assert.assertEquals(new Integer(123), text2.toNumber());
Text text3 = new Text(" 123 ");
Assert.assertEquals(new Integer(123), text3.toNumber());
}
@Test
public void testToNumber_containsInvalidCharaters() {
Text text1 = new Text("123a4");
Assert.assertNull(text1.toNumber());
Text text2 = new Text("123 4");
Assert.assertNull(text2.toNumber());
}
}
单元测试为何难落地执行?
- 一般情况下,单元测试的代码量要大于被测试代码量,甚至是要多出好几倍。
- 当开发任务紧了之后,就开始放低对单元测试的要求,一旦出现破窗效应,慢慢的,大家就都不写了,这种情况很常见。
- 由于历史遗留问题,原来的代码都没有写单元测试,代码已经堆砌了十几万行了,不可能再一个一个去补单元测试。这种情况下,我们首先要保证新写的代码都要有单元测试,其次,每次在改动到某个类时,如果没有单元测试就顺便补上。
- 还有人觉得,有了测试团队,写单元测试就是浪费时间。写好代码直接提交,然后丢给黑盒测试狠命去测,测出问题就反馈给开发团队再修改,测不出的问题就留在线上出了问题再修复。