Тест метода класса Java, вызывающего другие методы
От: djandy_spb  
Дата: 16.02.11 12:12
Оценка:
Есть класс, у которого есть один основной метод:
1) берет на входе некий объект для доступа к разнообразной информации, стрим например
2) совершает над ним операции по вытаскиванию информации
3) обрабатывает информацию
4) выдает результат обработки.

Тестировать в лоб основной метод очень сложно — миллион вариантов начальных данных и набор финальных результатов,

Допустим действия 2,3 и 4 вынесены в отдельные промежуточные методы вспомогательных подклассов, т.о. их по отдельности можно оттестировать и заменить моками.

Тогда основной метод можно переделать:
* он создает все вспомогательные классы и вызывает вызывает другой метод, в который передает эти экземпляры.
* этот другой метод уже будет в правильном порядке дергать методы подклассов.

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

А теперь вопрос — как протестировать самый главный метод, который создает подклассы?

Вижу два варианта:
1) в конструктор всего класса передавать фабрики подклассов
2) фабрики создавать в виде моков, которые будут также выдавать моки подклассов.
3) проверять что главный метод вызвал методы фабрик
4) кодировать моки подклассов так, чтобы на них получался эталонный результат, а потом с ним сравнивать.

Но тут в пункте 4) огромный недостаток: он неявно тестирует практически весь класс целиком — это опять по сути исходная проблема.

Второй вариант — извращенский:
1)-3) такие же
4) Создать класс-наследник тестируемого класса
5) Оверрайдить второй метод (который изнутри вызывается тестируемым главным методом)
6) вызвать главный метод и убедиться, что он создал правильные подклассы и передал в наш оверрайденый метод.

В этом случае все методы отделены друг от друга и не надо строить всю цепочку от начальных тестовых данных до правильного финального результата.

Но это все-таки извращение какое-то, да и не всегда так получится.
Что можно в консерватории подправить?
Re: Тест метода класса Java, вызывающего другие методы
От: Ikemefula Беларусь http://blogs.rsdn.org/ikemefula
Дата: 16.02.11 14:09
Оценка:
Здравствуйте, djandy_spb, Вы писали:

_>Подклассы тестируются отдельно, метод в который подклассы передаются — тестируется с помощью моков этих подклассов, все замечательно.


_>А теперь вопрос — как протестировать самый главный метод, который создает подклассы?


_>Но это все-таки извращение какое-то, да и не всегда так получится.

_>Что можно в консерватории подправить?

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

Дальнейшие действия по созданию тестопригодного кода и рефакторинг вобщем то сложности не представляют.

Я бы не менял код гигантского класса без заготовленых заранее тестов. Поначалу нужно худо-бедно обрезать лишние зависимости и написать тесты которые будут детектить конкретные ситуации, которые ты хочешь модифицировать.
Re[2]: Тест метода класса Java, вызывающего другие методы
От: djandy_spb  
Дата: 16.02.11 15:49
Оценка:
I>Покажи пример лучше нормальный. Все зависит от того кода, что в этом основном методе. Нужно определиться с конечным дизайном.

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

public class Statistics {
    public String do(Stream stream) {
        reader = new StreamReader();
        formatter = new ResultFormatter();

        Data data1 = reader.readData(stream);
        Data data2 = reader.readData(stream);
        Data data3 = reader.readData(stream);
        
        if (data1.valid()) {
            return formatter.format(VALID, data1);
        } else if (data2.valid()) {
            return formatter.format(INVALID, data1) + formatter.format(VALID, data2);
        } else if (data3.valid()) {
            return formatter.format(INVALID, data1) + formatter.format(INVALID, data2) + formatter.format(VALID, data3);
        }
    }
}


не хочется его тестить типа генерируя разные стримы а потом сравнивая результат с заготовленными результатами.

В этом примере отдельно можно оттестить StreamReader, ResultFormatter, можно логику обработки этих data1-data3 вынести в отдельный метод:

public class Statistics {
    public String do(Stream stream) {
        return doInternal(stream, new StreamReader(), new ResultFormatter(), new ResultStringBuilder());
    }

