В коде, который я недавно обнаружил, действительно плохой фрагмент кода, основанный на приведении классов с точки зрения выполнения некоторых действий над объектами. Конечно, код необходимо реорганизовать, но иногда вы не можете этого сделать / или не хотите этого делать (и это должно быть понятно), если сначала у вас нет модульных тестов этой функциональности. В следующем посте я покажу, как тестировать такой код, как его реорганизовать и что я думаю о таком коде.
Давайте посмотрим на структуру проекта:
Как показано в сообщении, касающемся Mocktio RETURNS_DEEP_STUBS Ответ для JAXB, еще раз мы имеем классы, сгенерированные JAXB компилятором JAXB в пакете com.blogspot.toomuchcoding.model . Давайте не будем обсуждать файл pom.xml, поскольку он точно такой же, как и в предыдущем посте.
В пакете com.blogspot.toomuchcoding.adapter у нас есть адаптеры для класса PlayerDetails JAXB, который обеспечивает доступ к интерфейсу Player. Здесь:
CommonPlayerAdapter.java
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
|
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
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
|
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
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
|
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
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
|
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; } } |
Итак, у нас есть фабрика, которая строит игроков. Давайте посмотрим на сервис, который использует фабрику:
PlayerServiceImpl.java
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
|
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; } } |
Давайте признаем это … этот код плохой. Внутренне, когда вы смотрите на него (независимо от того, использовал ли он экземпляр оператора или нет), вы чувствуете, что это зло. Как вы можете видеть в коде, у нас есть некоторые классы… Как же мы можем это проверить? В большинстве сред тестирования вы не можете выполнять такие приведения классов к макетам, поскольку они построены с помощью библиотеки CGLIB и могут быть выброшены некоторые исключения ClassCastException. Вы все еще не могли бы возвращать макеты и реальные реализации (при условии, что они не будут выполнять какие-либо уродливые вещи в процессе построения), и это могло бы действительно работать, но все же — это плохой код
На помощь приходит Mockito (хотя вам не следует злоупотреблять этой функцией — на самом деле, если вам нужно ее использовать, рассмотрите возможность ее рефакторинга) с ее функцией extraInterfaces :
extraInterfaces
MockSettings extraInterfaces (интерфейсы java.lang.Class <?>…)
Определяет дополнительные интерфейсы, которые должен реализовывать макет. Может быть полезно для устаревшего кода или некоторых угловых случаев. Для справки, см. Выпуск 51 здесь. Эта загадочная функция должна использоваться очень редко. Тестируемый объект должен точно знать своих соавторов и зависимости. Если вы используете его часто, убедитесь, что вы действительно производите простой, чистый и читаемый код.
Примеры:
1
2
3
4
5
6
7
|
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
001
002
003
004
005
006
007
008
009
010
011
012
013
014
015
016
017
018
019
020
021
022
023
024
025
026
027
028
029
030
031
032
033
034
035
036
037
038
039
040
041
042
043
044
045
046
047
048
049
050
051
052
053
054
055
056
057
058
059
060
061
062
063
064
065
066
067
068
069
070
071
072
073
074
075
076
077
078
079
080
081
082
083
084
085
086
087
088
089
090
091
092
093
094
095
096
097
098
099
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
|
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 внедряет все макеты либо конструктором, либо сеттерами (это здорово, не правда ли?).
Для защитного игрока мы используем дополнительный элемент аннотации extraInterfaces, который предоставляет дополнительные интерфейсы для данного макета. Вы также можете написать (что вы можете найти в тесте shouldReturnDefensivePlayerBeingADjAndAJavaDevByUsingWithSettings ):
1
|
DefensivePlayer defensivePlayerWithDjAndJavaDevSkills = mock(DefensivePlayer. class , withSettings().extraInterfaces(DJ. class , JavaDeveloper. class )); |
Давайте внимательнее посмотрим на тест, который мы написали для функциональности, связанной с DefensivePlayer, и части приведения протестированной функции:
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
|
@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 (…) и т. Д. Затем мы используем методы void с нашими пользовательскими Anwsers. В следующей строке вы можете увидеть, что для того, чтобы заглушить методы другого интерфейса, вы должны привести макет к данному интерфейсу. То же самое относится и к фазе верификации, когда я проверяю, был ли выполнен метод, вы должны привести макет к данному интерфейсу.
Вы можете улучшить тест, вернув реальную реализацию с завода, или, если это «тяжелая» операция по ее созданию, вы можете вернуть макет такой реализации. Здесь я хотел показать, как использовать дополнительные интерфейсы в Mockito (возможно, мой вариант использования не самый лучший). В любом случае, реализация, представленная в тесте, плохая, поэтому мы должны подумать о способах ее рефакторинга
Одной из идей может быть, предполагая, что дополнительная логика, выполняемая в Сервисе, является частью создания объекта, для перемещения кода на фабрику как таковую:
PlayFactoryImplWithFieldSettingLogic.java
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
|
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
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
|
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 .