文档章节

小程序中的大道理之四

国栋
 国栋
发布于 2014/12/03 17:43
字数 2614
阅读 3087
收藏 72

在讨论领域模型之前,先继续说下关于测试方面的内容,前面为了集中讨论相应主题而对此作了推迟,下面先补上关于测试方面的。

测试覆盖(Coverage)

先回到之前的一些步骤上,假设我们现在写好了getPattern方法,而getLineContent还处于TODO状态,如下:

public String getPattern(int lineCount) {
        if (lineCount < 1) {
            throw new IllegalArgumentException("行数不能小于1!");
        }
        if (lineCount > 20) {
            throw new IllegalArgumentException("行数不能大于20!");
        }
        
        StringBuilder pattern = new StringBuilder();
        for (int lineNumber = 0; lineNumber < lineCount; lineNumber++) {
            pattern.append(getLineContent(lineCount, lineNumber));
        }
        return pattern.toString();
    }

    private String getLineContent(int lineCount, int lineNumber) {
        // TODO Auto-generated method stub
        return null;
    }

显然,getPattern已经是ok的了,那么我们也应该为它写上一些测试了。

有人可能会想,现在到底能测试什么?毕竟它所调用的getLineContent还没有实现呢,这里好像没有什么业务逻辑可测试的。

但这里其实还是有些逻辑可测试的,最明显的,前面的两个前提条件,它是否能如我们所愿拦住那些错误的参数呢?对第一个条件让我们来测试一下:

    @Test(expected = IllegalArgumentException.class)
    public void testGetPatternSmallerThan1() {
        Pattern p = new Pattern();
        p.getPattern(0);
    }

这里用了一个小于1的参数“0”去调用它,并期待它能抛出相应的异常。

如果还想验证它的异常信息,可以这样写:

    @Test
    public void testGetPatternBiggerThan20() {
        Pattern p = new Pattern();
        try {
            p.getPattern(21);

            // 如果没有抛出异常,测试失败
            fail();
        } catch (Exception e) {
            // 检查抛出异常的类型及信息
            assertThat(e instanceof IllegalArgumentException).isTrue();
            assertThat(e.getMessage()).isEqualTo("行数不能大于20!");
        }
    }

当然,异常信息很简单,就是从源码中拷贝过来而已。可以让它带上所输入的参数,这样提示更有意义,从而也让我们的测试更有意义,如下:

assertThat(e.getMessage()).isEqualTo("行数不能大于20!输入值:21");

那么现在测试自然是不通过了,可以再运行一次来确认。那么现在再修改一下源码,在抛出异常信息的地方改成:

    public String getPattern(int lineCount) {
        if (lineCount < 1) {
            throw new IllegalArgumentException("行数不能小于1!输入值:" + lineCount);
        }
        if (lineCount > 20) {
            throw new IllegalArgumentException("行数不能大于20!输入值:" + lineCount);
        }
        
		// ......
    }

保存,再运行测试,如果这次通过了,那么你基本可确认你已经实现了需求。

以上实践已经非常接近测试驱动开发(TDD:Test Driven Development)所倡导的方式:

  1. 根据需求先写一些测试,而所测试的方法还没有实现这些需求,因此这些测试还不能通过;

  2. 接着再写源码实现那些需求并让测试通过。

这就是所谓的测试驱动。

说完了异常方面的测试,还有什么可测试的呢?这里真的没有其它业务逻辑可测试了吗?

没错,getLineContent确实还是空的,但不要纠结于这里,比方说:输入一个3,你调用了4次getLineContent,这不就错了吗?(可能的原因是在for循环部分的边界判断上没有写好)

那么怎么确切地去证明你的代码里只会不多不少只调用了3次呢?可以借助Mockito中的行为测试来验证这些逻辑:

    @Test
    public void testGetPatternTimes() {
        Pattern pattern = Mockito.spy(new Pattern());
        pattern.getPattern(3);
        
        // 验证方法调用的次数,但不关心方法的参数
        Mockito.verify(pattern, Mockito.times(3)).getLineContent(Mockito.anyInt(), Mockito.anyInt());
    }

