`
taowen
  • 浏览: 191027 次
  • 性别: Icon_minigender_1
  • 来自: 北京
社区版块
存档分类
最新评论

mock框架搞什么搞?

阅读更多
今天早上一时兴起,去网上下载下来JMock,EasyMock最新版本来玩玩。用来测试的行为很简单。就是有一个窗体,上面有一个文本框,一个按钮。如果点击按钮,就会把文本框的内容设置为“Hello”。应用MVP模式,这个行为应该在Presenter中,而View接口应该是这样的:

public interface View {
	public void setText(String text);
	public void addActionListener(ActionListener actionListener);
}


我这里就不TDD了,直接给出Presenter的实现,然后我们来看JMock和EasyMock如何处理这个问题。

public class Presenter {
	public Presenter(final View view) {
		view.addActionListener(new ActionListener() {
			public void actionPerformed() {
				view.setText("Hello");
			}
		});
	}
}


好的,我先来写一段伪测试代码:

引用

创建MockView
创建Presenter,传以MockView做为参数
在MockView上触发事件
验证MockView的SetText方法被调用了


然后就用JMock来实现这段伪代码。

@Test
public void test_click_button_should_set_text_hello() {
		Mockery mockery = new Mockery();
		final View mockView = mockery.mock(View.class);
		final ActionListenerMatcher actionListenerMatcher = new ActionListenerMatcher();
		mockery.checking(new Expectations() {
			{
				one(mockView).addActionListener(
						with(actionListenerMatcher));
				one(mockView).setText("Hello");
			}
		});
		new Presenter(mockView);
		actionListenerMatcher.fireActionPerformed();
		mockery.assertIsSatisfied();
	}


由于JMock没有对触发事件提供直接的支持,所以是自己写了一个ActionListenerMatcher来达到这个目的的。这个Matcher的实现如下:

public class ActionListenerMatcher extends BaseMatcher<ActionListener> {

	private ActionListener actionListener;

	public boolean matches(Object item) {
		actionListener = (ActionListener) item;
		return true;
	}

	public void fireActionPerformed() {
		actionListener.actionPerformed();
	}

	public void describeTo(Description description) {
	}

}


可以看到之所以要这个Matcher是因为,我们需要有一个地方来保存Presenter传进来的listener,并且提供一个用来触发事件的方法。

EasyMock的实现与之非常类似,此处不再赘述。代码可以参见附件。

我们可以看到什么?我看到的是,测试时候的Intention(意图)完全掩盖在冗长复杂的代码之中了。如果不用Mock该怎么做?很简单:

@Test
public void test_click_button_should_set_text_hello() {
		MockView mockView = new MockView();
		new Presenter(mockView);
		mockView.fireActionPerformed();
		Assert.assertEquals("Hello", mockView.getText());
	}


这段代码是不是和上面的伪代码一模一样?MockView是这样的:

private class MockView implements View {
		private ActionListener actionListener;
		private String text;
		public void addActionListener(ActionListener actionListener) {
			this.actionListener = actionListener;
		}
		public void setText(String text) {
			this.text = text;
		}
		public String getText() {
			return text;
		}
		public void fireActionPerformed() {
			actionListener.actionPerformed();
		}
	}


做完这个实验,我不得不说。Mock框架,搞什么搞!简单一点不好么?
分享到:
评论
23 楼 amonlei 2007-05-16  
走路和开车上楼梯,骂车没有人快。。。。
22 楼 firebody 2007-05-12  
taowen 写道
raimundox 写道
taowen 写道
view是如何得出来的,不是本帖的重点。我当然可以把你所说的两个步骤给隐匿掉。


哎...过程是重要的...没有过程就看出不来哪里做了决策,也就没有办法理解你这里为啥要用mock。如果按我上面列出的过程,看不出有用mock的必要,因此也就不用指责什么了。
个人意见觉得这里你硬说是mock的复杂有点论据不足。

复杂的论据有:
Mockery mockery = new Mockery();  

完全是为了mock框架引入的代码,什么是Mockery?
mockery.checking(new Expectations() {  
            {  
...
            }  
        });  

匿名类加静态构造函数?这是表达何意思?
actionListenerMatcher.fireActionPerformed();  

为什么要去一个Matcher上触发一个事件。Matcher,顾名思义,那是用来检查参数是否匹配的啊。

所以说,你是对的。这里没有必要用mock框架。但是这里确实有必要造一个假的,而不是用真的view来测试。所以这里需要用mock框架之外的办法来造假的。


如果造个假的,问题是这个工作量具体如何呢? 如果是一个简单的仿造监听器注册器的stub,那似乎还可以接受,不过TDD开始的,针对这个需求,或许很多人写出的测试代码都可能如下:

view.clickButton();
assertEquals("hello",view.getFieldText());


不知道view的stub需要迎合多少这样的需求?

21 楼 taowen 2007-05-12  
raimundox 写道
taowen 写道
view是如何得出来的,不是本帖的重点。我当然可以把你所说的两个步骤给隐匿掉。


哎...过程是重要的...没有过程就看出不来哪里做了决策,也就没有办法理解你这里为啥要用mock。如果按我上面列出的过程,看不出有用mock的必要,因此也就不用指责什么了。
个人意见觉得这里你硬说是mock的复杂有点论据不足。

复杂的论据有:
Mockery mockery = new Mockery();  

完全是为了mock框架引入的代码,什么是Mockery?
mockery.checking(new Expectations() {  
            {  
...
            }  
        });  

匿名类加静态构造函数?这是表达何意思?
actionListenerMatcher.fireActionPerformed();  

为什么要去一个Matcher上触发一个事件。Matcher,顾名思义,那是用来检查参数是否匹配的啊。

所以说,你是对的。这里没有必要用mock框架。但是这里确实有必要造一个假的,而不是用真的view来测试。所以这里需要用mock框架之外的办法来造假的。
20 楼 taowen 2007-05-12  
raimundox 写道
taowen 写道
用真实的view来测,用mock框架造一个来测,自己手写一个假的来测。三者没有谁推出谁的关系。我只是要说明一,在这里用MVP没有你说的过度设计的问题。二,这里用mock框架很笨拙。

对于你提到的设计步骤的问题。我同意你的看法。下次写代码的时候会注意用小步骤前进。最终的view接口是这样的没有什么问题吧?难道小步骤TDD会出现另外一个版本的view接口?


呵呵,我没说MVP是过渡设计,我说的是View interface...毕竟我受不了上来就一个接口...怎么也得有两个实现类吧...

两个实现类是有的啊。一个是真实的View。一个是测试用的View。
19 楼 raimundox 2007-05-12  
taowen 写道
view是如何得出来的,不是本帖的重点。我当然可以把你所说的两个步骤给隐匿掉。


哎...过程是重要的...没有过程就看出不来哪里做了决策,也就没有办法理解你这里为啥要用mock。如果按我上面列出的过程,看不出有用mock的必要,因此也就不用指责什么了。
个人意见觉得这里你硬说是mock的复杂有点论据不足。
18 楼 raimundox 2007-05-12  
taowen 写道
用真实的view来测,用mock框架造一个来测,自己手写一个假的来测。三者没有谁推出谁的关系。我只是要说明一,在这里用MVP没有你说的过度设计的问题。二,这里用mock框架很笨拙。

对于你提到的设计步骤的问题。我同意你的看法。下次写代码的时候会注意用小步骤前进。最终的view接口是这样的没有什么问题吧?难道小步骤TDD会出现另外一个版本的view接口?


呵呵,我没说MVP是过渡设计,我说的是View interface...毕竟我受不了上来就一个接口...怎么也得有两个实现类吧...
17 楼 taowen 2007-05-12  
用真实的view来测,用mock框架造一个来测,自己手写一个假的来测。三者没有谁推出谁的关系。我只是要说明一,在这里用MVP没有你说的过度设计的问题。二,这里用mock框架很笨拙。

对于你提到的设计步骤的问题。我同意你的看法。下次写代码的时候会注意用小步骤前进。最终的view接口是这样的没有什么问题吧?难道小步骤TDD会出现另外一个版本的view接口?view是如何得出来的,不是本帖的重点。我当然可以把你所说的两个步骤给隐匿掉。
16 楼 raimundox 2007-05-12  
taowen 写道

如果不用反射,很多控件的事件是很难触发的。所以才会有一个view接口出现。之所以有这个view接口出现是因为TDD,而不是因为不TDD。


嘿嘿,同学,但是你也没在View上定义出发的方法啊,如果你说TDD出来,用一个interface来做placeholder,那么也好,至少第一步代码是这样的

public void testShouldXXX() {
   View view = createView(); //空方法,不考虑实现
   new Present(view);
   view.clickButton();
   assertEqauls("Hello", view.getLabelText());
}

好了表达出意图了,那么再看接口,因为View是个接口,不希望因为测试多放几个方法,那么改,第二步:

public void testShouldXXX() {
   View view = createView(); //空方法,不考虑实现
   new Present(view);
   clickButtonOn(view); //空方法,不考虑实现
   assertEqauls("Hello", textOn(view)); //空方法,不考虑实现
}

好开始之前,考虑测试这里怎么实现,基本上两种选择,fake实现createView, clickButtonOn, textOn方法或着mock实现createView, clickButtonOn, textOn。正如你之前说的,如果真是环境下没有办法很简单的去click button的话。那么显然mock走不通,fake也是自然的选择。
总之,你的这个例子,用fake是TDD的自然推论,而用mock也自然是用错了地方。

15 楼 taowen 2007-05-12  
引用

view.buttom.Click(DUMMY_SENDER, DUMMY_EVENT_ARGS)

这个是不行的。event(即delegate)在其所声明的类之外是不能触发的,只能通过+=来添加listener。如果不用反射,很多控件的事件是很难触发的。所以才会有一个view接口出现。之所以有这个view接口出现是因为TDD,而不是因为不TDD。如果不测试,根本不用去考虑如何触发一个界面上的事件这样的问题。因为要测试,所以才会弄出来一个view接口。然后把逻辑分离到presenter里面。通过mockView来驱动presenter的设计。如果东西都写到了view里面,而写界面的框架又很难去模拟鼠标点击之类的事件。这样做的结果就是导致所有的测试都是通过手工来做。
14 楼 raimundox 2007-05-12  
taowen 写道
to raimundox:
但是实际情况中,比如view是Windows Forms,你拿到了button也是无法click的,因为button控件没有提供这个方法。所以很多时候应用MVP就是让界面上的代码可测试。所以才会有MockView的出现。


再ps一下,WinForms里不能掉Click吗?Click事件不是一个delegate吗?对于这个例子难道不可以

view.buttom.Click(DUMMY_SENDER, DUMMY_EVENT_ARGS) ?

当然我对.net没啥了解,这个是瞎猜的...
13 楼 raimundox 2007-05-12  
taowen 写道
to raimundox:
我明白你的意思。就是没有两个相似的具体的view的情况下,不需要一个抽象的view。也就是说不要一上来就套什么MVP。也就是说你认为使用MVP的唯一原因是因为View要可替换。但是实际情况中,比如view是Windows Forms,你拿到了button也是无法click的,因为button控件没有提供这个方法。所以很多时候应用MVP就是让界面上的代码可测试。所以才会有MockView的出现。


嘿嘿,这个我可没说。通篇我都没讨论MVP,我说的是Mock。我想说的是: 如果你按照TDD的方法来作,起码不会这么早就出现一个View interface,也就不会过早地去想要mock了。

p.s. 就算是WinForms吧,我相信其实WinForms可以更简单,且更不用mock,因为有delegate啊,你可以直接测delegate的方法,保证细力度行为正确,然后测试Present做了正确的装配。一样是连mock都不用要的啊
12 楼 taowen 2007-05-12  
to raimundox:
我明白你的意思。就是没有两个相似的具体的view的情况下,不需要一个抽象的view。也就是说不要一上来就套什么MVP。也就是说你认为使用MVP的唯一原因是因为View要可替换。但是实际情况中,比如view是Windows Forms,你拿到了button也是无法click的,因为button控件没有提供这个方法。所以很多时候应用MVP就是让界面上的代码可测试。所以才会有MockView的出现。

to firebody:
这个还没有到分层那个层次吧。我同意你的观点,代码质量更高是最实在的标准。
11 楼 firebody 2007-05-12  
那直接用view地实现来功能测试 也就足够了,没必要再多一个WhateverView来fake了。

这点倒似乎是最合理。

也不会违反taowen说的 “把listener放到view上的原则。”。

分层的好处虽然使得结构清晰,但是要真是TDD起来,mock那么多也是一种噩梦。

分层的好处更实在的作用倒不是“易于测试这点“,感觉还不如说使得 代码质量更高来的实在一些。


10 楼 raimundox 2007-05-12  
taowen 写道
to raimundox:
这样的写法实际上就是把一个序列的两个动作分开做了。我先测试addActionListener被调用了。然后再调用Listener的actionPerformed的逻辑是正确的。我不知道这样做好不好。你也可以说把这两个放在一起测是功能测试。所以我认同你的说法。拿mock框架放到做功能测试的场景下,不适合。这个是显而易见的。因为mock框架造出来的对象无法持有状态。

to firebody:
MVP模式的状态在view上,把listener放到presenter上是不行的。你的观点和raimundox一样,也就是只针对listener的逻辑进行测试好了。

这个例子不是我生造出来的。去年写的一个rich client的媒体上传客户端的测试代码里面就有不少这样的东西。只是那个时候的jMock还是1.x而Matcher还是叫Constrain。意图也就是一个,力图辨析出适合使用mock框架的场合。


不是说我可以叫他功能测试,而是本来你写的测试粒度就是functional级别的,而且没有TDD,直接跳步了,如果TDD的话,用Fake的结论是可以自然推断出来的,而不用一开始就假象要用mock。
TDD第一步,你需要一个真实的View,而不是一开始就有一个interface,因为interface是抽象,没有具体而去抽象就是过渡设计,就是过早优化,因此你第一个测试可能是这样的:

in PresentTest
void testShouldAddTextChangeBehaviourToXXXView() {
   XXXView view = new XXXView();
   new Present(view);
   view.getButtonA().click();
   assertEqauls("Hello", view.getLabelA().getText();
}


很简单,意图也很明显,基本和你写的是一样的。然后当你有了第二个View,你应该有这样的测试

in PresentTest
void testShouldAddTextChangeBehaviourToYYYView() {
   YYYView view = new YYYView();
   new Present(view);
   view.getButtonB().click();
   assertEqauls("Hello", view.getLabelB().getText();
}


这里Present有重复的实现了(因为要针对不同的View类型作出实现),如果这时候如果你希望重构,你不会先动测试,而是改实现
XXXView implements View {

  void setText(String text) {
     getLabelA().setText(text);
  }
  void addActionListener(ActionListerner listerner) {
     ....
  }
}


YYYView同理,然后改Present,使之不依赖于具体View,然后跑测试,发现一切ok,这时候你可以选择重构测试,也可以选择不重构,如果你选择重构的话:
void testShouldAddTextChangeBehaviourToXXXView() {
   View view = new XXXView();
   new Present(view);
   pressButtonOn((XXXView) view);
   assertEqauls("Hello", labelTextOf((XXXView)view).getText();
}

void testShouldAddTextChangeBehaviourToYYYView() {
   View view = new YYYView();
   new Present(view);
   pressButtonOn((YYYView) view);
   assertEqauls("Hello", labelTextOf((YYYView)view).getText();
}

private void pressButtonOn(XXXView view)...
private String labelTextOf(XXXView view)...
private void pressButtonOn(YYYView view)...
private String labelTextOf(YYYView view)...


跑测试通过了,然后很容易发现这两个测试的相似性,然后提取共同的intention,就会发现,实际上我传去那个View是不重要的,因此用不用真正的实现无所谓,所以可以用一个Fake来代替
void testShouldAddTextChangeBehaviourToView() {
   WhateverView view = new WhateverView();
   new Present(view);
   view.pressButton();
   assertEqauls("Hello", view.getLabel().getText();
}


然后干掉其余的。

结论,使用mock的确要主义场合,但是如果严格TDD的话,保证每一步都不提前假设,不提前设计,不提前优化,不提前给出shortcut,大多数不需要用mock的场景都可以避免。
9 楼 taowen 2007-05-12  
to raimundox:
这样的写法实际上就是把一个序列的两个动作分开做了。我先测试addActionListener被调用了。然后再调用Listener的actionPerformed的逻辑是正确的。我不知道这样做好不好。你也可以说把这两个放在一起测是功能测试。所以我认同你的说法。拿mock框架放到做功能测试的场景下,不适合。这个是显而易见的。因为mock框架造出来的对象无法持有状态。

to firebody:
MVP模式的状态在view上,把listener放到presenter上是不行的。你的观点和raimundox一样,也就是只针对listener的逻辑进行测试好了。

这个例子不是我生造出来的。去年写的一个rich client的媒体上传客户端的测试代码里面就有不少这样的东西。只是那个时候的jMock还是1.x而Matcher还是叫Constrain。意图也就是一个,力图辨析出适合使用mock框架的场合。
8 楼 firebody 2007-05-12  
几个不协调的地方:

主要的逻辑是:
   触发按钮事件--〉 调用view.setText()
测试代码如同楼主的代码:
     @Test  
    public void test_click_button_should_set_text_hello() {  
          MockView mockView = new MockView();  
         new Presenter(mockView);  
           mockView.fireActionPerformed();  
           Assert.assertEquals("Hello", mockView.getText());  
      }  


view是一个接口,楼主的测试代码对view进行了mock .

然而,我们看楼主Presenter地实现:

   public class Presenter {  
       public Presenter(final View view) {  
           view.addActionListener(new ActionListener() {  
               public void actionPerformed() {  
                  view.setText("Hello");  
               }  
         });  
      }  
    }  


这里面对于需要测试的逻辑:
   “ 触发按钮事件--〉 调用view.setText() “

的骨干代码 :

view.addActionListener(new ActionListener() {  
               public void actionPerformed() {  
                  view.setText("Hello");  
               }  
         });  

这里的代码意味着逻辑的实现需要依赖界面view对注册的actionListener的调用。

但是测试依赖的功能类只有 Presenter,View  .其中 view是接口,而Presenter仅仅调用了view的接口来注册listener ,这些代码逻辑仅仅完成了 需要测试的逻辑的功能代码的一部分。

而现在 为了让测试通过,却需要很多mock代码或者stub代码来完成这些逻辑。 本是功能代码需要实现的逻辑却让mock/stub代码来实现。 有点本末倒置了的感觉。

对于这样的测试,感觉还不如把Actionlistener注册的实现放到presenter来的好一些,也不用依赖于view了,当然这样可能带来一些问题,这个建议仅仅是针对这个例子而已。

如果不能把Listener脱离view,还不如直接针对Listener坐针对性的测试好了,至于集成的测试选择一个好一点界面验收框架,让这些验收测试对 Presenter ,View这里面“少而且简单“的代码做一个集成测试好了。





7 楼 yananay 2007-05-11  
我更愿意使用 httpunit
6 楼 raimundox 2007-05-11  
就拿你这个例子说吧,你这个例子的一些特点都指向Fake而不是Mock:
首先,View是一个interface,且上面没有触发事件的方法,也就是说,仅仅mock interface根本没法触发事件,所以才有了你的matcher.
其次,ActionListener是inner class,action的实例都拿不到。
第三,显然你这个不是TDD出来的,测试的意图有很明显的功能测试的意思,而mock恰恰多数是用于unit test的。
因此用Fake是最直接的做法。

而利于mock的写法,是把测试分开,ActionListener和Present来测, 且不用inner class

in PresentTest
public void testShouldAddTextChangeBehaviourToView {
  View mockView = controlMock(View.class);
  ActionListener expectedListener = new TextChangeActionListener(DUMMY_VIEW);
  mockView.addActionListener(expectedListener);
  replay(mockView);
  new Present(mockView, expectedListener); //一般来说如果Action出去了,就得DI了,这里假设constructor DI
  verify(mockView);
}

in TextChangeActionListenerTest

public void testShouldChangeTextOfView() {
   View mockView = controlMock(View.class);
   mockView.setText("Hello");
   replay(mockView);
   new TextChangeActionListener(mockView).actionPerformed();
   verify(mockView);
}

当然这个实现是否是好的,很难讲。不过个人不是很喜欢inner class的做法,不好TDD啊...
5 楼 firebody 2007-05-11  
raimundox 写道
你给的例子不是Mock,至多是Fake。各种区别见马大叔的Mocks Aren't Stubs:

* Dummy objects are passed around but never actually used. Usually they are just used to fill parameter lists.
* Fake objects actually have working implementations, but usually take some shortcut which makes them not suitable for production (an in memory database is a good example).
* Stubs provide canned answers to calls made during the test, usually not responding at all to anything outside what's programmed in for the test. Stubs may also record information about calls, such as an email gateway stub that remembers the messages it 'sent', or maybe only how many messages it 'sent'.
* Mocks are what we are talking about here: objects pre-programmed with expectations which form a specification of the calls they are expected to receive.

因此实际上,不是JMock或EasyMock框架复杂,而是你给的例子中,Mock不是最佳选择,如果View里东西再多一点,Fake可能就不太合适了。


也没必要用到stub .写起来也很是麻烦。

直接针对分离的业务逻辑作测试。当然,前提是代码里面已经把界面和业务逻辑分离好了。

如果按照楼主这样写出来的测试代码,我感觉没法看了,即使看得也很费力。



4 楼 taowen 2007-05-11  
像我上面那样用jMock的就很多的。用mock框架写测试的时候,不知不觉地就写出了很多本来不适合用mock框架解决的问题。所以fake也好,stub也好。都是要告诉大家,用之前先考虑考虑什么是最简单的解决办法。这dummy,fake,stub与mock的分别太过于微妙。只要能够达到目的就好了,管它叫什么名字呢?我觉得可以拿各种测试中需要用到的“mock”的场景拿出来,用各种实现方式比较一下,看看jMock这些框架到底在什么情况下写出来的代码行数更少,而且更好理解。我觉得不会太多。

相关推荐

Global site tag (gtag.js) - Google Analytics