Есть класс, у которого есть один основной метод:
1) берет на входе некий объект для доступа к разнообразной информации, стрим например
2) совершает над ним операции по вытаскиванию информации
3) обрабатывает информацию
4) выдает результат обработки.
Тестировать в лоб основной метод очень сложно — миллион вариантов начальных данных и набор финальных результатов,
Допустим действия 2,3 и 4 вынесены в отдельные промежуточные методы вспомогательных подклассов, т.о. их по отдельности можно оттестировать и заменить моками.
Тогда основной метод можно переделать:
* он создает все вспомогательные классы и вызывает вызывает другой метод, в который передает эти экземпляры.
* этот другой метод уже будет в правильном порядке дергать методы подклассов.
Подклассы тестируются отдельно, метод в который подклассы передаются — тестируется с помощью моков этих подклассов, все замечательно.
А теперь вопрос — как протестировать самый главный метод, который создает подклассы?
Вижу два варианта:
1) в конструктор всего класса передавать фабрики подклассов
2) фабрики создавать в виде моков, которые будут также выдавать моки подклассов.
3) проверять что главный метод вызвал методы фабрик
4) кодировать моки подклассов так, чтобы на них получался эталонный результат, а потом с ним сравнивать.
Но тут в пункте 4) огромный недостаток: он неявно тестирует практически весь класс целиком — это опять по сути исходная проблема.
Второй вариант — извращенский:
1)-3) такие же
4) Создать класс-наследник тестируемого класса
5) Оверрайдить второй метод (который изнутри вызывается тестируемым главным методом)
6) вызвать главный метод и убедиться, что он создал правильные подклассы и передал в наш оверрайденый метод.
В этом случае все методы отделены друг от друга и не надо строить всю цепочку от начальных тестовых данных до правильного финального результата.
Но это все-таки извращение какое-то, да и не всегда так получится.
Что можно в консерватории подправить?
Re: Тест метода класса Java, вызывающего другие методы
Здравствуйте, djandy_spb, Вы писали:
_>Подклассы тестируются отдельно, метод в который подклассы передаются — тестируется с помощью моков этих подклассов, все замечательно.
_>А теперь вопрос — как протестировать самый главный метод, который создает подклассы?
_>Но это все-таки извращение какое-то, да и не всегда так получится. _>Что можно в консерватории подправить?
Покажи пример лучше нормальный. Все зависит от того кода, что в этом основном методе. Нужно определиться с конечным дизайном.
Дальнейшие действия по созданию тестопригодного кода и рефакторинг вобщем то сложности не представляют.
Я бы не менял код гигантского класса без заготовленых заранее тестов. Поначалу нужно худо-бедно обрезать лишние зависимости и написать тесты которые будут детектить конкретные ситуации, которые ты хочешь модифицировать.
Re[2]: Тест метода класса Java, вызывающего другие методы
Здравствуйте, djandy_spb, Вы писали:
_>Реальный код выложить не могу, но например сначала класс вот такой:
_>
_>public class Statistics {
_>
_>не хочется его тестить типа генерируя разные стримы а потом сравнивая результат с заготовленными результатами.
Вообще то нужно полностью покрыть входы и выходы.
В твоем случае нужно разорвать зависимости от StreamReader и ResultFormatter
_>В этом примере отдельно можно оттестить StreamReader, ResultFormatter, можно логику обработки этих data1-data3 вынести в отдельный метод: _>
_>}
_>
_>Теперь все замечательно тестируется кроме метода do — как убедиться что он правильно вызвал doInternal?
Никак, он не учавствует в юниттестировании, только в интеграционном Но я бы не вводил новый класс без нужды.
см. ниже как бы я орагнизовал этот класс для тестирования. Пример у тебя не самый удачный, т.к. не ясно, зачем тестировать StreamReader, а логику ты явно накидал прямо в браузере.
Итого, код с предположением, что StreamReader это какой то твой класс а не системный, что нужно изолироваться от ResultFormatter и тд
Очевидно, если StreamReader это системный, а ResultFormatter уже изолирован, то сгодится и твой первоначальный код
Как создавать такой класс для тестирования ? Очевидно — делать мок для Statistics, например Moq и всех его депенденсов или же наколбасить мок вручную, как показано ниже
// класс для тестирования. Создается или руками или через Moqpublic class StatisticsMoq : Statistics {
public Func<Stream, StreamReader> FuncReaderFrom {get; set;}
public Func<ResultFormatter> FuncFormatter {get; set;}
public Func<ResultFormatter, Func<Data,bool, string> > FuncGetFormat {get; set;}
protected override StreamReader ReaderFrom(Stream stream)
{
return FuncReaderFrom();
}
protected override ResultFormatter Formatter()
{
return FuncFormatter();
}
protected override Func<Data,bool, string> getFormat(ResultFormatter formatter)
{
return FuncGetFormat(formatter);
}
}
public class Statistics {
public String do(Stream stream) {
// логика полностью тестируема со всеми потрохами
reader = ReaderFrom(stream);
formatter = Formatter();
var dataSequence = FetchFrom(reader);
return FormatSequence(dataSequence, getFormat(formatter) );
}
protected virtual Func<Data,bool, string> getFormat(ResultFormatter formatter)
{
// при тестировании можно(если нужно) что бы распознавать бехевиор FormatSequence return (data, valid, stream) => formatter.format(valid.ToValidEnum();
}
protected virtual StreamReader ReaderFrom(Stream stream)
{
// этот метод можно(если нужно) переопределить для тестирования и там возвращать мок-ридер, что бы изолировать тесты от стрима return new StreamReader(stream);
}
protected virtual ResultFormatter Formatter()
{
// этот метод можно(если нужно) переопределить для тестирования и там возвращать мок-форматтер,
// например если формат слишком сложный что бы колбасить его в тестахreturn new ResultFormatter();
}
public static IEnumerable<Data> FetchFrom(StreamReader reader)
{ // идея в том, что извлечение из стрима отделяется от остальной логики. метод тестируемый - зависимость только от ридера yield return reader.ReadData();
yield return reader.ReadData();
yield return reader.ReadData();
}
public static FormatSequence(IEnumerable<Data> seq, Func<Data,bool,string> format)
{
// основная логика полностью тестируема - депенденсы только от Datavar data1 = seq.ElementAt(1);
var data2 = seq.ElementAt(2);
var data3 = seq.ElementAt(3);
if (data1.valid())
return format(data1, true);
if (data2.valid())
return format(data1, false) + format(data2,true);
if (data3.valid())
return format(data1, false) + format(data2, false) + format(data3,true);
что вернуть если valid ни разу не встретилось ?
}
}
public enum ValidEnum
{
VALID,
INVALID
}
public static Helper
{
public static ValidEnum ToValidEnum(this bool isValid)
{
return isValid ? VALID : INVALID;
}
}
Re[4]: Тест метода класса Java, вызывающего другие методы
public interface StreamReader
{
Data readData(Stream stream);
}
public interface ResultStringBuilder
{
String getResult(Data data1, Data data2, Data data3);
}
public interface ResultFormatter
{
public String format(int i, Data data);
}
public class DefaultResultStringBuilder implements ResultStringBuilder
{
private ResultFormatter _formatter;
public DefaultResultStringBuilder(ResultFormatter formatter)
{
_formatter = formatter;
}
public String getResult(Data data1, Data data2, Data data3)
{
if (data1.valid())
{
return _formatter.format(1, data1);
}
else if (data2.valid())
{
return _formatter.format(2, data1) + _formatter.format(1, data2);
}
else if (data3.valid())
{
return _formatter.format(3, data1) + _formatter.format(2, data2) + _formatter.format(2, data3);
}
return null;
}
}
public class Statistics
{
private StreamReader _reader;
private ResultStringBuilder _resultStringBuilder;
public Statistics(StreamReader reader, ResultStringBuilder resultStringBuilder)
{
_reader = reader;
_resultStringBuilder = resultStringBuilder;
}
public String doIt(Stream stream)
{
Data data1 = _reader.readData(stream);
Data data2 = _reader.readData(stream);
Data data3 = _reader.readData(stream);
return _resultStringBuilder.getResult(data1, data2, data3);
}
}
Тестируем Statistics:
// create mocks and setup expectations
Statistics statistics = new Statistics(readerMock, resultBuilderMock);
statistics.doIt(testStream);
// verify here that the mocks have been called with proper parameters
Тестируем билдер:
// create mock and setup expectations
DefaultResultStringBuilder builder = new DefaultResultStringBuilder(formatterMock);
String result = builder.getResult(data1, data2, data3);
// test the result value and verify that thу mock has been called properly
Прочитал тут всё что понаписали и вспомнил нашу компанию и внедрение юнит-тестов.
Примерно так эволюция и происходила
— тестируем в лоб
— понимаем, что долго, надо по частям, при помощи моков
— в дальнейшем, постепенно, при правках и при рефакторинге понимаем, что все тесты из предыдущего подхода — шлак
— возвращаемся к первому варианту
После 5 лет писания юнит-тестов пришло понимание, что важно не переступить грань между "тестируем контракт публичного метода" и "тестируем фактическую имплементацию".
Тесты полученные вторым способом на самом деле становятся бесполезны сразу после написания, хотя в момент написания они архиполезны: позволяют критически взглянуть на код, правильно подойти к проектированию, быстро проверить все ветки алгоритма.
Использование моков гвоздями прибивает тесты к внутренней имплементации и простейшие изменения в этой имплементации (не затрагивающие контракт!) требуют переписывания тестов.
Стремление съэкономить пойдя по 2му пути на самом деле потом обернётся постоянными мелкими правками и невозможностью опираться на эти тесты во время рефакторинга.
Вообщем, коллеги, тестируйте контракт, не имплементацию, и будет вам счастье!