以上代码中,用Mockito来构建了一个pattern,并调用了getPattern方法,接着再断言getLineContent被调用了3次(Mockito.times(3))

至于用Mockito.spy而不是用Mockito.mock,原因是mock方式会让所有方法被覆盖,除非显式使用when…then来指定方法的行为;而spy

则会保留原有方法的行为,除非显式when…then来显式指定新的行为。现在我们想测试getPattern方法,所以用spy。

如果你对Mockito还不太熟悉,也没关系,你只要明白这里在验证方法调用的次数就够了。可以改变一下,比如改成times(4),再跑下就会发现以下错误提示:

image

另一方面,你可能已经注意代码中的Mockito.anyInt方法,你大概也能猜出这表示不考虑具体传递的参数是什么,但传递的参数其实也是很重要的逻辑。虽然在调用次数上正确了,但如果没有传递正确的参数,自然也不能算正确调用了方法。让我们来验证这一点:

    @Test
    public void testGetPatternParam() {
        Pattern pattern = Mockito.spy(new Pattern());
        pattern.getPattern(3);

        // 这里会验证方法调用的参数,但并不会验证方法调用的顺序
        Mockito.verify(pattern).getLineContent(3, 2);
        // 等价于Mockito.verify(pattern, Mockito.times(1)).getLineContent(3, 2);
        
        Mockito.verify(pattern).getLineContent(3, 0);
        Mockito.verify(pattern).getLineContent(3, 1);
    }

请注意,我们这里假定行号从0开始,这与之前的约定一致。因此三次调用的参数分别是(3,0),(3,1)和(3,2)。

如果你再用一个(3,3)去验证呢?显然,代码中不会产生这样的调用,因此将报错:

image

&#160;

另外,你可能还注意到了,代码中先验证了(3, 2),Mockito.verify并不关心方法调用的顺序,它只关注方法是否按照给定的参数被调用。但方法调用的顺序自然也是逻辑正确与否的一个重要方面,怎么去确保这一点呢?

因为getPattern方法有返回值,我们正好可利用这一点:

    @Test
    public void testGetPatternOrder() {
        Pattern pattern = Mockito.spy(new Pattern());
        
        // getLineContent尚未实现,我们先模拟它的行为
        Mockito.when(pattern.getLineContent(3, 1)).thenReturn("world");
        Mockito.when(pattern.getLineContent(3, 2)).thenReturn("!");
        Mockito.when(pattern.getLineContent(3, 0)).thenReturn("hello ");
        
        // 因为方法有返回值,且由所调用方法的返回值顺序组装而成,因此可以间接利用来验证调用的顺序
        String content = pattern.getPattern(3);

        assertThat(content).isEqualTo("hello world!");
    }

这里体现了用Mockito.spy的好处,一方面我们保留了getPattern方法的行为,因为这是我们想测试的;另一方面我们又可以去指定其它方法的行为,比如getLineContent的行为。需要注意的是,指定getLineContent的行为必须在调用getPattern方法之前。

在上面的测试中,我们用了一些比较随意的内容,你当然可以模拟得更加正式一些,如下:

    @Test
    public void testGetPattern() {
        Pattern pattern = Mockito.spy(new Pattern());
        
        // 可以模拟得很像,但通常是没必要的。因为在验证时的result也是由你来给出的。
        // 对getPattern方法而言,getLineContent究竟返回什么并不重要
        // 重要的getPattern是否以正确的顺序,正确的参数去调用了getLineContent
        Mockito.when(pattern.getLineContent(3, 0)).thenReturn("  *" + System.lineSeparator());
        Mockito.when(pattern.getLineContent(3, 1)).thenReturn(" ***" + System.lineSeparator());
        Mockito.when(pattern.getLineContent(3, 2)).thenReturn("*****" + System.lineSeparator());
        
        String content = pattern.getPattern(3);
        
        String result = "  *" + System.lineSeparator() 
                      + " ***" + System.lineSeparator() 
                      + "*****" + System.lineSeparator();
        
        assertThat(content).isEqualTo(result);
    }