    public String doInternal(Stream stream, StreamReader reader, ResultFormatter formatter, ResultStringBuilder resultBuilder) {
        Data data1 = reader.readData(stream);
        Data data2 = reader.readData(stream);
        Data data3 = reader.readData(stream);
        
        return resultBuilder.getResult(data1, data2, data3);
    }
    
    public static class ResultStringBuilder {
        public String getResult(Data data1, Data data2, Data data3) {
            if (data1.valid()) {
                return formatter.format(VALID, data1);
            } else if (data2.valid()) {
                return formatter.format(INVALID, data1) + formatter.format(VALID, data2);
            } else if (data3.valid()) {
                return formatter.format(INVALID, data1) + formatter.format(INVALID, data2) + formatter.format(VALID, data3);
            }
        }
    }
}


Теперь все замечательно тестируется кроме метода do — как убедиться что он правильно вызвал doInternal?
Re[3]: Тест метода класса Java, вызывающего другие методы
От: Ikemefula Беларусь http://blogs.rsdn.org/ikemefula
Дата: 17.02.11 09:11
Оценка:
Здравствуйте, djandy_spb, Вы писали:

_>Реальный код выложить не могу, но например сначала класс вот такой:


_>
_>public class Statistics {
_>


_>не хочется его тестить типа генерируя разные стримы а потом сравнивая результат с заготовленными результатами.


Вообще то нужно полностью покрыть входы и выходы.

В твоем случае нужно разорвать зависимости от StreamReader и ResultFormatter

_>В этом примере отдельно можно оттестить StreamReader, ResultFormatter, можно логику обработки этих data1-data3 вынести в отдельный метод:

_>
_>}
_>


_>Теперь все замечательно тестируется кроме метода do — как убедиться что он правильно вызвал doInternal?


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

см. ниже как бы я орагнизовал этот класс для тестирования. Пример у тебя не самый удачный, т.к. не ясно, зачем тестировать StreamReader, а логику ты явно накидал прямо в браузере.
Итого, код с предположением, что StreamReader это какой то твой класс а не системный, что нужно изолироваться от ResultFormatter и тд
Очевидно, если StreamReader это системный, а ResultFormatter уже изолирован, то сгодится и твой первоначальный код

Как создавать такой класс для тестирования ? Очевидно — делать мок для Statistics, например Moq и всех его депенденсов или же наколбасить мок вручную, как показано ниже


// класс для тестирования. Создается или руками или через Moq
public 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) 
    {
        // основная логика полностью тестируема - депенденсы только от Data
        
    var 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, вызывающего другие методы
От: Ikemefula Беларусь http://blogs.rsdn.org/ikemefula
Дата: 18.02.11 12:23
Оценка:
Как то поздно заметил, что речь про java
Re[3]: Тест метода класса Java, вызывающего другие методы
От: -VaS- Россия vaskir.blogspot.com
Дата: 19.03.11 20:55
Оценка:
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


Как создавать моки см. здесь
Re: Тест метода класса Java, вызывающего другие методы
От: avpavlov  
Дата: 10.04.11 09:48
Оценка: -1
Прочитал тут всё что понаписали и вспомнил нашу компанию и внедрение юнит-тестов.

Примерно так эволюция и происходила

— тестируем в лоб
— понимаем, что долго, надо по частям, при помощи моков
— в дальнейшем, постепенно, при правках и при рефакторинге понимаем, что все тесты из предыдущего подхода — шлак
— возвращаемся к первому варианту

После 5 лет писания юнит-тестов пришло понимание, что важно не переступить грань между "тестируем контракт публичного метода" и "тестируем фактическую имплементацию".

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

Использование моков гвоздями прибивает тесты к внутренней имплементации и простейшие изменения в этой имплементации (не затрагивающие контракт!) требуют переписывания тестов.

Стремление съэкономить пойдя по 2му пути на самом деле потом обернётся постоянными мелкими правками и невозможностью опираться на эти тесты во время рефакторинга.

Вообщем, коллеги, тестируйте контракт, не имплементацию, и будет вам счастье!
 
Подождите ...
Wait...
Пока на собственное сообщение не было ответов, его можно удалить.