Freewind @ Thoughtworks scala java javascript dart 工具 编程实践 月结 math python english [comments admin] [feed]

(2015-01-15) 一个起初看起来无害的测试为什么最后产生了一堆麻烦

广告: 云梯:翻墙vpn (省10元) 土行孙:科研用户翻墙http proxy (有优惠)

最近在做一个功能,需要把一个使用jdk1.6的系统升级为jdk1.8。该系统已经在产品环境上运行了很久,并且作为上游数据源为其它几个系统提供服务,所以我们在升级时非常小心。

虽然在中途遇到了一些问题,不过基本上都解决了,直到最后发现了一个测试用例,在升级后总是失败。

这是一个很简单的测试,简单到几乎没有测任何业务逻辑。

测试用例

下面的测试用例经过了改写,去掉了项目相关的信息:

@Test
public void infoSyncShouldSucceedEvenWithEmptyRequestBody() throws Exception {
    MockHttpServletRequest request = new MockHttpServletRequest("PUT", "/users/info");
    request.setContent(new byte[0]);
    request.setContentType("application/json");

    MockHttpServletResponse response = new MockHttpServletResponse();
    servlet.service(request, response);

    assertThat(response.getStatus(), is(200));
}

可以看到,这个测试是希望系统提供的某个url,可以接受body为空的PUT请求,并且返回值为200的http code

这个用例在spring 3.x下是正常的。然而我们升级jdk时,发现原来的spring版本不能正常工作,就把它升级到4.x,结果测试失败了。

在4.x下返回值的http code是400,而不是200

同时经过我们的尝试,发现如果写成:

request.setContent("{}".getBytes());

即提供一个{}来代表"空"json,这个测试就可以通过。

测试分析

如果先不看业务,只看测试本身,我觉得把它当作一个不合法的请求,返回400,更有意义一些。因为put通常是用来改变一些东西,如果请求里没有body,作为服务器,我会觉得有点难以理解,你到底想干什么?!

而且,指定了资源类型是application/json,那么给一个{}作为空json,似乎更说得过去。

不过结合业务,我觉得空body也可以理解。因为这里所测试的那个url对应的功能,是把用户发送的数据,原本地转发给另一个系统,它可能觉得这种判断自己就不需要多做了。

但是这么想来,这个测试本身又有矛盾了,因为它判断的是返回的http code是200,并没有说明这个200是后端系统给的,还是直接自己处理的。通过这个测试本身,实在是很难判断它到底想测什么。

所以我想这个测试的问题在于:它没有反映出真实的需求

有可能当时并没有这样的需求,只是作者觉得自己应该测试一些边缘情况,就顺手写了一个测试。也可能有这样的需求,但是在这个测试中,并没有很好的展现出来,导致现在已经无法知道它到底想测什么了。

与作者及相关人员的讨论

为了清楚作者当时的意图,我们尝试与作者联系,可惜他当前正在休假,委托给其他人帮我们解决。但是其他人也不清楚当时的需求是什么,最后经过讨论,有了这样两个建议:

  1. 我们可以去检查当前所有使用到该系统的项目,是否会发送空body,再来决定如何处理
  2. 如果只从测试角度看,似乎是希望对于空body也会返回200,我们最好还继续保持这个行为

似乎没有人清楚需求到底是什么,我们只能根据实际情况或者保险的方式来处理这个问题。同时,我们发现在版本系统中,看到这段代码是一年半以前写的,所以就算是作者在,他也很可能不清楚,当时为什么要写这个测试了。

对于方案1,我们需要检查当前所有使用了该服务的下游系统,仔细分析它们的代码来确定行为。这是巨大的工作量,而且仅仅通过代码可能还不能保证正确,还要找到熟悉那个系统的人,问问他们的意见。

对于方案2,初看似乎简单一些,但在我们阅读了spring 3.x和4.x相关源代码,以确定它们在内部到底做了什么后,发现比预想中的复杂。因为在3.x,这样的请求会被当作正确请求,继续执行后续逻辑,而4.x中spring直接就返回400了,不会执行后续逻辑。这个行为并没有在测试中反映出来,我们要不要保证?

准备尝试的方案

为了搞清楚相关的问题,以及阅读代码,与客户交流,已经花了我们的两天的时间。最后为了保险起见,我们决定采用下面的方式:

首先保证行为不变,然后在入口处增加一些log,看看有没有下游系统发送空的body。等过一段时间再来分析,如果有,还保持不变;如果没有,则把这个测试及新增的代码删除。

这是一位很有经验的客户给出来的建议,我们觉得这也许是最好的方案了。

我们已经感觉到那个测试本身不可信,所以只能通过分析已经存在的产品的行为来决定。

峰回路转

就在我们准备动手的时候,事情又有了新的进展。

客户通过对产品系统中相应的url发送了一个空body的请求,结果发现返回的http code居然是500。原来这个系统的上游系统,不能处理空body。

这样一来,我们前面准备使用的方案,似乎就没有什么意义了。因为:

  1. 测试没有反映真实需求,也没有反映真实行为
  2. 根据我们已经查明的某些下游系统,它们不会发送空body
  3. 返回500对于下游系统来说是没有意义的,他们肯定会避开

经过最终的讨论,我们决定把这个测试删除。

总结

我们为了解决这个问题,花费了很多时间和精力。由于这个系统我们并不熟悉,所以需要阅读它的代码以理解其行为,还要阅读spring 3.x和4.x相关代码以确定spring的行为,还需要与很多人交流,去讨论一个一年半以前的测试到底想测什么。

而这个测试本身看起来,似乎是微不足道,可有可无的。只是对于生产系统来说,哪怕改变一个这么微小的行为,都必须非常谨慎,因为谁也不知道它到底会影响什么。

当一个项目到了维护阶段,我们只能通过测试来了解当初的需求。如果一个不能反映出真实需求的测试,会给后来的维护带来巨大的麻烦。

这次我学到的是:

如果一个测试,如果没有需求,我们就不应该写,哪怕它很微小或者看起来似乎无害。如果有需求,则应该把需求搞清楚,并且在测试中把它真实地反映出来。

千万不要让它不清不楚的留在那里,然后在在未来的某个时间,浪费其他人几十倍的时间和精力。

comments powered by Disqus