Статьи

Тест-драйв Строителей с Mockito и Hamcrest

В прошлом многие люди спрашивали меня, тестирую ли я геттеры и сеттеры (свойства, атрибуты и т. Д.). Они также спросили меня, проверяю ли я своих строителей. Ответ, в моем случае это зависит.

Работая с унаследованным кодом, я не стал бы проверять структуры данных, то есть объекты, содержащие только геттеры и сеттеры, карты, списки и т. Д. Одна из причин заключается в том, что я никогда не высмеиваю их. Я использую их как они есть при тестировании классов, которые их используют. Для сборщиков, когда они используются только тестовыми классами, я также не тестирую их модульно, поскольку они используются в качестве «помощников» во многих других тестах. Если у них есть ошибка, тесты не пройдут. Таким образом, если бы эти структуры данных и компоновщики уже существовали, я бы не стал их модернизировать.

Но теперь давайте поговорим о создании TDD и предположим, что вам нужен новый объект с геттерами и сеттерами. В этом случае, да, я бы написал тесты для геттеров и сеттеров, поскольку мне нужно сначала обосновать их существование, написав мои тесты.
Чтобы иметь богатую модель предметной области, я обычно склонен связывать бизнес-логику с данными и иметь более богатую модель предметной области. Давайте посмотрим на следующий пример.

В реальной жизни я бы писал на тесте за раз, заставляя их проходить и рефакторинг. Для этого поста я просто дам вам полные уроки для ясности. Сначала напишем тесты:

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
package org.craftedsw.testingbuilders;
 
import static org.hamcrest.Matchers.is;
import static org.junit.Assert.assertThat;
import static org.mockito.Matchers.anyString;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
 
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.Mock;
import org.mockito.runners.MockitoJUnitRunner;
 
@RunWith(MockitoJUnitRunner.class)
public class TradeTest {
 
 private static final String INBOUND_XML_MESSAGE = '<message >';
 private static final boolean REPORTABILITY_RESULT = true;
 private Trade trade;
 
 @Mock private ReportabilityDecision reportabilityDecision;
 
 @Before
 public void initialise() {
  trade = new Trade();
  when(reportabilityDecision.isReportable(anyString()))
    .thenReturn(REPORTABILITY_RESULT);
 }
 
 @Test public void
 should_contain_the_inbound_xml_message() {
  trade.setInboundMessage(INBOUND_XML_MESSAGE);
 
  assertThat(trade.getInboundMessage(), is(INBOUND_XML_MESSAGE));
 }
 
 @Test public void
 should_tell_if_it_is_reportable() {
  trade.setInboundMessage(INBOUND_XML_MESSAGE);
  trade.setReportabilityDecision(reportabilityDecision);
 
  boolean reportable = trade.isReportable();
 
  verify(reportabilityDecision).isReportable(INBOUND_XML_MESSAGE);
  assertThat(reportable, is(REPORTABILITY_RESULT));
 }
 
}

Теперь реализация:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
package org.craftedsw.testingbuilders;
 
public class Trade {
 
 private String inboundMessage;
 private ReportabilityDecision reportabilityDecision;
 
 public String getInboundMessage() {
  return this.inboundMessage;
 }
 
 public void setInboundMessage(String inboundXmlMessage) {
  this.inboundMessage = inboundXmlMessage;
 }
 
 public boolean isReportable() {
  return reportabilityDecision.isReportable(inboundMessage);
 }
 
 public void setReportabilityDecision(ReportabilityDecision reportabilityDecision) {
  this.reportabilityDecision = reportabilityDecision;
 }
 
}

Этот случай интересен, поскольку у объекта Trade есть одно свойство, называемое inboundMessage, с соответствующими получателями и установщиками, а также использует коллаборатор (reportabilityDecision, введенный через установщик) в своем бизнес-методе isReportable.

Обычный подход, который я видел много раз для «тестирования» метода setReportabilityDecision, заключается во введении метода getReportabilityDecision, возвращающего объект reportabilityDecision (сотрудник).

Это определенно неправильный подход. Наша цель должна состоять в том, чтобы проверить, как используется коллаборатор, то есть, если он вызывается с правильными параметрами и используется ли все, что он возвращает (если он возвращает что-либо). Представлять геттер в этом случае не имеет смысла, так как он не гарантирует, что объект, после того, как соавтор введен через сеттер, взаимодействует с коллаборатором, как мы и предполагали.

Кроме того, когда мы пишем тесты о том, как будут использоваться коллаборационисты, определяя их интерфейс, это когда мы используем TDD как инструмент проектирования, а не просто как инструмент тестирования. Я расскажу об этом в следующем посте.

Хорошо, теперь представьте, что этот торговый объект может быть создан по-разному, то есть, с разными решениями по отчетности. Мы также хотели бы сделать наш код более читабельным и решили написать конструктор для объекта Trade. Давайте также предположим, что в этом случае мы хотим, чтобы конструктор также использовался в рабочем и тестовом коде. В этом случае мы хотим протестировать наш строитель.

