Статьи

Введение в Test Doubles

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

фиктивный

Первый — это Dummy Object, он самый простой, Dummy — это просто объект, который вы передаете для удовлетворения конструктора, у него не будет реализован какой-либо метод, и он не должен.

Когда мы тестируем класс, мы не хотим ничего делать с регистратором, так что же нам делать?

Например, есть этот PaymentService с регистратором:

1
2
3
public interface Logger {
    void append(String text);
}
01
02
03
04
05
06
07
08
09
10
11
12
13
public class PaymentService {
 
    private Logger logger;
 
    public PaymentService(Logger logger) {
        this.logger = logger;
    }
 
    public PaymentRequest createPaymentRequest(Sale sale, CreditCard creditCard) {
        logger.append("Creating payment for sale " + sale.toString());
        throw new UnsupportedOperationException();
    }
}

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

1
2
3
4
5
public class LoggerDummy implements Logger {
 
    @Override
    public void append(String text) {}
}

В том, что? Там нет кода внутри Dummy. Для этого случая нам не нужна какая-либо реализация внутри, и мы готовы написать тест.

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
class PaymentServiceShould {
 
    @Test
    void create_payment_request() {
        LoggerDummy loggerDummy = new LoggerDummy();
        Customer customer= new Customer("name", "address");
        Item item = new Item("item", 1000);
        List<Item> items= asList(item);
        Sale sale = new Sale(customer, items);
        CreditCard creditCard = new CreditCard(customer, "1");
 
        PaymentService paymentService = new PaymentService(loggerDummy);
        PaymentRequest actual = paymentService.createPaymentRequest(sale, creditCard);
        assertEquals(new PaymentRequest(1000, "1"), actual);
    }
}

Столбики

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

Когда вы тестируете, вы хотите, чтобы ваши тесты были детерминированными и повторяемыми, чтобы они не перестали работать через некоторое время из-за смены соавтора.

Теперь в PaymentRequest должен быть PaymentRequest платеж оператора кредитной карты, размер которого определяется оператором кредитной карты, который определяется первыми четырьмя цифрами карты. Для реализации этого необходимо создать заглушку и добавить необходимые Изменения в PaymentService . Первым шагом будет реализация интерфейса, который нам нужен для нашей заглушки и производственного кода, это та часть, которую вы разрабатываете заранее, думая о том, какими должны быть параметры в вашей заглушке, а что нужно вернуть, не думайте о внутренняя реализация, но контракт, который у вас есть с этим сотрудником:

1
2
3
public interface OperatorRate {
    int feeRate(String operator)
}

Определив интерфейс, мы можем начать писать заглушку:

01
02
03
04
05
06
07
08
09
10
11
12
public class OperatorRateStub implements OperatorRate {
    private int rate;
 
    public OperatorRateStub(int rate){
 
        this.rate = rate;
    }
    @Override
    public int feeRate(String operator) {
        return rate;
    }
}

Заглушка всегда будет возвращать значение, переданное в конструктор, и мы полностью контролируем заглушку, и она полностью изолирована от производственного кода. Теперь тестовый код реализован

01
02
03
04
05
06
07
08
09
10
11
12
13
14
@Test
void create_payment_request() {
    LoggerDummy loggerDummy = new LoggerDummy();
    Customer customer= new Customer("name", "address");
    Item item = new Item("item", 1000);
    List<Item> items= asList(item);
    Sale sale = new Sale(customer, items);
    CreditCard creditCard = new CreditCard(customer, "1");
 
    OperatorRate operatorRate = new OperatorRateStub(10);
    PaymentService paymentService = new PaymentService(loggerDummy, operatorRate);
    PaymentRequest actual = paymentService.createPaymentRequest(sale, creditCard);
    assertEquals(new PaymentRequest(1000, "1", 100), actual);
}

Mocks

Насмешки — это объекты, которые вы можете сказать, что они ожидают получить. Они используются для проверки поведения между тестируемой системой и ее соавторами.

Вы устанавливаете свои ожидания, вызываете метод SUT и проверяете, был ли метод вызван в конце.