但正如注释中所说的那样,在这里所进行的测试,关注的其实是getPattern的逻辑。在这一层面上,我们假定getLineContent能正常工作,然后考察依赖于它的getPattern方法的行为是否正确,比方说是否以正确的参数进行了调用,是否正确处理了返回的结果等等,这些显然都是getPattern方法的职责。

如果我们通过Mock方式已经测试到了getPattern的方方面面,理论上而言,只要getLineContent正确了,最终结果也会是正确的。更重要的是,当我们断言getPattern能正常工作时,我们并不依赖于getLineContent的任何具体实现,正如最开始时那样,getLineContent甚至可以是尚未实现的。

关注点的分离(SoC:Separation of Concerts)

我们说前面的测试关注的是getPattern的逻辑,但首先,getPattern必须专注于自己的逻辑。在代码中,我们正是这么做的,我们没有让getPattern方法大包大揽,而是把生成每一行具体内容这一关注点分离到了getLineContent中,从而让getPatternt专注于集成getLineContent返回的内容上。

SoC是一种重要的设计原则,你或许更常在AOP(Aspect-Oriented Programming,面向切面编程)的实践中听到所谓的横切关注点(cross-cutting concerns),也即所谓的切面了。自然,AOP也实践了SoC这一原则,但SoC本身是一个更宽泛的原则,你当然可以怀疑套用在这里是否有点牵强,但我认为不必过于狭隘地去理解它。

单一职责原则(SRP:Single Responsibility Principle)

可以看到,getPattern方法并没有过多的职责,生成每一行具体内容的职责被委托到了getLineContent上。正如我们前面用一个“hello world!”形式去验证那样,具体返回什么那已经是getLineContent的职责了,getPattern做好自己的事情就行了,它不受其它变化的影响。

SRP同样也是一种重要的设计原则,你更常听到的可能是一个类或一个模块应该具有单一的职责。在这里我们说的是方法,你当然可以继续怀疑套用在这里是否有点牵强,但我还是那句话,不必过于狭隘地去理解它。重要的是领会这些思想的精神实质,你或许还能隐约感受它与SoC有点关系。

Robert C. Martin把“职责”定义成“更改的原因”(reason to change), 认为一个类或一个模块应该有且只有一个更改的理由(a class or module should have one, and only one, reason to change.)

实际上,Mockito不赞成使用spy方法,它认为,如果你要用spy,你的设计可能存在一些问题。事实上,如果增加一个叫Line的类,并把getLineContent移到它的里面(或许名字还可改成更短的getContent),让Pattern类依赖于这一Line类,那么就可以用Mockito.mock来构造Line的实例去测试Pattern类,正像前面测试PatternFile与Pattern时那样,Pattern类也能因此变得更加简单。

当然,由于这是一个很小的例子,你可以怀疑是否值得这么去做。但在现实中,如果你发现一个类正在不断膨胀,你或许应该停下来好好想想它是否承担了过多的职责,也许你已经到了一个值得拆分它的时间点。

© 著作权归作者所有

国栋

国栋

粉丝 395
博文 79
码字总数 154046
作品 0
东莞
程序员
私信 提问
加载中

评论(9)

国栋
国栋 博主

引用来自“中山野鬼”的评论

不过测试,万变不离其中的,就是封装测试和关联测试。关联测试,更多是接口对接和系统联调。如果关联测试都要追溯到模块内部进行边界检测,工作量就没个谱了。
这里的测试算是白盒测试了,就是看着代码的逻辑来测。
国栋
国栋 博主

引用来自“中山野鬼”的评论

