在代码中,我最近遇到了一段非常糟糕的代码,这些代码基于对对象执行某些操作的类转换。 当然,代码需要重构,但是如果您首先没有对该功能进行单元测试,则有时您可能无法做到/或者不想这样做(这应该是可以理解的)。 在下面的文章中,我将展示如何测试这种代码,如何重构它以及实际上我对这种代码的看法。
让我们看一下项目结构:
如关于Mocktio的JAXB答复REPOSTS_DEEP_STUBS的帖子所述,在com.blogspot.toomuchcoding.model包中,我们再次使用JAXB编译器生成了JAXB生成的类。 让我们省略对pom.xml文件的讨论,因为它与上一篇文章完全相同。
在com.blogspot.toomuchcoding.adapter包中,我们在JAXB PlayerDetails类上具有适配器,该类提供对Player接口的访问。 有:
CommonPlayerAdapter.java
package com.blogspot.toomuchcoding.adapter;import com.blogspot.toomuchcoding.model.Player;
import com.blogspot.toomuchcoding.model.PlayerDetails;/*** User: mgrzejszczak* Date: 09.06.13* Time: 15:42*/
public class CommonPlayerAdapter implements Player {private final PlayerDetails playerDetails;public CommonPlayerAdapter(PlayerDetails playerDetails){this.playerDetails = playerDetails;}@Overridepublic void run() {System.out.printf("Run %s. Run!%n", playerDetails.getName());}public PlayerDetails getPlayerDetails() {return playerDetails;}
}
DefencePlayerAdapter.java
package com.blogspot.toomuchcoding.adapter;import com.blogspot.toomuchcoding.model.DJ;
import com.blogspot.toomuchcoding.model.DefensivePlayer;
import com.blogspot.toomuchcoding.model.JavaDeveloper;
import com.blogspot.toomuchcoding.model.PlayerDetails;/*** User: mgrzejszczak* Date: 09.06.13* Time: 15:42*/
public class DefencePlayerAdapter extends CommonPlayerAdapter implements DefensivePlayer, DJ, JavaDeveloper {public DefencePlayerAdapter(PlayerDetails playerDetails){super(playerDetails);}@Overridepublic void defend(){System.out.printf("Defence! %s. Defence!%n", getPlayerDetails().getName());}@Overridepublic void playSomeMusic() {System.out.println("Oops I did it again...!");}@Overridepublic void doSomeSeriousCoding() {System.out.println("System.out.println(\"Hello world\");");}
}
OffensivePlayerAdapter.java
package com.blogspot.toomuchcoding.adapter;import com.blogspot.toomuchcoding.model.OffensivePlayer;
import com.blogspot.toomuchcoding.model.PlayerDetails;/*** User: mgrzejszczak* Date: 09.06.13* Time: 15:42*/
public class OffensivePlayerAdapter extends CommonPlayerAdapter implements OffensivePlayer {public OffensivePlayerAdapter(PlayerDetails playerDetails){super(playerDetails);}@Overridepublic void shoot(){System.out.printf("%s Shooooot!.%n", getPlayerDetails().getName());}
}
好的,现在让我们转到更有趣的部分。 让我们假设我们有一个非常简单的玩家工厂:
PlayerFactoryImpl.java
package com.blogspot.toomuchcoding.factory;import com.blogspot.toomuchcoding.adapter.CommonPlayerAdapter;
import com.blogspot.toomuchcoding.adapter.DefencePlayerAdapter;
import com.blogspot.toomuchcoding.adapter.OffensivePlayerAdapter;
import com.blogspot.toomuchcoding.model.Player;
import com.blogspot.toomuchcoding.model.PlayerDetails;
import com.blogspot.toomuchcoding.model.PositionType;/*** User: mgrzejszczak* Date: 09.06.13* Time: 15:53*/public class PlayerFactoryImpl implements PlayerFactory {@Overridepublic Player createPlayer(PositionType positionType) {PlayerDetails player = createCommonPlayer(positionType);switch (positionType){case ATT:return new OffensivePlayerAdapter(player);case MID:return new OffensivePlayerAdapter(player);case DEF:return new DefencePlayerAdapter(player);case GK:return new DefencePlayerAdapter(player);default:return new CommonPlayerAdapter(player);}}private PlayerDetails createCommonPlayer(PositionType positionType){PlayerDetails playerDetails = new PlayerDetails();playerDetails.setPosition(positionType);return playerDetails;}
}
好的,我们有一家制造Player的工厂。 让我们看一下使用工厂的服务:
PlayerServiceImpl.java
package com.blogspot.toomuchcoding.service;import com.blogspot.toomuchcoding.factory.PlayerFactory;
import com.blogspot.toomuchcoding.model.*;/*** User: mgrzejszczak* Date: 08.06.13* Time: 19:02*/
public class PlayerServiceImpl implements PlayerService {private PlayerFactory playerFactory;@Overridepublic Player playAGameWithAPlayerOfPosition(PositionType positionType) {Player player = playerFactory.createPlayer(positionType);player.run();performAdditionalActions(player);return player;}private void performAdditionalActions(Player player) {if(player instanceof OffensivePlayer){OffensivePlayer offensivePlayer = (OffensivePlayer) player;performAdditionalActionsForTheOffensivePlayer(offensivePlayer);}else if(player instanceof DefensivePlayer){DefensivePlayer defensivePlayer = (DefensivePlayer) player;performAdditionalActionsForTheDefensivePlayer(defensivePlayer);}}private void performAdditionalActionsForTheOffensivePlayer(OffensivePlayer offensivePlayer){offensivePlayer.shoot();}private void performAdditionalActionsForTheDefensivePlayer(DefensivePlayer defensivePlayer){defensivePlayer.defend();try{DJ dj = (DJ)defensivePlayer;dj.playSomeMusic();JavaDeveloper javaDeveloper = (JavaDeveloper)defensivePlayer;javaDeveloper.doSomeSeriousCoding();}catch(ClassCastException exception){System.err.println("Sorry, I can't do more than just play football...");}}public PlayerFactory getPlayerFactory() {return playerFactory;}public void setPlayerFactory(PlayerFactory playerFactory) {this.playerFactory = playerFactory;}
}
让我们承认吧……这段代码很糟糕。 在内部,当您查看它时(无论它是否使用了 operator 实例 ),您都认为它是邪恶的。 正如您在代码中看到的那样,我们正在进行一些类强制转换……我们到底如何进行测试? 在大多数测试框架中,您无法对模拟进行此类类转换,因为它们是使用CGLIB库构建的,并且可能会抛出一些ClassCastExceptions。 您仍然无法返回模拟和真实的实现(假设这些模拟和构建不会在构建过程中执行任何丑陋的工作),并且它实际上可以工作,但仍然如此–这是错误的代码
自带的Mockito给救援(虽然你不应该过度使用此功能-事实上,如果你需要使用它,请考虑重构它)与其extraInterfaces特点:
extraInterfaces
MockSettings extraInterfaces (java.lang.Class <?>…接口)
指定模拟应实现的额外接口。 对于遗留代码或某些极端情况可能很有用。 有关背景信息,请参见此处的问题51。此神秘功能应经常使用。 被测对象应该确切知道其协作者和依赖性。 如果您碰巧经常使用它,请确保您确实在生成简单,干净且可读的代码。
例子:
Foo foo = mock(Foo.class, withSettings().extraInterfaces(Bar.class, Baz.class));//now, the mock implements extra interfaces, so following casting is possible:Bar bar = (Bar) foo;Baz baz = (Baz) foo;
参数:
interfaces
–应该实现的额外接口。
返回值:设置实例,以便您可以流畅地指定其他设置
现在让我们看一下测试:
PlayerServiceImplTest.java
package com.blogspot.toomuchcoding.service;import com.blogspot.toomuchcoding.factory.PlayerFactory;
import com.blogspot.toomuchcoding.model.*;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.invocation.InvocationOnMock;
import org.mockito.runners.MockitoJUnitRunner;
import org.mockito.stubbing.Answer;import static org.hamcrest.CoreMatchers.is;
import static org.junit.Assert.assertThat;
import static org.mockito.BDDMockito.*;/*** User: mgrzejszczak* Date: 08.06.13* Time: 19:26*/
@RunWith(MockitoJUnitRunner.class)
public class PlayerServiceImplTest {@MockPlayerFactory playerFactory;@InjectMocksPlayerServiceImpl objectUnderTest;@Mock(extraInterfaces = {DJ.class, JavaDeveloper.class})DefensivePlayer defensivePlayerWithDjAndJavaDevSkills;@MockDefensivePlayer defensivePlayer;@MockOffensivePlayer offensivePlayer;@MockPlayer commonPlayer;@Testpublic void shouldReturnOffensivePlayerThatRan() throws Exception {//givengiven(playerFactory.createPlayer(PositionType.ATT)).willReturn(offensivePlayer);//whenPlayer createdPlayer = objectUnderTest.playAGameWithAPlayerOfPosition(PositionType.ATT);//thenassertThat(createdPlayer == offensivePlayer, is(true));verify(offensivePlayer).run();}@Testpublic void shouldReturnDefensivePlayerButHeWontBeADjNorAJavaDev() throws Exception {//givengiven(playerFactory.createPlayer(PositionType.GK)).willReturn(defensivePlayer);//whenPlayer createdPlayer = objectUnderTest.playAGameWithAPlayerOfPosition(PositionType.GK);//thenassertThat(createdPlayer == defensivePlayer, is(true));verify(defensivePlayer).run();verify(defensivePlayer).defend();verifyNoMoreInteractions(defensivePlayer);}@Testpublic void shouldReturnDefensivePlayerBeingADjAndAJavaDev() throws Exception {//givengiven(playerFactory.createPlayer(PositionType.GK)).willReturn(defensivePlayerWithDjAndJavaDevSkills);doAnswer(new Answer<Object>() {@Overridepublic Object answer(InvocationOnMock invocationOnMock) throws Throwable {System.out.println("Hit me baby one more time!");return null;}}).when(((DJ) defensivePlayerWithDjAndJavaDevSkills)).playSomeMusic();doAnswer(new Answer<Object>() {@Overridepublic Object answer(InvocationOnMock invocationOnMock) throws Throwable {System.out.println("public static void main(String... args){\n}");return null;}}).when(((JavaDeveloper) defensivePlayerWithDjAndJavaDevSkills)).doSomeSeriousCoding();//whenPlayer createdPlayer = objectUnderTest.playAGameWithAPlayerOfPosition(PositionType.GK);//thenassertThat(createdPlayer == defensivePlayerWithDjAndJavaDevSkills, is(true));verify(defensivePlayerWithDjAndJavaDevSkills).run();verify(defensivePlayerWithDjAndJavaDevSkills).defend();verify((DJ) defensivePlayerWithDjAndJavaDevSkills).playSomeMusic();verify((JavaDeveloper) defensivePlayerWithDjAndJavaDevSkills).doSomeSeriousCoding();}@Testpublic void shouldReturnDefensivePlayerBeingADjAndAJavaDevByUsingWithSettings() throws Exception {//givenDefensivePlayer defensivePlayerWithDjAndJavaDevSkills = mock(DefensivePlayer.class, withSettings().extraInterfaces(DJ.class, JavaDeveloper.class));given(playerFactory.createPlayer(PositionType.GK)).willReturn(defensivePlayerWithDjAndJavaDevSkills);doAnswer(new Answer<Object>() {@Overridepublic Object answer(InvocationOnMock invocationOnMock) throws Throwable {System.out.println("Hit me baby one more time!");return null;}}).when(((DJ) defensivePlayerWithDjAndJavaDevSkills)).playSomeMusic();doAnswer(new Answer<Object>() {@Overridepublic Object answer(InvocationOnMock invocationOnMock) throws Throwable {System.out.println("public static void main(String... args){\n}");return null;}}).when(((JavaDeveloper) defensivePlayerWithDjAndJavaDevSkills)).doSomeSeriousCoding();//whenPlayer createdPlayer = objectUnderTest.playAGameWithAPlayerOfPosition(PositionType.GK);//thenassertThat(createdPlayer == defensivePlayerWithDjAndJavaDevSkills, is(true));verify(defensivePlayerWithDjAndJavaDevSkills).run();verify(defensivePlayerWithDjAndJavaDevSkills).defend();verify((DJ) defensivePlayerWithDjAndJavaDevSkills).playSomeMusic();verify((JavaDeveloper) defensivePlayerWithDjAndJavaDevSkills).doSomeSeriousCoding();}@Testpublic void shouldReturnCommonPlayer() throws Exception {//givengiven(playerFactory.createPlayer(null)).willReturn(commonPlayer);//whenPlayer createdPlayer = objectUnderTest.playAGameWithAPlayerOfPosition(null);//thenassertThat(createdPlayer, is(commonPlayer));}
}
这里有很多测试,所以让我们看一下最有趣的测试。 但是在开始之前,我们先:提供@RunWith(MockitoJUnitRunner.class)批注,它使我们能够使用Mockito批注,例如@Mock和@InjectMocks 。
说到哪个@Mock注释会创建一个Mock,而@InjectMocks则通过构造函数或setter注入所有模拟(这太棒了吗?)。
对于防御性玩家,我们使用注解extraInterfaces的extra元素,该元素为给定的Mock提供其他接口。 您还可以编写(可以在shouldReturnDefensivePlayerBeingADjAndAJavaDevByUsingWithSettings测试中找到的内容 ):
DefensivePlayer defensivePlayerWithDjAndJavaDevSkills = mock(DefensivePlayer.class, withSettings().extraInterfaces(DJ.class, JavaDeveloper.class));
让我们仔细看看为与DefensivePlayer相关的功能和被测函数的转换部分编写的测试:
@Testpublic void shouldReturnDefensivePlayerBeingADjAndAJavaDev() throws Exception {//givengiven(playerFactory.createPlayer(PositionType.GK)).willReturn(defensivePlayerWithDjAndJavaDevSkills);doAnswer(new Answer<Object>() {@Overridepublic Object answer(InvocationOnMock invocationOnMock) throws Throwable {System.out.println("Hit me baby one more time!");return null;}}).when(((DJ) defensivePlayerWithDjAndJavaDevSkills)).playSomeMusic();doAnswer(new Answer<Object>() {@Overridepublic Object answer(InvocationOnMock invocationOnMock) throws Throwable {System.out.println("public static void main(String... args){\n}");return null;}}).when(((JavaDeveloper) defensivePlayerWithDjAndJavaDevSkills)).doSomeSeriousCoding();//whenPlayer createdPlayer = objectUnderTest.playAGameWithAPlayerOfPosition(PositionType.GK);//thenassertThat(createdPlayer == defensivePlayerWithDjAndJavaDevSkills, is(true));verify(defensivePlayerWithDjAndJavaDevSkills).run();verify(defensivePlayerWithDjAndJavaDevSkills).defend();verify((DJ) defensivePlayerWithDjAndJavaDevSkills).playSomeMusic();verify((JavaDeveloper) defensivePlayerWithDjAndJavaDevSkills).doSomeSeriousCoding();}
我们正在使用BDDMockito静态方法,如给定(...)。willReturn(...)。willAnswer(...)等。然后,将空方法与自定义Anwsers结合使用。 在下一行中,您可以看到,为了存根另一个接口的方法,必须将模拟转换为给定的接口。 这与验证阶段有关,在该阶段,我不检查是否已执行方法,您必须将模拟转换为给定的接口。
您可以通过从工厂返回一个实际的实现来改进测试,或者如果创建一个繁琐的操作,则可以返回这种实现的模拟。 我想在这里展示的是如何在Mockito中使用额外的接口(也许我的用例不是最好的用例)。 无论如何,测试中提出的实现都是不好的,所以我们应该考虑重构它的方法……
一种想法可能是,假设在Service中完成的附加逻辑是对象创建的一部分,将代码原样移至工厂:
PlayFactoryImplWithFieldSettingLogic.java
package com.blogspot.toomuchcoding.factory;import com.blogspot.toomuchcoding.adapter.CommonPlayerAdapter;
import com.blogspot.toomuchcoding.adapter.DefencePlayerAdapter;
import com.blogspot.toomuchcoding.adapter.OffensivePlayerAdapter;
import com.blogspot.toomuchcoding.model.*;/*** User: mgrzejszczak* Date: 09.06.13* Time: 15:53*/public class PlayerFactoryImplWithFieldSettingLogic implements PlayerFactory {@Overridepublic Player createPlayer(PositionType positionType) {PlayerDetails player = createCommonPlayer(positionType);switch (positionType){case ATT:return createOffensivePlayer(player);case MID:return createOffensivePlayer(player);case DEF:return createDefensivePlayer(player);case GK:return createDefensivePlayer(player);default:return new CommonPlayerAdapter(player);}}private Player createDefensivePlayer(PlayerDetails player) {DefencePlayerAdapter defencePlayerAdapter = new DefencePlayerAdapter(player);defencePlayerAdapter.defend();defencePlayerAdapter.playSomeMusic();defencePlayerAdapter.doSomeSeriousCoding();return defencePlayerAdapter;}private OffensivePlayer createOffensivePlayer(PlayerDetails player) {OffensivePlayer offensivePlayer = new OffensivePlayerAdapter(player);offensivePlayer.shoot();return offensivePlayer;}private PlayerDetails createCommonPlayer(PositionType positionType){PlayerDetails playerDetails = new PlayerDetails();playerDetails.setPosition(positionType);return playerDetails;}
}
这样就没有强制转换代码是真正干净的。 现在,PlayerService如下所示:
PlayerServiceImplWIthoutUnnecessaryLogic.java
package com.blogspot.toomuchcoding.service;import com.blogspot.toomuchcoding.factory.PlayerFactory;
import com.blogspot.toomuchcoding.model.*;/*** User: mgrzejszczak* Date: 08.06.13* Time: 19:02*/
public class PlayerServiceImplWithoutUnnecessaryLogic implements PlayerService {private PlayerFactory playerFactory;/*** What's the point in having this method then?* @param positionType* @return*/@Overridepublic Player playAGameWithAPlayerOfPosition(PositionType positionType) {return playerFactory.createPlayer(positionType);}public PlayerFactory getPlayerFactory() {return playerFactory;}public void setPlayerFactory(PlayerFactory playerFactory) {this.playerFactory = playerFactory;}
}
随之而来的问题是,您的代码库中是否甚至需要这种方法……
总结一下,我希望我能展示如何:
- 使用MockitoJUnitRunner以干净的方式注入模拟
- 使用注释或静态方法添加可以被您的模拟使用的额外接口
- 使用BDDMockito执行方法存根
- 带有自定义答案的存根无效方法
- 附加接口的存根和验证方法
- 重构使用类强制转换的代码
这些源可在TooMuchCoding Bitbucket存储库和TooMuchCoding Github存储库中找到。
翻译自: https://www.javacodegeeks.com/2013/06/mockito-extra-interfaces-with-annotations-and-static-methods.html