Продвигаясь вперед с нашей системой, которую мы поддерживаем, есть новая Пользовательская История, которую мы должны завершить, клиент хочет, чтобы на каждый PaymentRequest свыше 1000 фунтов отправлялось электронное письмо администрации. Есть две причины для изоляции отправки электронной почты:

  • Отправка электронных писем — это занятие, связанное с внешним миром, мы не можем отправлять электронные письма каждый раз, когда запускаем наши тесты, это замедлит тесты и будет очень раздражающим.
  • Служба PaymentService не должна знать о реализации отправителя электронной почты, поскольку смешивание этих двух вещей может создать связь и усложнить обслуживание службы или изменить способ отправки электронных писем, поэтому отправитель электронной почты получает службу самостоятельно.

Шаги, которым мы должны следовать:

  • Создать интерфейс
  • Создать макет, реализующий интерфейс
  • Напишите наш тест

Интерфейс:

1
2
3
public interface PaymentEmailSender {
    void send(PaymentRequest paymentRequest);
}

Тогда мы должны реализовать наш макет:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
public class PaymentServiceMock implements PaymentEmailSender {
 
    private List<PaymentRequest> paymentRequestSent = new ArrayList<>();
    private List<PaymentRequest> expectedPaymentRequest = new ArrayList<>();
 
    @Override
    public void send(PaymentRequest paymentRequest) {
        paymentRequestSent.add(paymentRequest);
    }
 
    public void expect(PaymentRequest paymentRequest) {
        expectedPaymentRequest.add(paymentRequest);
    }
 
    public void verify() {
        assertEquals(paymentRequestSent, expectedPaymentRequest);
    }
}

Это очень простой фиктивный объект, но он будет работать, мы реализуем интерфейс, который мы только что создали, и мы заставляем метод send сохранять PaymentRequest и мы добавляем два метода для настройки mock, expect и verify , метод PaymentRequest использует метод jUnit assertEqual для сравнения ожидаемого значения с значением, передаваемым SUT.

Мы пишем тест для новой истории пользователя:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
@Test
void send_email_to_the_administration_if_sale_is_over_1000() {
    EmailSenderMock emailSender = new EmailSenderMock();
    LoggerDummy loggerDummy = new LoggerDummy();
    OperatorRate operatorRate = new OperatorRateStub(10);
    PaymentService paymentService = new PaymentService(loggerDummy, operatorRate, emailSender);
        PaymentRequest paymentRequest = new PaymentRequest(1000, "1", 100);
    Customer customer= new Customer("name", "address");
    Item item = new Item("item", 1000);
    List<Item> items = asList(item);
    Sale sale = new Sale(customer, items);
    CreditCard creditCard = new CreditCard(customer, "1");
 
    paymentService.createPaymentRequest(sale, creditCard);
 
    emailSender.expect(paymentRequest);
    emailSender.verify();
}

и результат теста:

1
2
3
org.opentest4j.AssertionFailedError:
Expected :[]
Actual   :[PaymentRequest{total=2500, cardNumber='1234123412341234', gatewayFee=250}]

Затем мы переходим к реализации производственного кода:

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
public class PaymentService {
 
        private Logger logger;
        private OperatorRate operatorRate;
        private final EmailSender emailSender;
 
        public PaymentService(Logger logger, OperatorRate operatorRate, EmailSender emailSender) {
            this.logger = logger;
            this.operatorRate = operatorRate;
            this.emailSender = emailSender;
        }
 
        public PaymentRequest createPaymentRequest(Sale sale, CreditCard creditCard) {
            logger.append("Creating payment for sale: " + sale);
 
            int feeRate = operatorRate.feeRate(creditCard.cardNumber);
            int fee = (feeRate * sale.total()) / 100;
 
            PaymentRequest paymentRequest = new PaymentRequest(sale.total(), creditCard.cardNumber, fee);
 
            if (sale.total() >= 1000) {
                emailSender.send(paymentRequest);
            }
            return paymentRequest;
        }
    }

Тесты пройдены и мы закончили с нашей историей.

шпион

Подумайте о шпионе, как о ком-то, кто проник в вашу SUT и записывает каждое его движение, как киношпион. В отличие от насмешек, шпион молчит, и вы должны утверждать, основываясь на данных, которые он предоставляет.

Вы используете шпионов, когда вы не совсем уверены в том, что ваша SUT будет вызывать от вашего соавтора, поэтому вы записываете все и утверждаете, что шпион вызвал нужные данные.