哈,一直觉得写c好累。包括测试工具的堆建。看看这个测试方法,也算心安了。测试,哪都一样复杂。哈。
对C不太了解,没有发言权。构建测试有时确实不是简单的事,这也许是大家更愿手动测试的一大原因吧。
12叔
12叔
俺 一般不写测试。。。
中山野鬼
中山野鬼
不过测试,万变不离其中的,就是封装测试和关联测试。关联测试,更多是接口对接和系统联调。如果关联测试都要追溯到模块内部进行边界检测,工作量就没个谱了。
中山野鬼
中山野鬼
哈,一直觉得写c好累。包括测试工具的堆建。看看这个测试方法,也算心安了。测试,哪都一样复杂。哈。
国栋
国栋 博主

引用来自“eel”的评论

Mokito是什么?
一个Mock测试的框架
国栋
国栋 博主

引用来自“soxgoon”的评论

HotShots?
ColdShots
修改登录密码
修改登录密码
Mokito是什么?
s
soxgoon
HotShots?
OSChina 技术周刊第十二期 —— 每周技术抢先看

每周技术抢先看,总有你想要的! 移动开发 【博客】Android仿微信录音功能,自定义控件的设计技巧【OSC 新客户端部分功能解说哦】 前端开发 【翻译】AngularJS – 如何处理 XSS 漏洞【我就是...

OSC编辑部
2014/12/07
3.4K
4
OSChina 技术周刊十二期

每周技术抢先看,总有你想要的! 移动开发 【博客】Android仿微信录音功能,自定义控件的设计技巧【OSC 新客户端部分功能解说哦】 前端开发 【翻译】AngularJS – 如何处理 XSS 漏洞【我就是...

OSC编辑部
2014/12/07
315
0
《鸡啄米VS2010/MFC编程入门》系列技术文章整理收藏

《鸡啄米VS2010/MFC编程入门》系列技术文章整理收藏 1VS2010/MFC编程入门之前言 http://www.lai18.com/content/410337.html 2VS2010/MFC编程入门之二(利用MFC向导生成单文档应用程序框架) ...

开元中国2015
2015/06/27
267
0
VS2010/MFC编程入门教程之目录和总结(鸡啄米)

鸡啄米的这套VS2010/MFC编程入门教程到此就全部完成了,虽然有些内容还未涉及到,但帮助大家进行VS2010/MFC的入门学习业已足够。以此教程的知识为基础,学习VS2010/MFC较为深入的内容已非难事...

weixin_40647819
2018/05/23
0
0
Silverlight 解密游戏 之十 自定义粒子特效

在第四篇《Silverlight 解谜游戏 之四 粒子特效》中我们为游戏添加了一个粒子特效,但是当前的ParticleControl 只提供了一种圆形粒子,本篇将为其添加方形、三角形、星形等形状。 以下是五角...

junwong
2012/03/09
134
0

没有更多内容

加载失败,请刷新页面

加载更多

Mybatis Plus删除

/** @author beth @data 2019-10-17 00:30 */ @RunWith(SpringRunner.class) @SpringBootTest public class DeleteTest { @Autowired private UserInfoMapper userInfoMapper; /** 根据id删除......

一个yuanbeth
今天
4
0
总结

一、设计模式 简单工厂:一个简单而且比较杂的工厂,可以创建任何对象给你 复杂工厂:先创建一种基础类型的工厂接口,然后各自集成实现这个接口,但是每个工厂都是这个基础类的扩展分类,spr...

BobwithB
今天
5
0
java内存模型

前言 Java作为一种面向对象的,跨平台语言,其对象、内存等一直是比较难的知识点。而且很多概念的名称看起来又那么相似,很多人会傻傻分不清楚。比如本文我们要讨论的JVM内存结构、Java内存模...

ls_cherish
今天
4
0
友元函数强制转换

友元函数强制转换 p522

天王盖地虎626
昨天
5
0
js中实现页面跳转(返回前一页、后一页)

本文转载于:专业的前端网站➸js中实现页面跳转(返回前一页、后一页) 一:JS 重载页面,本地刷新,返回上一页 复制代码代码如下: <a href="javascript:history.go(-1)">返回上一页</a> <a h...

前端老手
昨天
5
0

没有更多内容

加载失败,请刷新页面

加载更多

返回顶部
顶部