Сделать приватный метод общедоступным для модульного тестирования ... хорошая идея? - PullRequest
279 голосов
/ 16 августа 2011

Примечание модератора: Здесь уже опубликовано 39 ответов (некоторые из них были удалены). Перед тем как опубликовать ваш ответ, подумайте, стоит ли илиВы не можете добавить что-то значимое к обсуждению.Скорее всего, вы просто повторяете то, что уже сказал кто-то еще.


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

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

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

ОБНОВЛЕНИЕ

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

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

Ответы [ 33 ]

0 голосов
/ 16 августа 2011

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

0 голосов
/ 31 декабря 2017

Очень отвеченный вопрос.
IHMO, отличный ответ от @BlueRaja - Дэнни Пфлугхофт - один из лучших.

Много ответов предлагают только тестирование публичного интерфейса, но ИМХО это нереально - если метод делает что-то, что занимает 5 шагов, вам нужно проверить эти пять шагов отдельно, а не все вместе. Это требует тестирования всех пяти методов, которые (кроме тестирования) в противном случае может быть частным.


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

Здесь некоторые ответы можно обобщить так: «Часто хорошо, что« или »никогда не бывает, это плохо. Не обманывайте API и тестируйте только публичное поведение» «.
Это меня очень раздражает, потому что качество проектирования для тестов и реализации является важным вопросом, и этот вопрос подразумевает много последствий для обоих.


Публичный метод private или извлечение приватного метода как public метода в другом классе (новом или существующем)?

Для случаев, когда приемлемо расширение видимости private, решение, основанное на использовании метода private public, часто оказывается не лучшим способом.
Это снижает качество дизайна и тестируемость класса.
Модульный тест должен проверить поведение one API метод / функция.
Если вы тестируете метод public, который вызывает другой метод public, принадлежащий тому же компоненту, вы не юнит-тест метод. Вы тестируете несколько public методов одновременно.
Как следствие, вы можете дублировать тесты, тестовые таблицы, тестовые утверждения, тестовое сопровождение и, в более общем плане, дизайн приложения.
По мере того, как значение тестов уменьшается, они часто теряют интерес для разработчиков, которые пишут или поддерживают их.

Чтобы избежать всего этого дублирования, вместо метода private метод public во многих случаях лучшим решением является извлечение метода private как метода public в новом или существующем класс .
Это не создаст дефект дизайна.
Это сделает код более значимым, а класс - менее раздутым.
Кроме того, иногда метод private является подпрограммой / подмножеством класса, тогда как поведение лучше подходит для конкретной структуры.
Наконец, это также делает код более тестируемым и позволяет избежать дублирования тестов.
Мы действительно можем предотвратить дублирование тестов, выполнив модульное тестирование метода public в своем собственном классе тестирования, а в классе тестирования клиентских классов - просто смоделировать зависимость.

Насмешливые приватные методы?

Конечно, это возможно при использовании отражения или с инструментами в качестве PowerMock, но IHMO Я думаю, что это часто - способ обойти проблему проектирования.
Тестовый класс - это класс как другой.
Член private не предназначен для других классов. Поэтому мы должны следовать тому же правилу для тестовых классов.

Насмешливые публичные методы тестируемого объекта?

Вы можете изменить модификатор private на public для проверки метода.
Затем, чтобы протестировать метод, использующий этот измененный публичный метод, у вас может возникнуть желание смоделировать измененный метод public, используя инструменты в качестве Mockito ( spy concept ), но, аналогично использованию насмешливых методов private, мы должны избегать издеваться над тестируемым объектом.

Документация Mockito.spy() говорит сама за себя:

Создает шпиона реального объекта. Шпион вызывает реальные методы, если они не>> заглушены.

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

Опыт показывает, что использование spy() обычно снижает качество теста и его удобочитаемость.
Кроме того, оно намного более подвержено ошибкам, так как тестируемый объект является и фиктивным, и реальным объектом.
Эточасто лучший способ написать недействительный приемочный тест.


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

Случай 1) Никогда не используйте метод private public, если этот метод вызывается один раз .
Это метод private для одного метод.Таким образом, вы никогда не сможете дублировать тестовую логику, поскольку она вызывается один раз.

Случай 2) Вы должны задаться вопросом, следует ли реорганизовать метод private как метод public, если метод privateвызвано более одного раза .

Как решить?

  • Метод private не производит дублирования втесты.
    -> Сохранить метод private как есть.

  • Метод private производит дублирование в тестах.То есть вам нужно повторить некоторые тесты, чтобы утверждать ту же логику для каждого теста, который выполняет модульное тестирование public методов с использованием метода private.
    -> Если повторная обработка может сделать частьAPI предоставляется клиентам (без проблем безопасности, без внутренней обработки и т. Д.), извлекает метод private как метод public в новом классе .
    ->В противном случае, , если повторная обработка не делает часть API предоставленной клиентам (проблема безопасности, внутренняя обработка и т. Д.), не расширяет видимость privateметод к public.
    Вы можете оставить его без изменений или переместить метод в класс пакета private, который никогда не станет частью API и никогда не будет доступен клиентам.