В этом примере мы можем использовать тот же интерфейс, который мы создали для макета, и реализовать новый тест с нашим шпионом.

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
public class PaymentEmailSpy implements PaymentEmailSender {
 
    private List<PaymentRequest> paymentRequests = new ArrayList<>();
 
    @Override
    public void send(PaymentRequest paymentRequest) {
        paymentRequests.add(paymentRequest);
    }
 
    public int timesCalled() {
        return paymentRequests.size();
    }
 
    public boolean calledWith(PaymentRequest paymentRequest) {
        return paymentRequests.contains(paymentRequest);
    }
}

Реализация Spy близка к макету, но вместо того, чтобы давать ожидаемые вызовы, мы просто записываем поведение класса, затем мы идем на тест и можем утверждать, что нам там нужно.

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
class PaymentServiceShould {
 
    private OperatorRate operatorRate;
    private EmailSenderMock emailSender;
    private PaymentService paymentService;
    private LoggerDummy loggerDummy;
    public static final Customer BOB = new Customer("Bob", "address");
    public static final Item IPHONE = new Item("iPhone X", 1000);
    public static final CreditCard BOB_CREDIT_CARD = new CreditCard(BOB, "1");
 
    @BeforeEach
    void setUp() {
        loggerDummy = new LoggerDummy();
        operatorRate = new OperatorRateStub(10);
        emailSender = new EmailSenderMock();
        paymentService = new PaymentService(loggerDummy, operatorRate, emailSender);
    }
 
 
    @Test
    void not_send_email_for_sales_under_1000() {
        Item iphoneCharger = new Item("iPhone Charger", 50);
        Sale sale = new Sale(BOB, asList(iphoneCharger));
        EmailSenderSpy emailSpy = new EmailSenderSpy();
        PaymentService spiedPaymentService = new PaymentService(loggerDummy, operatorRate, emailSpy);
 
        spiedPaymentService.createPaymentRequest(sale, BOB_CREDIT_CARD);
 
        assertEquals(0, emailSpy.timesCalled());
    }
}

Подделки

Мы создаем PaymentService со шпионом, делаем необходимые звонки, а затем можем утверждать, основываясь на данных, предоставленных шпионом.

Подделки отличаются от всех других примеров, которые у нас были, вместо обычных ответов или просто записи звонков, они имеют упрощенную версию бизнес-логики.

Примером Fake может служить репозиторий InMemory, где у нас есть логика для хранения, извлечения и даже выполнения некоторых запросов, но у него не будет реальной базы данных, фактически все может быть сохранено в списке, или вы можете подделать внешний сервис, такой как API.

В этом случае мы могли бы создать подделку для имитации API, который подключается к платежному шлюзу, и использовать его для тестирования нашей производственной реализации OperatorRate .

В этом случае наша производственная реализация отправит Json на шлюз с оператором кредитной карты и получит Json обратно со скоростью, затем выполнит правильный анализ и вернет значение, которое находится в Json.

Итак, мы начинаем писать тест для класса CreditCardRate который реализует OperatorRate

01
02
03
04
05
06
07
08
09
10
11
12
13
public class CreditCardRateShould {
 
    @Test
    void return_rate_for_credit_card_payment() {
        PaymentGateway fakeCreditCardGateway = new FakeCreditCardGateway();
        CreditCardRate creditCardRate = new CreditCardRate(fakeCreditCardGateway);
        String operator = "1234123412341234";
 
        int result = creditCardRate.feeRate(operator);
 
        assertEquals(10, result);
    }
}

Класс, который тестируется, FakeCreditCardGateway с внешним сервисом. Этот сервис подделан FakeCreditCardGateway .

Поддельный шлюз анализирует Json и применяет некоторую действительно простую логику и возвращает другой Json.

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
public class FakeCreditCardGateway implements PaymentGateway {
    @Override
    public String rateFor(String cardOperator) {
        String operator = parseJson(cardOperator);
 
        int rate = 15;
 
        if (operator.startsWith("1234")) {
            rate = 10;
        }
 
        if (operator.startsWith("1235")) {
            rate = 8;
        }
 
        return jsonFor(rate);
    }
 
