1.介绍

1.1.概述

在这篇文章中,我们将谈谈金宝搏官网188be嘲笑:它是什么,为什么要使用它,以及如何使用一些最常用的Java模拟库来模拟相同的测试用例。

我们将从模仿概念的正式/半正式定义开始;然后,我们将展示测试中的案例,然后为每个库提供示例,最后给出一些结论。所选的库是5EasyMock.,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.4javadoc.).注意,使用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.24javadoc.),版本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项目所以请随意下载并使用它。

通用底部

从Spring 5和Spring Boot 2开始学习春天课程:

>>看看课程
10评论
最古老的
最新的
内联反馈
查看所有评论
评论在本文上关闭!