Вот пример, который я обычно нахожу, когда разработчики тестируют реализацию сборки.

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
package org.craftedsw.testingbuilders;
 
import static org.craftedsw.testingbuilders.TradeBuilder.aTrade;
import static org.hamcrest.Matchers.is;
import static org.junit.Assert.assertThat;
import static org.mockito.Mockito.verify;
 
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.Mock;
import org.mockito.runners.MockitoJUnitRunner;
 
@RunWith(MockitoJUnitRunner.class)
public class TradeBuilderTest {
 
 private static final String TRADE_XML_MESSAGE = '<message >';
 
 @Mock
 private ReportabilityDecision reportabilityDecision;
 
 @Test public void
 should_create_a_trade_with_inbound_message() {
  Trade trade = aTrade()
        .withInboundMessage(TRADE_XML_MESSAGE)
        .build();
 
  assertThat(trade.getInboundMessage(), is(TRADE_XML_MESSAGE));
 }
 
 @Test public void
 should_create_a_trade_with_a_reportability_decision() {
  Trade trade = aTrade()
        .withInboundMessage(TRADE_XML_MESSAGE)
        .withReportabilityDecision(reportabilityDecision)
        .build();
 
  trade.isReportable();
 
  verify(reportabilityDecision).isReportable(TRADE_XML_MESSAGE);
 }
 
}

Теперь давайте посмотрим на эти тесты. Хорошая новость в том, что тесты были написаны так, как разработчики хотят их прочитать. Это также означает, что они «проектировали» открытый интерфейс TradeBuilder (публичные методы). Плохая новость в том, как они это проверяют.

Если вы посмотрите ближе, тесты для компоновщика практически идентичны тестам в классе TradeTest.
Вы можете сказать, что все в порядке, поскольку создатель создает объект и тесты должны быть похожими. Единственное отличие состоит в том, что в TradeTest мы создаем экземпляр объекта вручную, а в TradeBuilderTest мы используем его создатель, но утверждения должны быть одинаковыми, верно?
Для меня, во-первых, у нас есть дублирование. Во-вторых, TradeBuilderTest не показывает его истинное намерение.
После многих рефакторингов и изучения различных идей во время парного программирования с одним из парней из моей команды мы разработали такой подход:

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
package org.craftedsw.testingbuilders;
 
import static org.mockito.BDDMockito.given;
import static org.mockito.Mockito.verify;
 
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.Spy;
import org.mockito.runners.MockitoJUnitRunner;
 
@RunWith(MockitoJUnitRunner.class)
public class TradeBuilderTest {
 
 private static final String TRADE_XML_MESSAGE = '<message >';
 
 @Mock private ReportabilityDecision reportabilityDecision;
 @Mock private Trade trade;
 
 @Spy @InjectMocks TradeBuilder tradeBuilder;
 
 @Test public void
 should_create_a_trade_with_all_specified_attributes() {
  given(tradeBuilder.createTrade()).willReturn(trade);
 
  tradeBuilder
   .withInboundMessage(TRADE_XML_MESSAGE)
   .withReportabilityDecision(reportabilityDecision)
   .build();
 
  verify(trade).setInboundMessage(TRADE_XML_MESSAGE);
  verify(trade).setReportabilityDecision(reportabilityDecision);
 }
 
}

Итак, теперь TradeBuilderTest выражает то, что ожидается от TradeBuilder, то есть побочный эффект при вызове метода сборки. Мы хотим, чтобы он создал сделку и установил ее атрибуты. Там нет дубликатов с TradeTest. TradeTest оставляет за собой право гарантировать правильное поведение объекта Trade.
Для завершения, вот последний класс TradeBuider:

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 org.craftedsw.testingbuilders;
 
public class TradeBuilder {
 
 private String inboundMessage;
 private ReportabilityDecision reportabilityDecision;
 
 public static TradeBuilder aTrade() {
  return new TradeBuilder();
 }
 
 public TradeBuilder withInboundMessage(String inboundMessage) {
  this.inboundMessage = inboundMessage;
  return this;
 }
 
 public TradeBuilder withReportabilityDecision(ReportabilityDecision reportabilityDecision) {
  this.reportabilityDecision = reportabilityDecision;
  return this;
 }
 
 public Trade build() {
  Trade trade = createTrade();
  trade.setInboundMessage(inboundMessage);
  trade.setReportabilityDecision(reportabilityDecision);
  return trade;
 }
 
 Trade createTrade() {
  return new Trade();
 }
 
}

Сочетание Mockito и Hamcrest чрезвычайно мощно, что позволяет нам писать более качественные и удобочитаемые тесты.

Ссылка: тест-драйв Builders с Mockito и Hamcrest от нашего партнера JCG Сандро Манкузо в блоге Crafted Software .