    private String jsonFor(int rate) {
        return new JsonObject()
                .add("rate", rate)
                .toString();
    }
 
    private String parseJson(String cardOperator) {
        JsonObject payload = Json.parse(cardOperator).asObject();
        return payload.getString("operator", "");
    }
}

и, наконец, есть производственный код для класса CreditCardRate

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
public class CreditCardRate implements OperatorRate {
    private PaymentGateway paymentGateway;
 
    public CreditCardRate(PaymentGateway paymentGateway) {
        this.paymentGateway = paymentGateway;
    }
 
    @Override
    public int feeRate(String operator) {
 
        String payload = jsonFor(operator);
 
        String rateJson = paymentGateway.rateFor(payload);
 
        return parse(rateJson);
    }
 
    private int parse(String rateJson) {
        return Json.parse(rateJson).asObject()
                .getInt("rate", 0);
    }
 
    private String jsonFor(String operator) {
        return new JsonObject()
                .add("operator", operator)
                .toString();
    }
}

С помощью этой фальшивки мы можем проверить, является ли Json, который мы отправляем на шлюз, правильным, иметь некоторую логику, чтобы фальшивый шлюз мог отвечать на разные скорости, и, наконец, мы можем проверить, правильно ли мы анализируем ответ Json.

Это очень специальная реализация без необходимости иметь дело с HTTP-запросом, но мы можем иметь представление о том, как это отразится на реальном мире. Если вы хотите, чтобы интеграционные тесты выполняли реальные HTTP-вызовы, вы можете взглянуть на такие вещи, как WireMock и mockingjay-server .

Мокито и синдром утки

Этот синдром утки имеет не только Mockito, но и большинство насмешливых структур, где они могут делать несколько вещей, утка может плавать, летать и ходить. В этих работах есть манекены, шутки, шпионы и огрызки.

Итак, как мы узнаем, что мы используем при издевательстве со структурой? Чтобы помочь с этим, мы собираемся использовать тесты, которые были написаны с ручным тестом, удваивается и рефакторинг их, чтобы использовать Mockito.

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
57
58
59
60
61
class PaymentServiceShould {
 
    private OperatorRate operatorRate;
    private EmailSenderMock emailSender;
    private PaymentService paymentService;
    private LoggerDummy loggerDummy;
    public static final Customer BOB = new Customer("Bob", "address");
    public static final Item IPHONE = new Item("iPhone X", 1000);
    public static final CreditCard BOB_CREDIT_CARD = new CreditCard(BOB, "1");
 
    @BeforeEach
    void setUp() {
        loggerDummy = new LoggerDummy();
        operatorRate = new OperatorRateStub(10);
        emailSender = new EmailSenderMock();
        paymentService = new PaymentService(loggerDummy, operatorRate, emailSender);
    }
 
    @Test
    void create_payment_request() {
        Sale sale = new Sale(BOB, asList(IPHONE));
 
        PaymentRequest actual = paymentService.createPaymentRequest(sale, BOB_CREDIT_CARD);
 
        assertEquals(new PaymentRequest(1000, "1", 100), actual);
    }
 
    @Test
    void send_email_to_the_administration_if_sale_is_over_1000() {
        Sale sale = new Sale(BOB, asList(IPHONE));
 
        paymentService.createPaymentRequest(sale, BOB_CREDIT_CARD);
 
        emailSender.expect(new PaymentRequest(1000, "1", 100));
        emailSender.verify();
    }
 
    @Test
    void not_send_email_for_sales_under_1000() {
        Item iphoneCharger = new Item("iPhone Charger", 50);
        Sale sale = new Sale(BOB, asList(iphoneCharger));
        EmailSenderSpy emailSpy = new EmailSenderSpy();
        PaymentService spiedPaymentService = new PaymentService(loggerDummy, operatorRate, emailSpy);
 
        spiedPaymentService.createPaymentRequest(sale, BOB_CREDIT_CARD);
 
        assertEquals(0, emailSpy.timesCalled());
    }
 
