Учитывая интерфейс, например:
public interface IFoo
{
string Bar();
}
И класс, который реализует это как:
public class Foo : IFoo
{
public string Bar()
{
returns "Bar";
}
public string SomeOtherMethod()
{
returns "Something else";
}
}
Есть ли проблема с этим кодом? Должны ли мы добавить всех членов в интерфейс?
Один пример: представьте частный метод, который становится достаточно сложным, чтобы его можно было тестировать. Не могли бы вы сделать его общедоступным (чтобы его можно было вызывать из тестового проекта), но не добавлять его в интерфейс (потому что клиентам не нужно его вызывать)?
Could you make it public (so it can be called from the test project) but not add it to the interface (because no clients need to call it)? -- да.Реализовано множество классов IDisposable. Если бы единственным общедоступным методом, который у них был, был Dispose, они не были бы очень полезными.
Обычно я создаю частные методы, которые хочу протестировать, внутренние, а не общедоступные.
Рука об руку с тем, что говорит Роберт, это I в SOLID для разделения интерфейсов: codeburst.io/…
Методы тестирования, которые обычная программа никогда не сможет вызвать, — довольно скользкий путь. Это не имеет ничего общего с тестированием, все, что связано с диагностикой «где ошибка». Для этого вы используете отладчик, он не возражает против установки точки останова на частном или внутреннем методе. Модульный тест — отличный способ активировать эту точку останова, убедитесь, что вам удобно использовать отладчик в модульном тесте.
Модульные тесты не должны быть нацелены на частные методы, поскольку частные методы являются деталями реализации и, следовательно, должны быть свободны для рефакторинга даже без каких-либо общедоступных изменений в вашем классе. Вы должны тестировать закрытые члены только через публичные члены, которые их используют.





Обычно, когда класс реализует интерфейс, все члены интерфейса должны быть реализованы, а не наоборот.
Также при реализации члена интерфейса соответствующий член реализующего класса должен быть общедоступным, нестатическим и иметь то же имя и сигнатуру параметра, что и член интерфейса. У вас могут быть методы, даже общедоступные, в классе, которые не определены в интерфейсе.
Кроме того, сам интерфейс не предоставляет функциональности, которую класс или структура могут наследовать так же, как они могут наследовать функциональность базового класса. Однако если базовый класс реализует интерфейс, любой класс, производный от базового класса, наследует эту реализацию.
Если то, что делает класс, достаточно просто, чтобы мы могли одновременно тестировать его открытые и частные методы, то мы можем просто протестировать общедоступные методы.
Если приватный метод станет настолько сложным, что между публичным и приватным методом нам потребуется слишком много комбинаций тестов, то пришло время выделить приватный метод в отдельный класс. Если сделать закрытый метод общедоступным, это нарушит инкапсуляцию класса. Даже если мы не добавим метод в interface, если сделать метод класса общедоступным, метод все равно будет добавлен в открытый интерфейс самого класса.
Итак, если у нас есть это:
public class ClassWithComplexPrivateMethod
{
public void DoSomething(int value)
{
PrivateMethodThatNeedsItsOwnTests(value);
}
private string PrivateMethodThatNeedsItsOwnTests(int value)
{
// This method has gotten really complicated!
return value.ToString();
}
}
Мы могли бы провести рефакторинг примерно так:
public interface IRepresentsWhatThePrivateMethodDid
{
string MethodThatNeedsItsOwnTests(int value);
}
public class RefactoredClass
{
private readonly IRepresentsWhatThePrivateMethodDid _dependency;
public RefactoredClass(IRepresentsWhatThePrivateMethodDid dependency)
{
_dependency = dependency;
}
public string DoSomething(int value)
{
return _dependency.MethodThatNeedsItsOwnTests(value);
}
}
И теперь новый класс реализует IRepresentsWhatThePrivateMethodDid.
Теперь, когда мы тестируем рефакторинговый класс, мы имитируем IRepresentsWhatThePrivateMethodDid и пишем отдельные модульные тесты для всех классов, реализующих IRepresentsWhatThePrivateMethodDid.
Может показаться противоречием утверждение, что раскрытие закрытого метода как общедоступного нарушает инкапсуляцию, а раскрытие его как отдельного класса — нет. Есть два отличия:
Также легко увлечься этим и ввести отдельную зависимость слишком рано, когда мы могли бы протестировать класс, включая его частные методы, через публичные методы. Я делал это много раз, и это может привести к большому количеству ненужных интерфейсов и дополнительных классов. Нет совершенства, только наши усилия, чтобы сбалансировать его.
Еще одна мысль: мы склонны использовать интерфейсы для представления зависимостей, но это не обязательно. Если то, что мы извлекаем, было всего лишь одним закрытым методом, то, возможно, мы могли бы представить его с помощью делегата или Func, например:
public class RefactoredClass
{
private readonly Func<int, string> _dependency;
public RefactoredClass(Func<int, string> dependency)
{
_dependency = dependency;
}
public string DoSomething(int value)
{
return _dependency(value);
}
}
Или мы можем использовать делегат, который мне нравится больше, чем Func, потому что он указывает, что делает функция.
Интерфейс должен представлять только требуемый API без учета юнит-тестирования и чего-то еще.
Для тестирования публичный-методов класса необходимо выполнить ничего, т.к. Test-проект имеет к ним доступ. Вам просто нужно создать экземпляр класса и внедрить все необходимые зависимости с помощью Mock.
Пример: класс тестирования без каких-либо зависимостей
Пример: тестовый класс с зависимостью
Для тестирования приватных методов класса необходимо сделать их видимыми для тестового проекта: