Очень отвеченный вопрос.
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
.