    @Test
    void send_email_to_hmrs_for_sales_over_10_thousand() {
        Item reallyExpensiveThing = new Item("iPhone Charger", 50000);
        Sale sale = new Sale(BOB, asList(reallyExpensiveThing));
        EmailSenderSpy emailSpy = new EmailSenderSpy();
        PaymentService spiedPaymentService = new PaymentService(loggerDummy, operatorRate, emailSpy);
 
        spiedPaymentService.createPaymentRequest(sale, BOB_CREDIT_CARD);
 
        assertEquals(2, emailSpy.timesCalled());
    }
}

фиктивный

Когда вы создаете Mockito-макет, объект является Dummy, к нему не прикрепляется никакого поведения, поэтому мы можем начать рефакторинг тестов и изменить LoggerDummy для использования объекта Mockito.

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
class PaymentServiceShould {
 
        private OperatorRate operatorRate;
        private EmailSenderMock emailSender;
        private PaymentService paymentService;
-    private LoggerDummy loggerDummy;
+    private Logger logger;
        public static final Customer BOB = new Customer("Bob", "address");
        public static final Item IPHONE = new Item("iPhone X", 1000);
        public static final CreditCard BOB_CREDIT_CARD = new CreditCard(BOB, "1");
 
        @BeforeEach
        void setUp() {
-        loggerDummy = new LoggerDummy();
+        logger = mock(Logger.class);
            operatorRate = new OperatorRateStub(10);
            emailSender = new EmailSenderMock();
-        paymentService = new PaymentService(loggerDummy, operatorRate, emailSender);
+        paymentService = new PaymentService(logger, operatorRate, emailSender);
        }
 
