Подтвердить что ты не робот

Единичное тестирование класса без возвращаемого значения?

Я не нашел многого в учебниках по этому конкретному вопросу.

Итак, у меня есть класс под названием "Job", в котором есть общедоступные ctors и одна общедоступная функция Run(). Все в классе является частным и инкапсулируется в классе. (Вы можете вспомнить более старое сообщение здесь, на этом Тестирование только общедоступного метода в классе среднего класса?, ответы на который мне очень помогли)

Этот метод Run() выполняет кучу вещей - принимает файл excel в качестве входных данных, извлекает данные из него, отправляет запрос стороннему поставщику данных, берет результат и помещает его в базу данных и записывает начало/конец задания.

Этот класс работы использует 3 отдельных интерфейса/классы внутри его метода запуска (IConnection будет подключаться к стороннему поставщику и отправлять запрос, IParser будет анализировать результаты, а IDataAccess сохранит результаты в базе данных). Итак, единственная реальная логика в моем методе Run() - это извлечение входного файла excel и отправка его по цепочке других классов. Я создал 3 макетных класса и использовал DI в классе Job ctor, и все в порядке и dandy...

Кроме того, я все еще немного потерял, как чертовски проверить мой метод Run() - потому что он недействителен и ничего не возвращает...

В этом случае следует добавить возвращаемое значение в метод Run(), который возвращает количество записей, извлеченных из файла Excel? Так как это единственная логика, выполняемая в этой функции сейчас.. это не будет обрабатываться в реальном коде, но будет в модульных тестах... что кажется мне немного вонючим, но я новичок до тех пор, пока true TDD...

Второй вопрос - должен ли я создать четвертый класс, называемый IExcelExtractor, который делает эту логику для меня? Или это немного взрыв класса?

Даже если бы я сделал последнее, как бы я протестировал свою функцию Run(), если она вернет void, и вся ее работа выполняется издевательскими объектами, которые на самом деле ничего не делают? Я мог понять, имела ли моя функция значимое возвращаемое значение... но в этом случае я смущен.

Большое спасибо за прочтение всего этого, если вы сделали это так далеко.

4b9b3361

Ответ 1

То, что вы описываете часто называется верификацией поведения (в отличие от проверки состояния). Он получил своих сторонников и хулителей, но для нескольких категорий классов это единственная игра в городе, если вы хотите unit test.

В unit test класс, поведение которого ограничено взаимодействием с коллабораторами, вы, как правило, передаете макетные объекты-коллабораторы, которые инструментаризованы таким образом, чтобы вы могли проверить, что их методы были вызваны так, как вы ожидаете.

Если вы должны сделать это вручную (yuck!) для классов, упомянутых в вашем вопросе, вы можете создать класс MockParser, который реализует IParser, и добавляет свойства, которые записывают, когда и как были вызваны его методы.

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

В наши дни я использовал NMock2, и тесты выглядят примерно так:

// 'mockery' is the central framework object and Mock object factory
IParser mockParser   = mockery.NewMock<IParser>();

// Other dependencies omitted
Job     job          = new Job(mockParser);

// This just ensures this method is called so the return value doesn't matter
Expect.Once.On(mockParser).
    .Method("Parse").
    .WithAnyArguments().
    .Will(Return.Value(new object()));

job.Run();
mockery.VerifyAllExpectationsHaveBeenMet();

Ответ 2

Когда вы вводите макет, вы передаете конструктору класса Run тестовый класс, который вы спросите, прошел ли тест. Например, вы можете проверить, что макет IParser получил правильный запрос, переданный в файл excel, который вы передали в конструкторе. Вы можете сделать это через свой собственный класс, собрать в него результаты и проверить, что он собрал, или сделать это с помощью насмешливой структуры, которая дает вам способы выражения такого тестирования без создания класса.

Я вижу, что вы пометили свой вопрос с помощью tdd, но в истинном tdd вы действительно не задаете этот вопрос (вы делаете, но задаетесь по-другому), потому что сначала вы создаете тест, который определяет интерфейс, а не строит класс интерфейс, а затем подумайте, как вы собираетесь тестировать эту вещь. Необходимость протестировать диски дизайна. Вы по-прежнему используете одни и те же методы (и, вероятно, в конечном итоге, с тем же дизайном), но вопрос возник бы немного иначе.

Ответ 3

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

Почему бы не создать некоторые известные значения для возврата из вашего мошеннического IConnection, просто передайте все их через ваш mock IParser и сохраните их в вашем mock IDataAccess, а затем в тестовой проверке, чтобы увидеть, что результаты в mock IDataAccess соответствуют ожидаемые результаты ввода из ложного IConnection после запуска метода run()?

Отредактировано, чтобы добавить пример -

Интерфейсы/классы приложений:

public interface IConnection {
    public List<Foo> findFoos();
}

public interface IParser {
    public List<Foo> parse(List<Foo> originalFoos);
}

public interface IDataAccess {
    public void save(List<Foo> toSave);
}

public class Job implements Runnable {
    private IConnection connection;
    private IParser parser;
    private IDataAccess dataAccess;

    public Job(IConnection connection, IParser parser, IDataAccess dataAccess) {
        this.connection = connection;
        this.parser = parser;
        this.dataAccess = dataAccess;
    }

    public void run() {
        List<Foo> allFoos = connection.findFoos();
        List<Foo> someFoos = parser.parse(allFoos);
        dataAccess.save(someFoos);
    }
}

Mocks/Test classes:

public class MockConnection implements IConnection {
    private List<Foo> foos;