Примеры кода

Примеры основаны на Java и следующих библиотеках: JUnit, AssertJ (сопоставление утверждений) и Mockito.
Но я думаю, что общий подход также действителен для C #.

1) Пример, где метод private не создает дублирования в тестовом коде

Вот класс Computation, который предоставляет методы для выполнениянекоторые вычисления.
Все открытые методы используют метод mapToInts().

public class Computation {

    public int add(String a, String b) {
        int[] ints = mapToInts(a, b);
        return ints[0] + ints[1];
    }

    public int minus(String a, String b) {
        int[] ints = mapToInts(a, b);
        return ints[0] - ints[1];
    }

    public int multiply(String a, String b) {
        int[] ints = mapToInts(a, b);
        return ints[0] * ints[1];
    }

    private int[] mapToInts(String a, String b) {
        return new int[] { Integer.parseInt(a), Integer.parseInt(b) };
    }

}

Вот код теста:

public class ComputationTest {

    private Computation computation = new Computation();

    @Test
    public void add() throws Exception {
        Assert.assertEquals(7, computation.add("3", "4"));
    }

    @Test
    public void minus() throws Exception {
        Assert.assertEquals(2, computation.minus("5", "3"));
    }

    @Test
    public void multiply() throws Exception {
        Assert.assertEquals(100, computation.multiply("20", "5"));
    }

}

Мы могли видеть, что вызов private метод mapToInts() не дублирует логику теста.
Это промежуточная операция, и она не дает определенного результата, который мы должны утверждать в тестах.

2) Пример, где метод private создает нежелательное дублирование в тестовом коде

Вот класс MessageService, который предоставляет методы для создания сообщений.
Все publicметоды используют метод createHeader():

public class MessageService {

    public Message createMessage(String message, Credentials credentials) {
        Header header = createHeader(credentials, message, false);
        return new Message(header, message);
    }

    public Message createEncryptedMessage(String message, Credentials credentials) {
        Header header = createHeader(credentials, message, true);
        // specific processing to encrypt
        // ......
        return new Message(header, message);
    }

    public Message createAnonymousMessage(String message) {
        Header header = createHeader(Credentials.anonymous(), message, false);
        return new Message(header, message);
    }

    private Header createHeader(Credentials credentials, String message, boolean isEncrypted) {
        return new Header(credentials, message.length(), LocalDate.now(), isEncrypted);
    }

}

Вот код теста:

import java.time.LocalDate;

import org.assertj.core.api.Assertions;
import org.junit.Test;

import junit.framework.Assert;

public class MessageServiceTest {

    private MessageService messageService = new MessageService();

    @Test
    public void createMessage() throws Exception {
        final String inputMessage = "simple message";
        final Credentials inputCredentials = new Credentials("user", "pass");
        Message actualMessage = messageService.createMessage(inputMessage, inputCredentials);
        // assertion
        Assert.assertEquals(inputMessage, actualMessage.getMessage());
        Assertions.assertThat(actualMessage.getHeader())
                  .extracting(Header::getCredentials, Header::getLength, Header::getDate, Header::isEncryptedMessage)
                  .containsExactly(inputCredentials, 9, LocalDate.now(), false);
    }

    @Test
    public void createEncryptedMessage() throws Exception {
        final String inputMessage = "encryted message";
        final Credentials inputCredentials = new Credentials("user", "pass");
        Message actualMessage = messageService.createEncryptedMessage(inputMessage, inputCredentials);
        // assertion
        Assert.assertEquals("Aç4B36ddflm1Dkok49d1d9gaz", actualMessage.getMessage());
        Assertions.assertThat(actualMessage.getHeader())
                  .extracting(Header::getCredentials, Header::getLength, Header::getDate, Header::isEncryptedMessage)
                  .containsExactly(inputCredentials, 9, LocalDate.now(), true);
    }

    @Test
    public void createAnonymousMessage() throws Exception {
        final String inputMessage = "anonymous message";
        Message actualMessage = messageService.createAnonymousMessage(inputMessage);
        // assertion
        Assert.assertEquals(inputMessage, actualMessage.getMessage());
        Assertions.assertThat(actualMessage.getHeader())
                  .extracting(Header::getCredentials, Header::getLength, Header::getDate, Header::isEncryptedMessage)
                  .containsExactly(Credentials.anonymous(), 9, LocalDate.now(), false);
    }

}

Мы могли видеть, что вызов метода private createHeader()создает некоторое дублирование в тестовой логике.
createHeader() создает действительно конкретный результат, который нам нуженошибаться в тестах.
Мы утверждаем содержимое заголовка в 3 раза, в то время как требуется одно утверждение.