        @Test
@@ -48,7 +49,7 @@ class PaymentServiceShould {
            Item iphoneCharger = new Item("iPhone Charger", 50);
            Sale sale = new Sale(BOB, asList(iphoneCharger));
            EmailSenderSpy emailSpy = new EmailSenderSpy();
-        PaymentService spiedPaymentService = new PaymentService(loggerDummy, operatorRate, emailSpy);
+        PaymentService spiedPaymentService = new PaymentService(logger, operatorRate, emailSpy);
 
            spiedPaymentService.createPaymentRequest(sale, BOB_CREDIT_CARD);
 
@@ -60,7 +61,7 @@ class PaymentServiceShould {
            Item reallyExpensiveThing = new Item("iPhone Charger", 50000);
            Sale sale = new Sale(BOB, asList(reallyExpensiveThing));
            EmailSenderSpy emailSpy = new EmailSenderSpy();
-        PaymentService spiedPaymentService = new PaymentService(loggerDummy, operatorRate, emailSpy);
+        PaymentService spiedPaymentService = new PaymentService(logger, operatorRate, emailSpy);
 
            spiedPaymentService.createPaymentRequest(sale, BOB_CREDIT_CARD);

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

Столбики

Теперь мы должны начать придавать определенное поведение нашим макетам, и, следуя тому же порядку, что и в нашем двойном тесте, мы должны преобразовать объект Mockito в заглушку, так как у Mockito есть метод Given given() котором мы можем установить значение. быть возвращенным.

Для примитивов Mockito возвращает 0, null для объектов и пустую коллекцию для коллекций, таких как List, Map или Set.

given() работает следующим образом:

1
given(<method to be called>).willReturn(returnValue);

и мы изменим реализацию в наших тестах.

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
import static java.util.Arrays.asList;
    import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.mockito.ArgumentMatchers.anyString;
+import static org.mockito.BDDMockito.given;
    import static org.mockito.Mockito.mock;
 
@@ -20,9 +22,10 @@ class PaymentServiceShould {
        @BeforeEach
        void setUp() {
            logger = mock(Logger.class);
-        operatorRate = new OperatorRateStub(10);
+        operatorRate = mock(OperatorRate.class);
            emailSender = new EmailSenderMock();
            paymentService = new PaymentService(logger, operatorRate, emailSender);
+        given(operatorRate.feeRate(BOB_CREDIT_CARD.cardNumber)).willReturn(10);
    }

Теперь макет действует как огрызок, и испытания проходят.

Издевательства и шпионы

В предыдущем тесте, который мы создали, мы все еще используем PaymentEmailMock нами PaymentEmailMock , теперь мы можем изменить его на тот, что в Mockito.

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
@@ -8,11 +8,12 @@ import static org.junit.jupiter.api.Assertions.assertEquals;
    import static org.mockito.ArgumentMatchers.anyString;
    import static org.mockito.BDDMockito.given;
    import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.verify;
 
    class PaymentServiceShould {
 
        private OperatorRate operatorRate;
-    private EmailSenderMock emailSender;
+    private EmailSender emailSender;
        private PaymentService paymentService;
        private Logger logger;
        public static final Customer BOB = new Customer("Bob", "address");
@@ -23,7 +24,7 @@ class PaymentServiceShould {
        void setUp() {
            logger = mock(Logger.class);
            operatorRate = mock(OperatorRate.class);
-        emailSender = new EmailSenderMock();
+        emailSender = mock(EmailSender.class);
            paymentService = new PaymentService(logger, operatorRate, emailSender);
            given(operatorRate.feeRate(BOB_CREDIT_CARD.cardNumber)).willReturn(10);
        }
@@ -43,8 +44,8 @@ class PaymentServiceShould {
 
            paymentService.createPaymentRequest(sale, BOB_CREDIT_CARD);
 
-        emailSender.expect(new PaymentRequest(1000, "1", 100));
-        emailSender.verify();
+        PaymentRequest paymentRequest = new PaymentRequest(1000, "1", 100);
+        verify(emailSender).send(paymentRequest);
        }

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

У нас все еще есть тесты, которые используют шпиона, мы можем изменить тесты, чтобы использовать только mockito.

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
class PaymentServiceShould {
        void not_send_email_for_sales_under_1000() {
            Item iphoneCharger = new Item("iPhone Charger", 50);
            Sale sale = new Sale(BOB, asList(iphoneCharger));
-        EmailSenderSpy emailSpy = new EmailSenderSpy();
-        PaymentService spiedPaymentService = new PaymentService(logger, operatorRate, emailSpy);
 
-        spiedPaymentService.createPaymentRequest(sale, BOB_CREDIT_CARD);
+        paymentService.createPaymentRequest(sale, BOB_CREDIT_CARD);
 
-        assertEquals(0, emailSpy.timesCalled());
+        verify(emailSender, never()).send(any(PaymentRequest.class));
        }
 
        @Test
        void send_email_to_hmrs_for_sales_over_10_thousand() {
            Item reallyExpensiveThing = new Item("iPhone Charger", 50000);
            Sale sale = new Sale(BOB, asList(reallyExpensiveThing));
-        EmailSenderSpy emailSpy = new EmailSenderSpy();
-        PaymentService spiedPaymentService = new PaymentService(logger, operatorRate, emailSpy);
 
-        spiedPaymentService.createPaymentRequest(sale, BOB_CREDIT_CARD);
+        paymentService.createPaymentRequest(sale, BOB_CREDIT_CARD);
 
-        assertEquals(2, emailSpy.timesCalled());
+        PaymentRequest paymentRequest = new PaymentRequest(50000, "1", 5000);
+        verify(emailSender, times(2)).send(paymentRequest);
        }
    }

verify имеет несколько модификаторов, таких как:

  • atLeast(int)
  • atLeastOnce()
  • atMost(int)
  • times(int)

Опять же, у нас есть фиктивный объект, имеющий несколько функций, на этот раз есть Макет и Шпион

Что насчет подделок?

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

Как говорит дядя Боб, его пост «Маленький насмешник»:

Да хм Я не часто пишу подделки. На самом деле, я не писал ни одного более тридцати лет.

Хорошая практика и запахи.

CQS, заглушки и издевательства

Если вы не знакомы с CQS, прочитайте:

OO Tricks: Искусство разделения командного запроса

блики: CommandQuerySeparation

Хорошее эмпирическое правило для принятия решения о том, где использовать заглушки и макеты, — это следовать принципу разделения командных запросов, где у вас есть:

команды

  • У них нет возвращаемых значений
  • Используется для изменения данных внутри вашего класса.
  • Используйте verify verify() при издевательстве с Mockito.

Запросы

  • Запрашивать данные из класса
  • Не создавайте побочных эффектов
  • Просто возвращает данные.
  • Используйте given() при издевательстве с Мокито

Только классы Mock / Stub у вас есть

Одна вещь, которую мы должны понять о насмешках, это не только тестирование, но и проектирование того, как наша SUT работает со своими сотрудниками, будет сложно найти приложение, в котором вы не будете использовать стороннюю библиотеку, но это не значит, что вы должны над ними издеваться, на самом деле вы никогда не должны этого делать. Главное, что вы издеваетесь над сторонними библиотеками, это то, что вы подвержены их изменениям, изменение подписи нарушит все ваши тесты.

Решение? Создавая тонкую оболочку вокруг этой библиотеки, используя инструменты-насмешки, вы можете создать тонкую оболочку, которая получает и возвращает только необходимую информацию, но как мы тестируем наши оболочки?

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

Не издевайтесь над структурами данных.

Если у вас есть свои собственные структуры данных, вам не нужно имитировать их, вы можете просто создать экземпляр с нужными вам данными, в случае, если структуру данных сложно создать, или вам нужно несколько объектов, которые вы можете использовать шаблон Builder.

Вы можете узнать о модели Builder здесь .

Сделайте ваши тесты минималистами

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

Представьте себе, что вместо использования Dummy для логгера в примере в начале использовался макет. Тогда макет будет проверять все сообщения, которые пропускает регистратор, и что-либо менять, что сломает тест. Никто не хочет, чтобы их тесты ломались только потому, что они исправили опечатку в журналах.

Не используйте макеты / заглушки для проверки граничных / изолированных объектов

Объекты, у которых нет коллабораторов, не должны тестироваться с фиктивными объектами, подобный объект просто нуждается в утверждениях в значениях, которые возвращаются или хранятся. Звучит немного очевидно, но это хорошо, чтобы усилить это.

Для зависимости, такой как анализатор JSON, вы можете протестировать упаковщик на работающей реальной зависимости. Вы можете увидеть это в действии в примере для Fake, вместо того, чтобы издеваться над библиотекой Json, использовалась настоящая, что-то вроде обертки, чтобы можно было использовать преобразование, тогда мы должны были бы протестировать обертку с настоящим Json библиотеку и посмотрим, правильно ли создан json, в этом случае мы никогда не посмеемся над этой зависимостью.

Не добавлять поведение

Мок — это тестовые двойники, и вам не нужно добавлять сложность в ваши тестовые двойники, у вас есть подделки, которые содержат некоторую логику, но, кроме того, ни один из тестовых двойников не должен содержать логику, это признак того, что вы расставили обязанности.

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

Только издеваться / заглушки ваших ближайших соседей

Сложный объект, который может иметь несколько зависимостей, может быть трудно тестировать, и один из признаков, который мы можем видеть из этого, состоит в том, что настройка теста сложна, и тест также трудно читать. Модульные тесты должны быть направлены на то, чтобы протестировать одну вещь одновременно, и должны устанавливать только ожидания для своих соседей (подумайте о Законе Деметры). Возможно, вам придется ввести роль для соединения объекта и его окружения.

Слишком много издевательств

У вашего SUT может быть несколько соавторов, и ваши тесты становятся более сложными в настройке и трудными для чтения, как в других ситуациях, которые мы видели, у SUT может быть слишком много обязанностей, чтобы решить, что вам придется сломать ваш объект на более мелкие, более сфокусированные.

Так что если у вас есть сервис с несколькими классами в конструкторе, например:

1
2
3
4
5
6
7
8
public ReadCommand(UserRepository userRepository, MessageRepository messageRepository,
                    MessageFormatter messageFormatter, Console console, String username) {
    this.userRepository = userRepository;
    this.messageRepository = messageRepository;
    this.messageFormatter = messageFormatter;
    this.console = console;
    this.username = username;
}

Вы можете изменить это, чтобы стать:

1
2
3
4
5
6
7
public ReadCommand(UserRepository userRepository, MessageRepository messageRepository,
                                        MessagePrinter messagePrinter, String username) {
    this.userRepository = userRepository;
    this.messageRepository = messageRepository;
    this.messagePrinter = messagePrinter;
    this.username = username;
}

Теперь у MessagePrinter есть MessageFormatter и Console работающие вместе, поэтому при тестировании класса ReadCommand вам просто нужно проверить, был ли вызван метод для печати.

Смотрите оригинальную статью здесь: Введение в Test Doubles

Мнения, высказанные участниками Java Code Geeks, являются их собственными.