    public List<Foo> findFoos() {
        return foos;
    }

    public void setFoos(List<Foo> foos) {
        this.foos = foos;
    }
}

public class MockParser implements IParser {

    private int[] keepIndexes = new int[0];

    public List<Foo> parse(List<Foo> originalFoos) {
        List<Foo> parsedFoos = new ArrayList<Foo>();
        for (int i = 0; i < originalFoos.size(); i++) {
            for (int j = 0; j < keepIndexes.length; j++) {
                if (i == keepIndexes[j]) {
                    parsedFoos.add(originalFoos.get(i));
                }
            }
        }
        return parsedFoos;
    }

    public void setKeepIndexes(int[] keepIndexes) {
        this.keepIndexes = keepIndexes;
    }
}

public class MockDataAccess implements IDataAccess {
    private List<Foo> saved;

    public void save(List<Foo> toSave) {
        saved = toSave;
    }

    public List<Foo> getSaved() {
        return saved;
    }
}

public class JobTestCase extends TestCase {

    public void testJob() {
        List<Foo> foos = new ArrayList<Foo>();
        foos.add(new Foo(0));
        foos.add(new Foo(1));
        foos.add(new Foo(2));
        MockConnection connection = new MockConnection();
        connection.setFoos(foos);
        int[] keepIndexes = new int[] {1, 2};
        MockParser parser = new MockParser();
        parser.setKeepIndexes(keepIndexes);
        MockDataAccess dataAccess = new MockDataAccess();
        Job job = new Job(connection, parser, dataAccess);
        job.run();
        List<Foo> savedFoos = dataAccess.getSaved();
        assertTrue(savedFoos.length == 2);
        assertTrue(savedFoos.contains(foos.get(1)));
        assertTrue(savedFoos.contains(foos.get(2)));
        assertFalse(savedFoos.contains(foos.get(0)));
    }
}

Ответ 4

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

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

Ответ 5

Если единственное, что делает ваш метод run(), - это вызвать другие объекты, тогда вы проверяете его, но проверяете, что были вызваны mocks. Точно, как вы это делаете, это зависит от пакета mock, но обычно вы найдете какой-то метод "ожидать".

Не записывайте код в свой метод run(), который будет отслеживать его выполнение. Если вы не можете проверить операцию метода на основе своих взаимодействий с сотрудниками (mocks), это указывает на необходимость переосмысления этих взаимодействий. Это также мешает основному коду, увеличивая затраты на обслуживание.

Ответ 6

Я задал аналогичный вопрос.

Хотя (смысл теории), я думаю, что некоторым методам не нужны модульные тесты, пока (и пока) они:

  • не возвращать значения
  • не изменять внутреннее состояние класса или системы, которые можно проверить.
  • не полагайтесь ни на что другое (как на ввод или вывод), чем на ваш макет

Если их функциональность (то есть последовательность вызовов) жизненно важна, вам нужно будет убедиться, что внутренняя функциональность выполнена. Это означает, что вы должны проверить (используя ваши mocks), что эти методы были вызваны с правильными параметрами и правильной последовательностью (если это имеет значение).

Ответ 7

Прежде всего, поскольку ваш метод run() является своего рода пусковой установкой рабочего процесса, и у вас есть несколько шагов, которые необходимо выполнить внутри рабочего процесса, я думаю, вам нужно больше одного unit test, возможно, даже для разделения существующий класс на несколько более мелких, каждый из которых соответствует шагу в рабочем процессе.

Таким образом, вы также будете изолировать каждый шаг рабочего процесса, если в любой момент рабочий процесс завершится неудачно, эти более мелкие модульные тесты позволят вам легче определить неисправную часть (шаг, который выходит из строя)

Но, возможно, это уже так, я не знаю, если у вас нет такого разделения.

В любом случае, вернемся к вашему методу run(), ответ лежит в вашем вопросе:

Этот метод Run() выполняет кучу вещей - принимает файл excel в качестве входных данных, извлекает данные из него, отправляет запрос стороннему поставщику данных, берет результат и помещает его в базу данных и записывает начало/завершение задания

Итак, у вас есть:

  • некоторые входные данные (из файла excel)

  • некоторые "выходные" данные или, скорее, результат wokflow.

Для успешного выполнения вашего запуска() вам необходимо проверить, что:

a) запрос был отправлен третьей стороне и/или результат был получен. Я не знаю, какой из них будет легче проверить, но, по крайней мере, вы могли бы зарегистрировать запрос/ответ и проверить журналы (в unit test) для выполняемой операции. Это обеспечит выполнение всего рабочего процесса (мы можем представить сценарий, в котором правильные данные присутствуют в db в конце рабочего процесса, но не потому, что работа работала правильно, а потому, что данные уже были там или что-то в этом направлении строки - если чистка перед тестом не удаляет некоторые данные, например)

b) проверьте базу данных на правильные значения (в отношении входных значений), которые вставлены/обновлены в соответствующих местах в результате рабочего процесса.

c) вы даже можете проверить журналы, о которых вы упоминаете (начало/конец задания), для достоверности задержки между двумя операциями (если вы знаете, что он не может работать быстрее, чем 10 секунд, если ваш log говорит, что работа выполнена за 1 секунду, вы узнаете, что что-то пошло не так...)


Изменить: в качестве первого теста перед а) выше вы можете также проверить входные данные, так как вы можете представить себе ошибки там (отсутствующий файл excel или содержимое изменилось, re с неправильным вводом и т.д.)