Мы могли бы также отметить, что дублирование утверждения близко между методами, но не обязательно одинаковотак как метод private имеет особую логику: конечно, у нас может быть больше различий в зависимости от логической сложности метода private.
Кроме того, каждый раз мы добавляем новый метод public в MessageService который вызывает createHeader(), нам нужно будет добавить это утверждение.
Обратите внимание также, что если createHeader() изменяет его поведение, все эти тесты также могут нуждаться в изменении.
Определенно, это не очень хорошоdesign.

Шаг рефакторинга

Предположим, мы находимся в случае, когда createHeader() приемлемо для создания части API.
Мы начнемпутем рефакторинга класса MessageService путем изменения модификатора доступа createHeader() на public:

public Header createHeader(Credentials credentials, String message, boolean isEncrypted) {
    return new Header(credentials, message.length(), LocalDate.now(), isEncrypted);
}

Теперь мы можем протестировать этот метод однократно:

@Test
public void createHeader_with_encrypted_message() throws Exception {
  ...
  boolean isEncrypted = true;
  // action
  Header actualHeader = messageService.createHeader(credentials, message, isEncrypted);
  // assertion
  Assertions.assertThat(actualHeader)
              .extracting(Header::getCredentials, Header::getLength, Header::getDate, Header::isEncryptedMessage)
              .containsExactly(Credentials.anonymous(), 9, LocalDate.now(), true);
}

@Test
public void createHeader_with_not_encrypted_message() throws Exception {
  ...
  boolean isEncrypted = false;
  // action
  messageService.createHeader(credentials, message, isEncrypted);
  // assertion
  Assertions.assertThat(actualHeader)
              .extracting(Header::getCredentials, Header::getLength, Header::getDate, Header::isEncryptedMessage)
              .containsExactly(Credentials.anonymous(), 9, LocalDate.now(), false);

}

Но как насчет тестов, которые мы пишем ранее для public методов класса, которые используют createHeader()?
Не так много различий.
На самом деле, мы все еще раздражаемся, так как эти public методы все еще должны быть проверены относительно возвращенного значения заголовка.
Если мы удалим эти утверждения, мы не сможем обнаружить регрессии по этому поводу.
Мы должныв состоянии естественным образом изолировать эту обработку, но мы не можем, так как метод createHeader() относится к тестируемому компоненту.
Вот почему я объяснил в начале своего ответа, что в большинстве случаев мы должны отдавать предпочтение извлечению privateметод в другом классе для изменения модификатора доступа на public.

Итак, мы вводим HeaderService:

public class HeaderService {

    public Header createHeader(Credentials credentials, String message, boolean isEncrypted) {
        return new Header(credentials, message.length(), LocalDate.now(), isEncrypted);
    }

}

И мы переносим тесты createHeader() вHeaderServiceTest.

Теперь MessageService определяется с зависимостью HeaderService:

public class MessageService {

    private HeaderService headerService;

    public MessageService(HeaderService headerService) {
        this.headerService = headerService;
    }

    public Message createMessage(String message, Credentials credentials) {
        Header header = headerService.createHeader(credentials, message, false);
        return new Message(header, message);
    }

    public Message createEncryptedMessage(String message, Credentials credentials) {
        Header header = headerService.createHeader(credentials, message, true);
        // specific processing to encrypt
        // ......
        return new Message(header, message);
    }

    public Message createAnonymousMessage(String message) {
        Header header = headerService.createHeader(Credentials.anonymous(), message, false);
        return new Message(header, message);
    }

}

А в тестах MessageService нам больше не нужноУтвердите каждое значение заголовка, так как оно уже проверено.
Мы хотим просто убедиться, что Message.getHeader() возвращает то, что вернул HeaderService.createHeader().

Например, вот новая версия createMessage() тест:

@Test
public void createMessage() throws Exception {
    final String inputMessage = "simple message";
    final Credentials inputCredentials = new Credentials("user", "pass");
    final Header fakeHeaderForMock = createFakeHeader();
    Mockito.when(headerService.createHeader(inputCredentials, inputMessage, false))
           .thenReturn(fakeHeaderForMock);
    // action
    Message actualMessage = messageService.createMessage(inputMessage, inputCredentials);
    // assertion
    Assert.assertEquals(inputMessage, actualMessage.getMessage());
    Assert.assertSame(fakeHeaderForMock, actualMessage.getHeader());
}

Обратите внимание на assertSame()используйте для сравнения ссылок на объекты для заголовков, а не их содержимого.
Теперь HeaderService.createHeader() может изменить свое поведение и вернуть разные значения, это не имеет значения с точки зрения теста MessageService.

0 голосов
/ 16 августа 2011

Лично у меня такие же проблемы при тестировании частных методов, потому что некоторые инструменты тестирования ограничены.Нехорошо, когда ваш дизайн управляется ограниченными инструментами, если они не отвечают на ваши потребности, меняйте инструмент, а не дизайн.Поскольку вы просите C #, я не могу предложить хорошие инструменты тестирования, но для Java есть два мощных инструмента: TestNG и PowerMock, и вы можете найти соответствующие инструменты тестирования для платформы .NET

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...