1.介绍
1.1.概述
在这篇文章中,我们将谈谈金宝搏官网188be嘲笑:它是什么,为什么要使用它,以及如何使用一些最常用的Java模拟库来模拟相同的测试用例。
我们将从模仿概念的正式/半正式定义开始;然后,我们将展示测试中的案例,然后为每个库提供示例,最后给出一些结论。所选的库是5,EasyMock.,JMockit.
如果你觉得你已经知道了嘲笑的基本原理,也许你可以跳过下面的三点跳到第二点。
1.2.使用mock的理由
我们将开始假设您已经按照一些以测试(TDD、ATDD或BDD)为中心的驱动开发方法编写代码。或者您只是想为依赖于依赖项来实现其功能的现有类创建一个测试。
在任何情况下,当对一个类进行单元测试时,我们都希望只测试它的功能,而不是它的依赖项(要么是因为我们相信他们的实现,要么是因为我们将自己测试它)。
为了实现这一点,我们需要为被测对象提供一个可以控制该依赖的替换。通过这种方式,我们可以强制执行极端的返回值、异常抛出或简单地将耗时的方法减少到固定的返回值。
这种受控的替代是模拟,它将帮助您简化测试编码并减少测试执行时间。
1.3.模拟概念和定义
让我们来看看a的四个定义文章由Martin Fowler撰写的,总结了每个人都应该了解Mocks的基础知识:金宝搏官网188be
- 假的对象被传递,但从未实际使用。通常,它们只是用来填充参数列表。
- 伪造的对象有工作的实现,但通常会采取一些捷径,使它们不适合生产(内存数据库就是一个很好的例子)。
- 存根为测试期间的呼叫提供固定的回答,通常对测试程序之外的任何东西都没有反应。存根还可能记录有关呼叫的信息,比如一个电子邮件网关存根,它会记住“发金宝搏官网188be送”的信息,或者可能只记住“发送”的信息数量。
- 模拟是我们在这里讨论的:预期预期预期的对象,这些预期金宝搏官网188be形成了他们预期收到的呼叫的规范。
1.4嘲笑或不嘲笑:这是问题
不是所有的东西都必须被嘲笑.有时候,最好进行集成测试,因为模拟那个方法/特性不会带来什么实际好处。在我们的测试用例中(这将在下一点中显示),它将测试LoginDao.
的LoginDao将使用一些第三方库进行DB访问,并且嘲笑它只会在确保为呼叫准备参数时,但我们仍然需要测试呼叫返回我们想要的数据。
因此,它不会包含在此示例中(虽然我们可以用模拟呼叫编写单元测试,用于第三方库调用和与DBUNIT进行集成测试以测试第三方库的实际性能)。
2.测试用例
考虑到前一节中的所有内容,让我们提出一个非常典型的测试用例,以及我们将如何使用模型测试它(当使用模拟有意义时)。这将有助于我们在以后能够比较不同的嘲弄库。
2.1提出案例
所提出的测试用例将是具有分层体系结构的应用程序中的登录过程。
登录请求将由一个使用服务的控制器处理,该服务使用DAO(在DB上查找用户凭证)。我们不会深入到每一层的实现,而是将更多的关注于组件之间的相互作用每层。
这边,我们会有LoginController, 一种LoginService和一个LoginDAO.让我们看一下澄清图:

2.2实现
我们现在随着用于测试案例的实现,所以我们可以了解测试发生的情况(或应该发生的事情)。
我们将从用于所有操作的模型开始,UserForm,它将只保存用户名和密码(我们使用公共访问修饰符来简化)和getter方法用户名字段允许对该属性进行模拟:
公共类UserForm {公共字符串密码;公共字符串用户名;public string getusername(){return username;}}
让我们跟随LoginDAO,这将是无效的,因为我们只想要其方法,所以我们可以在需要时嘲笑它们:
公共类Logindao {public int登录(Userform UserForm){返回0;}}
LoginDao将被LoginService在其登录方法。LoginService也会有一个setCurrentuser.返回的方法无效来测试你的嘲笑。
公共类loginservice {private logindao logindao;私有字符串函数器;公共布尔登录(Userform UserForm){Assert Null!= UserForm;int loginResults = logindao.login(UserForm);切换(LoginResults){案例1:返回true;默认值:返回false;public void setCurrentUser(字符串用户名){if(null!= username){this.currentuser =用户名;}}}
最后,LoginController将使用LoginService为其登录方法。这将包括:
- 没有调用mock服务的情况。
- 只调用一个方法的情况。
- 所有方法都将被调用的情况。
- 将测试异常抛出的情况。
public class LoginController {public LoginService LoginService;public String login(UserForm UserForm){if(null == UserForm){return "ERROR";其他}{布尔记录;try {log = loginService.login(userForm);} catch(异常e){返回"ERROR";}如果(记录){loginService.setCurrentUser (userForm.getUsername ());返回“OK”;其他}{返回“KO”;} } } }
现在我们已经看到了要测试的是什么,让我们看看如何用每个库来模拟它。
3.测试设置
3.1 5
对于Mockito,我们将使用版本2.8.9.
创建和使用模拟的最简单方法是通过@Mock和@InjectMocks注释。第一个将为用于定义字段的类创建一个模拟,第二个将尝试将所创建的模拟注入到带注释的模拟中。
还有更多的注释,比如@间谍这允许您创建部分模拟(在非模拟方法中使用正常实现的模拟)。
话虽如此,你得打个电话MockitoAnnotations.initMocks(这)在执行任何测试的任何测试之前,为所有这些“魔术”为工作。这通常是在a中完成的@Before带注释的方法。你也可以用theMockitoJUnitRunner.
public class LoginControllerTest {@Mock private LoginDao;@Spy @ injectmock私有LoginService spiedLoginService;@Mock private LoginService;@ injectmock私有LoginController;@Before public void setUp() {loginController = new loginController ();MockitoAnnotations.initMocks(这个);}}
3.2 EasyMock
对于EasyMock,我们将使用版本3.4(javadoc.).注意,使用EasyMock,要使mock开始“工作”,必须调用EasyMock.replay(模拟)在每个测试方法上,否则您将收到一个异常。
mock和测试类也可以通过注释定义,但在本例中,我们将使用EasyMockRunner.用于测试类。
控件创建mock@Mock的注释和测试对象@TestSubject一个(它将从创建的模拟中注入它的依赖项)。测试对象必须内联创建。
@RunWith(EasyMockRunner.class) public class LoginControllerTest {@Mock private LoginDao;@Mock private LoginService;@TestSubject private LoginController LoginController = new LoginController();}
3.3.JMockit
对于JMockit,我们将使用version1.24(javadoc.),版本1.25还没有发布(至少在编写本文的时候)。
对于JMockit的设置与Mockito一样简单,但除了部分模型(并且真的无需)没有具体的注释,您必须使用JMockit作为测试跑步者。
模拟使用@Injectable注释(只创建一个模拟实例)或@mocked.注释(将为注释字段的类的每个实例创建模拟)。
使用的测试实例创建(并注入其映射的依赖项)@Tested注释。
@runwith(jmockit.class)public class logincontrollertest {@injectable private logindao logindao;@Injectable私有LoginService Loginservice;@Tested私人Logincontroller Logincontroller;}
4.验证没有呼叫Mock
4.1.5
为了验证mock在Mockito中没有接收到调用,可以使用这个方法验证程序()接受嘲笑。
@Test public void assertthatnomethodhasbecalled () {loginController.login(null);Mockito.verifyZeroInteractions (loginService);}
4.2.EasyMock.
为了验证Mock收到没有调用您根本没有指定行为,您将重播模拟,最后,您验证。
@test public void aserthatnomethodhasbeencalled(){easymock.replay(loginservice);logincontroller.login(null);EasyMock.verify (loginService);}
4.3.JMockit
为了验证一个mock没有收到任何调用,您只需为该mock指定期望,并执行FullVerifications(模拟)对于所述模拟。
@Test public void assertthatnomethodhasbecalled () {loginController.login(null);新FullVerifications (loginService) {};}
5.定义模拟方法调用和验证对模拟的调用
5.1。5
为模拟方法调用,你可以使用Mockito.When(Mock.method(args))。然后return(价值).在这里,您可以返回不同的值,只需一个呼叫即可将它们添加到更多参数:return (value1, value2, value-n,…).
注意,您不能使用此语法模拟void返回方法。在上述情况下,您将使用所述方法的验证(如第11行所示)。
为验证呼叫到你可以使用的模仿Mockito.verify(模拟).method (args)并且您也可以验证使用更多的电话对模拟使用verifyNoMoreInteractions(模拟).
为验证参数,您可以通过特定值或使用预定义的匹配器任何(),anyString (),anyInt()。还有很多这样的匹配器,甚至可以定义你的匹配器,我们将在下面的例子中看到。
@Test public void aserttwomethodshavebeencalled(){Userform UserForm = New UserForm();userform.username =“foo”;Mockito.when (loginService.login (userForm) .thenReturn(真正的);String login = loginController.login(userForm);assert.asstequals(“确定”,登录);Mockito.verify (loginService) .login (userForm);Mockito.verify (loginService) .setCurrentUser (" foo ");@test public void asertonlyonemethodhasbeencalled(){Userform UserForm = New UserForm();userform.username =“foo”;Mockito.When(loginservice.login(Userform))。然后return(false); String login = loginController.login(userForm); Assert.assertEquals("KO", login); Mockito.verify(loginService).login(userForm); Mockito.verifyNoMoreInteractions(loginService); }
5.2。EasyMock.
为模拟方法调用,可以使用EasyMock.expect (mock.method (args) .andReturn(值).
为验证呼叫要嘲弄,你可以用EasyMock..verify(模拟),但你必须叫它总是在之后调用EasyMock.replay(模拟).
为验证参数,您可以传递特定值,或者您已像ISA这样的预定义匹配器(Class.class),anyString (),anyint()和一个更多的那种匹配者再次定义匹配者的可能性。
@Test public void aserttwomethodshavebeencalled(){Userform UserForm = New UserForm();userform.username =“foo”;EasyMock.expect (loginService.login (userForm) .andReturn(真正的);loginService.setCurrentUser (" foo ");EasyMock.replay (loginService);String login = loginController.login(userForm);assert.asstequals(“确定”,登录);EasyMock.verify (loginService);@test public void asertonlyonemethodhasbeencalled(){Userform UserForm = New UserForm();userform.username =“foo”; EasyMock.expect(loginService.login(userForm)).andReturn(false); EasyMock.replay(loginService); String login = loginController.login(userForm); Assert.assertEquals("KO", login); EasyMock.verify(loginService); }
5.3。JMockit
使用JMockit,您已经定义了步骤用于测试:记录,重放和验证。
记录是在新做的吗期望(){{}}块(您可以为多个模型定义操作),重播只需通过调用测试类的方法(应该调用一些嘲笑对象)来完成。验证是做在一个新的里面吗验证(){{}}块(可以在其中定义几个模拟的验证)。
为模拟方法调用,你可以使用mock.method (args);结果=值;在任何预期堵塞。在这里,您可以刚刚使用多个呼叫返回不同的值返回(value1, value2,…,valuen);而不是结果=值;.
为验证呼叫对于mock,您可以使用新的验证() {{mock.call(值)}}或新验证(模拟){{}}来验证前面定义的每个预期调用。
为验证参数,你可以传递特定的值,或者你有预定义的值就像任何,anyString,任何隆起,以及许多这类特殊值,以及定义匹配器(必须是Hamcrest匹配器)的可能性。
@Test public void aserttwomethodshavebeencalled(){Userform UserForm = New UserForm();userform.username =“foo”;新期望(){{loginservice.login(Userform);结果= true;loginService.setCurrentUser (" foo ");}};String login = loginController.login(userForm);assert.asstequals(“确定”,登录);新FullVerifications (loginService) {};@test public void asertonlyonemethodhasbeencalled(){Userform UserForm = New UserForm(); userForm.username = "foo"; new Expectations() {{ loginService.login(userForm); result = false; // no expectation for setCurrentUser }}; String login = loginController.login(userForm); Assert.assertEquals("KO", login); new FullVerifications(loginService) {}; }
6.嘲笑异常投掷
6.1。5
可以使用异常抛出.thenThrow (ExceptionClass.class)后Mockito.When(模仿)(args)).
@Test public void mockexceptionthrow () {UserForm UserForm = new UserForm();Mockito.when (loginService.login (userForm) .thenThrow (IllegalArgumentException.class);String login = loginController.login(userForm);assert.assertequals(“错误”,登录);Mockito.verify (loginService) .login (userForm);Mockito.verifyZeroInteractions (loginService);}
6.2。EasyMock.
可以使用异常抛出.andThrow(新ExceptionClass ())答:EasyMock.expect(…)调用。
@test public void mockexceptionionthroning(){UserForm UserForm = New UserForm();EasyMock.expect(loginservice.login(Userform))。讨论(newrestalargumentexception());EasyMock.replay (loginService);String login = loginController.login(userForm);assert.assertequals(“错误”,登录);EasyMock.verify (loginService);}
6.3。JMockit
用JMockito呕吐异常特别容易。仅返回一个例外,因为模拟方法调用而不是“正常”返回。
@test public void mockexceptionionthroning(){UserForm UserForm = New UserForm();新期望(){{loginservice.login(Userform);结果= New IllegalArgumentException();//为SetCurrentUser}没有期望};String login = loginController.login(userForm);assert.assertequals(“错误”,登录);新FullVerifications (loginService) {};}
7.模仿要传递的对象
7.1。5
您可以创建模拟也将作为方法调用的参数传递。使用Mockito,你可以用一衬里这样做。
@Test public void mockAnObjectToPassAround() {UserForm UserForm = Mockito.when(Mockito.mock(UserForm.class).getUsername()) .thenReturn("foo").getMock();Mockito.when (loginService.login (userForm) .thenReturn(真正的);String login = loginController.login(userForm);assert.asstequals(“确定”,登录);Mockito.verify (loginService) .login (userForm);Mockito.verify (loginService) .setCurrentUser (" foo ");}
7.2。EasyMock.
可以与EasyMock.mock(class.class).之后,你可以使用EasyMock.expect (mock.method ())为执行做好准备,时刻记得调用EasyMock.replay(模拟)在使用之前。
@Test public void mockAnObjectToPassAround() {UserForm UserForm = EasyMock.mock(UserForm.class);EasyMock.expect (userForm.getUsername ()) .andReturn (" foo ");EasyMock.expect (loginService.login (userForm) .andReturn(真正的);loginService.setCurrentUser (" foo ");EasyMock.replay (userForm);EasyMock.replay (loginService);String login = loginController.login(userForm);assert.asstequals(“确定”,登录);EasyMock.verify (userForm);EasyMock.verify (loginService); }
7.3。JMockit
为了仅为一个方法模拟一个对象,您可以简单地将mock对象作为参数传递给测试方法。然后您可以像使用其他mock一样创建期望。
@Test public void mockAnObjectToPassAround(@ mock UserForm UserForm) {new Expectations() {{UserForm . getusername ();结果=“foo”;loginService.login (userForm);结果= true;loginService.setCurrentUser (" foo ");}};String login = loginController.login(userForm);assert.asstequals(“确定”,登录);新FullVerifications (loginService) {};新FullVerifications (userForm) {}; }
8.自定义参数匹配
8.1。5
有时模拟调用的参数匹配需要比固定值或复杂一点anyString ().对于这种情况,Mockito有它的matcher类argThat (ArgumentMatcher < >).
@Test public void argumentMatching() {UserForm UserForm = new UserForm();userform.username =“foo”;//默认匹配器Mockito.when(loginService.login(Mockito.any(UserForm.class))).thenReturn(true);String login = loginController.login(userForm);assert.asstequals(“确定”,登录);Mockito.verify (loginService) .login (userForm);//复杂的匹配器Mockito.verify(loginService). setcurrentuser (ArgumentMatchers. setcurrentuser)argThat(new ArgumentMatcher() {@Override public boolean matches(String argument) {return argument. startswith ("foo");}}));}
8.2。EasyMock.
自定义参数匹配与EasyMock有点复杂,因为您需要创建一种创建实际匹配器的静态方法,然后向其报告EasyMock.reportMatcher (IArgumentMatcher).
一旦创建了这个方法,就可以通过对该方法的调用在mock期望上使用它(如行中示例所示)。
@Test public void argumentMatching() {UserForm UserForm = new UserForm();userform.username =“foo”;//默认matcher EasyMock.expect(loginService.login(EasyMock.isA(UserForm.class))).andReturn(true);// complex matcher loginService.setCurrentUser(specificArgumentMatching("foo"));EasyMock.replay (loginService);String login = loginController.login(userForm);assert.asstequals(“确定”,登录);EasyMock.verify (loginService);} private static String specificArgumentMatching(String expected) {EasyMock。reportMatcher(new IArgumentMatcher() {@Override public boolean matches(Object argument) {return argument instanceof String && ((String) argument).startsWith(expected); } @Override public void appendTo(StringBuffer buffer) { //NOOP } }); return null; }
8.3。JMockit
与JMockit匹配的自定义参数是通过特殊的withArgThat(匹配器)方法(接收Hamcrests匹配器对象)。
@Test public void argumentMatching() {UserForm UserForm = new UserForm();userform.username =“foo”;//默认匹配器新期望(){{loginservice.login((Userform)任何);结果= true;//复杂匹配器loginservice.setCurrentUser(withargthat(new basematcher (over opput boolean匹配(对象项){return项目instanceof string &&(string)项).startswith(“foo”); @override公共空白描述(描述说明){// noop}}));}};String login = loginController.login(userForm);assert.asstequals(“确定”,登录);新FullVerifications (loginService) {}; }
9.部分嘲笑
9.1。5
Mockito允许以两种方式进行部分模拟(在某些方法中使用实际实现而不是模拟方法调用的模拟)。
你可以使用.thenCallRealMethod ()在一个普通的模拟方法调用定义中,或者您可以创建间谍而不是mock,在这种情况下,它的默认行为将是在所有非mock方法中调用实际实现。
@Test public void partialMocking(){//使用部分模拟loginController。loginService = spiedLoginService;UserForm = new UserForm();userform.username =“foo”;//让服务的登录使用实现,让我们模拟DAO调用Mockito.when(loginDao.login(userForm)).thenReturn(1);String login = loginController.login(userForm);assert.asstequals(“确定”,登录);//验证模拟调用Mockito.verify(spiedLoginService).setCurrentUser("foo");}
9.2。EasyMock.
在EasyMock中,部分模拟也变得更加复杂,因为您需要在创建模拟时定义将模拟哪些方法。
这是用EasyMock.PartialMockBuilder(class.class).addmockedmethod(“methodname”)。createmock().完成此操作后,就可以将模拟作为任何其他非部分模拟使用了。
@Test public void partialMocking() {UserForm UserForm = new UserForm();userform.username =“foo”;//使用部分模拟LoginService loginServicePartial = EasyMock.partialMockBuilder(LoginService.class) .addMockedMethod("setCurrentUser").createMock();loginServicePartial.setCurrentUser (" foo ");//让服务的登录使用实现,让我们模拟DAO调用EasyMock.expect(loginDao.login(userForm)).andReturn(1);loginServicePartial.setLoginDao (loginDao);loginController。登录Service = loginServicePartial; EasyMock.replay(loginDao); EasyMock.replay(loginServicePartial); String login = loginController.login(userForm); Assert.assertEquals("OK", login); // verify mocked call EasyMock.verify(loginServicePartial); EasyMock.verify(loginDao); }
9.3。JMockit
使用JMockit进行部分mocking特别容易。类中定义的没有模拟行为的每个方法调用期望(){{}}使用“真实”实施.
现在让我们想象我们想要部分地嘲笑LoginService类来嘲弄setCurrentUser ()使用实际实现的方法登录()方法。
为此,我们首先创建并传递一个实例LoginService到期望阻止。然后,我们只记录期望setCurrentUser ()方法:
@Test public void partialMocking() {LoginService partialLoginService = new LoginService();partialLoginService.setLoginDao (loginDao);loginController。登录Service = partialLoginService; UserForm userForm = new UserForm(); userForm.username = "foo"; new Expectations(partialLoginService) {{ // let's mock DAO call loginDao.login(userForm); result = 1; // no expectation for login method so that real implementation is used // mock setCurrentUser call partialLoginService.setCurrentUser("foo"); }}; String login = loginController.login(userForm); Assert.assertEquals("OK", login); // verify mocked call new Verifications() {{ partialLoginService.setCurrentUser("foo"); }}; }
10.结论
在本文中,我们比较了三个Java模拟库,每个库都有其优点和缺点。
- 他们三个都是很容易配置使用注释来帮助您定义模拟和被测试对象,使用运行器来尽可能轻松地进行模拟注入。
- 我们可以说Mockito在这里会胜出,因为它有一个用于部分模拟的特殊注释,但JMockit甚至不需要它,所以我们假设它是这两者之间的一个平局。
- 其中三个都追随或多或少record-replay-verify模式但是,在我们看来,最好的是这样做的是JMockit,因为它迫使你在块中使用这些,因此测试得到更多结构化。
- 从容的使用是很重要的,因此您可以尽可能少地工作来定义您的测试。JMockit将是其固定的始终相同的结构的选择。
- Mockito或多或少是最着名的社区将会更大。
- 有打电话给重播每次想用mock都是清晰的闲人免进的,所以我们在EasyMock上加一个- 1。
- 一致性/简单对我来说也很重要。我们喜欢JMockit返回结果的方式,它对“正常”结果和异常结果都是一样的。
说了这么多,我们会做出选择吗JMockit即使是一件胜利者,即使直到现在我们一直在使用5因为我们已经被它的简单和固定的结构所吸引,并将从现在开始尝试使用它。
的全面实施可在GitHub项目所以请随意下载并使用它。