《Effective Unit Testing》 读书笔记 6 可读性 code smell

本书第二部分中主要介绍了可读性/可维护性/可靠性三个方面的code smell或者说反模式(anti-pattern)。
我们首先看可读性的code smell. 再次解释一下所谓可读性,就是代码的意图和行为能通过阅读代码就能得到准确而清晰的表达。

  • 原始的断言 (Primitive assertions)
    意思是assert所做的判断抽象层级过于低,是在测试代码的实现。如书中下面的例子:
@Test
public void outputHasLineNumbers() {
    String content = "1st match on #1\nand\n2nd match on #3";
    String out = grep.grep("match", "test.txt", content);
    assertTrue(out.indexOf("test.txt:1 1st match") != -1);
    assertTrue(out.indexOf("test.txt:3 2nd match") != -1);
}

其中的indexOf 和 magic number -1 都过于和java的String类的实现相关了。
修改后的版本用到了org.junit.JUnitMatchers#containsString()方法,对比之前,是不是可读性好了很多呢?

@Test
public void outputHasLineNumbers() {
    String content = "1st match on #1\nand\n2nd match on #3";
    String out = grep.grep("match", "test.txt", content);
    assertThat(out, containsString("test.txt:1 1st match"));
    assertThat(out, containsString("test.txt:3 2nd match"));
}

我们应该追求测试代码的本质,也就是正确的行为,而不是去测试代码的实现细节。

  • 超断言(Hyperassertions)
    这是指断言的内容过于具体和琐碎,或者过于脆弱,非本质的改变就会影响测试结果。或者说我们所测试的输出过于庞大,并且采用过于精细的比较去判断。
    文中举得一个例子是测试一段Log代码输出,简单的判断log要和预定的一套String完全一样。这样的问题在于如果稍微改一下log的格式,比如时间显示格式(假设测试的不是时间格式对不对),测试就会通不过,而且不看输出细节还不知道到底是哪里输出错了(理想情况是希望通过方法名直接知道什么错误而不需要看output)。
    不过我个人觉得,这种超断言在有些特殊情况下也是可以用的,比如一些UI测试直接用了截屏比较图片的方式,可以认为是很脆弱的超断言,因为简单的CSS改动就会导致测试通不过,但如果每个UI对象去测试的维护成本更高或者根本难以写出这种测试的话,图片比较方式也是可以接受的甚至是唯一现实可行的方案。
    这在我们系统中有个例子,就是Order Interface的测试,为了简单起见,直接拿接口的xml输出去和一个认为正确的文本比较是否完全一致,虽然写起来简单,也导致了测试的脆弱性,其中利弊需要权衡。

  • 逐比特位断言 (Bitwise assertions)
    这个其实也不用单独写出来,是一种特殊的原始断言,给个例子看看就明白了。

public class PlatformTest {
  @Test
  public void platformBitLength() {
    assertTrue(Platform.IS_32_BIT ^ Platform.IS_64_BIT);
  }
}

应该改成下面这样

public class PlatformTest {
 @Test
  public void platformBitLength() {
   assertTrue("Not 32 or 64-bit platform?",
     Platform.IS_32_BIT || Platform.IS_64_BIT);
   assertFalse("Can’t be 32 and 64-bit at the same time.",
     Platform.IS_32_BIT && Platform.IS_64_BIT);
 }
}
  • 附加的细节(Incidental details)
    这个是说单段的测试代码太长了,所有东西写在一个方法里,抽象层次混杂,无关的细节太多, 让人一眼看不出代码到底想要干啥,从而可读性不好。解决办法是抽方法出来,让测试方法保持在同一个抽象级别上。具体就不说了。

  • 分裂的人格 (Split personality)
    简单说来就是同一个测试方法里断言了几个不太相关的东西,可以说是不同的人格(personality)或者不同的兴趣点(interest)。当然,如何划分所谓不想关的事情需要具体情况具体分析,也可能不同人理解不同。根据不同情况,我们采取的措施可以是简单拆成不同的测试方法。如果拆方法还不够,那我们自然还可以拆成不同的测试类(相同部分可以提取抽象测试基类)。

  • 分裂的逻辑 (Split logic)
    这说的是测试的代码过于长导致看了后面的忘了前面的,或者逻辑被分隔在不同的文件中,看了这个文件忘了那个文件。
    书中的例子如下

public class TestRuby {
  private Ruby runtime;
  @Before
  public void setUp() throws Exception {
    runtime = Ruby.newInstance();
  }
  @Test
  public void testVarAndMet() throws Exception {
    runtime.getLoadService().init(new ArrayList());
    eval("load 'test/testVariableAndMethod.rb'");
    assertEquals("Hello World", eval("puts($a)"));
    assertEquals("dlroW olleH", eval("puts $b"));
    assertEquals("Hello World",
    eval("puts $d.reverse, $c, $e.reverse"));
    assertEquals("135 20 3",
    eval("puts $f, \" \", $g, \" \", $h"));
  }
}

上面这段代码的最大问题就在于 eval("load 'test/testVariableAndMethod.rb'"); 这句话。这个测试从外部加载了另一个ruby文件,读者不两个文件对比着看,根本不知道在测试什么。
解决办法是,如果testVariableAndMethod.rb这个文件内容不多的话,直接把文件内容插在测试代码里(inline)。如果一定要分开放的话,要和测试放在同一个目录下面,要能通过相对路径来访问。

  • 魔法数(Magic number)
    这个很好理解,一般来说解决方法是抽取常量。不过书中还写了一些特殊方式来解决,比如用独特的方法名来表明入参的含义。见下面这个例子
  public class BowlingGameTest {
  @Test
  public void perfectGame() throws Exception {
    roll(pins(10), times(12));
    assertThat(game.score(), is(equalTo(300)));
  }
  private int pins(int n) { return n; }
  private int times(int n) { return n; }
}
  • 过长的初始化 (Setup sermon)
    就是说写了一大堆的代码用于初始化测试所需的fixture,是一种特殊情况的Incidental details。解决方法无非抽方法,变量取好名字,保持同一方法中的抽象层级一致性。

  • 过保护的测试(Overprotective tests)
    测试一些非核心的并不是你测试代码需要测试的内容。

@Test
public void count() {
  Data data = project.getData();
  assertNotNull(data);
  assertEquals(4, data.count());
}

上面代码中的第一个null断言不需要写,因为这并不是这个测试方法所要测试的逻辑。虽然说隐式的假设data不为空不是完全正确,但是显式去判断这种无关紧要的东西会影响可读性。另外就算不写这句话,如果data为null了测试同样会抛错导致测试失败,所以也不会影响测试结果。

可读性的code smell到此介绍结束,下一章将介绍可维护性。

你可能感兴趣的:(《Effective Unit Testing》 读书笔记 6 可读性 code smell)