在代码中,我最近遇到了一段非常糟糕的代码,这些代码基于对对象执行某些操作的类转换。 当然,代码需要重构,但是如果您首先没有对该功能进行单元测试,则有时您可能无法做到/或者不想这样做(这应该是可以理解的)。 在下面的文章中,我将展示如何测试这种代码,如何重构它以及实际上我对这种代码的看法。
让我们看一下项目结构:
如关于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;
}
@Override
public 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);
}
@Override
public void defend(){
System.out.printf("Defence! %s. Defence!%n", getPlayerDetails().getName());
}
@Override
public void playSomeMusic() {
System.out.println("Oops I did it again...!");
}
@Override
public 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);
}
@Override
public 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 {
@Override
public 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;
@Override
public 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 {
@Mock
PlayerFactory playerFactory;
@InjectMocks
PlayerServiceImpl objectUnderTest;
@Mock(extraInterfaces = {DJ.class, JavaDeveloper.class})
DefensivePlayer defensivePlayerWithDjAndJavaDevSkills;
@Mock
DefensivePlayer defensivePlayer;
@Mock
OffensivePlayer offensivePlayer;
@Mock
Player commonPlayer;
@Test
public void shouldReturnOffensivePlayerThatRan() throws Exception {
//given
given(playerFactory.createPlayer(PositionType.ATT)).willReturn(offensivePlayer);
//when
Player createdPlayer = objectUnderTest.playAGameWithAPlayerOfPosition(PositionType.ATT);
//then
assertThat(createdPlayer == offensivePlayer, is(true));
verify(offensivePlayer).run();
}
@Test
public void shouldReturnDefensivePlayerButHeWontBeADjNorAJavaDev() throws Exception {
//given
given(playerFactory.createPlayer(PositionType.GK)).willReturn(defensivePlayer);
//when
Player createdPlayer = objectUnderTest.playAGameWithAPlayerOfPosition(PositionType.GK);
//then
assertThat(createdPlayer == defensivePlayer, is(true));
verify(defensivePlayer).run();
verify(defensivePlayer).defend();
verifyNoMoreInteractions(defensivePlayer);
}
@Test
public void shouldReturnDefensivePlayerBeingADjAndAJavaDev() throws Exception {
//given
given(playerFactory.createPlayer(PositionType.GK)).willReturn(defensivePlayerWithDjAndJavaDevSkills);
doAnswer(new Answer<Object>() {
@Override
public 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>() {
@Override
public Object answer(InvocationOnMock invocationOnMock) throws Throwable {
System.out.println("public static void main(String... args){\n}");
return null;
}
}).when(((JavaDeveloper) defensivePlayerWithDjAndJavaDevSkills)).doSomeSeriousCoding();
//when
Player createdPlayer = objectUnderTest.playAGameWithAPlayerOfPosition(PositionType.GK);
//then
assertThat(createdPlayer == defensivePlayerWithDjAndJavaDevSkills, is(true));
verify(defensivePlayerWithDjAndJavaDevSkills).run();
verify(defensivePlayerWithDjAndJavaDevSkills).defend();
verify((DJ) defensivePlayerWithDjAndJavaDevSkills).playSomeMusic();
verify((JavaDeveloper) defensivePlayerWithDjAndJavaDevSkills).doSomeSeriousCoding();
}
@Test
public void shouldReturnDefensivePlayerBeingADjAndAJavaDevByUsingWithSettings() throws Exception {
//given
DefensivePlayer defensivePlayerWithDjAndJavaDevSkills = mock(DefensivePlayer.class, withSettings().extraInterfaces(DJ.class, JavaDeveloper.class));
given(playerFactory.createPlayer(PositionType.GK)).willReturn(defensivePlayerWithDjAndJavaDevSkills);
doAnswer(new Answer<Object>() {
@Override
public 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>() {
@Override
public Object answer(InvocationOnMock invocationOnMock) throws Throwable {
System.out.println("public static void main(String... args){\n}");
return null;
}
}).when(((JavaDeveloper) defensivePlayerWithDjAndJavaDevSkills)).doSomeSeriousCoding();
//when
Player createdPlayer = objectUnderTest.playAGameWithAPlayerOfPosition(PositionType.GK);
//then
assertThat(createdPlayer == defensivePlayerWithDjAndJavaDevSkills, is(true));
verify(defensivePlayerWithDjAndJavaDevSkills).run();
verify(defensivePlayerWithDjAndJavaDevSkills).defend();
verify((DJ) defensivePlayerWithDjAndJavaDevSkills).playSomeMusic();
verify((JavaDeveloper) defensivePlayerWithDjAndJavaDevSkills).doSomeSeriousCoding();
}
@Test
public void shouldReturnCommonPlayer() throws Exception {
//given
given(playerFactory.createPlayer(null)).willReturn(commonPlayer);
//when
Player createdPlayer = objectUnderTest.playAGameWithAPlayerOfPosition(null);
//then
assertThat(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相关的功能和被测函数的转换部分编写的测试:
@Test
public void shouldReturnDefensivePlayerBeingADjAndAJavaDev() throws Exception {
//given
given(playerFactory.createPlayer(PositionType.GK)).willReturn(defensivePlayerWithDjAndJavaDevSkills);
doAnswer(new Answer<Object>() {
@Override
public 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>() {
@Override
public Object answer(InvocationOnMock invocationOnMock) throws Throwable {
System.out.println("public static void main(String... args){\n}");
return null;
}
}).when(((JavaDeveloper) defensivePlayerWithDjAndJavaDevSkills)).doSomeSeriousCoding();
//when
Player createdPlayer = objectUnderTest.playAGameWithAPlayerOfPosition(PositionType.GK);
//then
assertThat(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 {
@Override
public 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
*/
@Override
public 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存储库中找到。