第三部分:理论二

第三部分:理论二

如何保证重构不出错呢?

  • 需要熟练掌握各种设计原则、思想、模式,还需要对所重构的业务和代码有足够的了解。
  • 除了这些个人能力因素之外,最可落地执行、最有效的保证重构不出错的手段应该就是单元测试(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());
	}
}

单元测试为何难落地执行?

  • 一般情况下,单元测试的代码量要大于被测试代码量,甚至是要多出好几倍。
  • 当开发任务紧了之后,就开始放低对单元测试的要求,一旦出现破窗效应,慢慢的,大家就都不写了,这种情况很常见。
  • 由于历史遗留问题,原来的代码都没有写单元测试,代码已经堆砌了十几万行了,不可能再一个一个去补单元测试。这种情况下,我们首先要保证新写的代码都要有单元测试,其次,每次在改动到某个类时,如果没有单元测试就顺便补上。
  • 还有人觉得,有了测试团队,写单元测试就是浪费时间。写好代码直接提交,然后丢给黑盒测试狠命去测,测出问题就反馈给开发团队再修改,测不出的问题就留在线上出了问题再修复。
posted @ 2021-10-04 20:11  起床睡觉  阅读(79)  评论(0编辑  收藏  举报