Модульное тестирование класса без возвращаемого значения? - PullRequest
23 голосов
/ 22 октября 2009

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

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

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

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

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

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

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

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

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

Ответы [ 7 ]

17 голосов
/ 22 октября 2009

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

Чтобы выполнить модульное тестирование класса, поведение которого ограничено взаимодействием с соавторами, вы обычно передаете фиктивные объекты соавтора, которые инструментированы таким образом, что позволяет вам проверить, что их методы были вызваны так, как вы ожидаете.

Если бы вы делали это вручную (чёрт!) Для классов, которые вы упомянули в вашем вопросе, вы могли бы создать класс 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();
5 голосов
/ 22 октября 2009

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

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

3 голосов
/ 22 октября 2009

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

Почему бы не создать некоторые известные значения для возврата из фиктивного IConnection, просто пропустите все их через фиктивный IParser и сохраните их в фиктивном IDataAccess - затем в тестовой проверке, чтобы увидеть, что результаты в фиктивном 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);
    }
}

Макеты / Тестовые классы:

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)));
    }
}
2 голосов
/ 22 октября 2009

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

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

1 голос
/ 22 октября 2009

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

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

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

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

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

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

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

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

Для успешного выполнения run () необходимо проверить следующее:

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

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

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


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

1 голос
/ 22 октября 2009

Я задал похожий вопрос .

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

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

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

1 голос
/ 22 октября 2009

Если единственное, что делает ваш метод run(), это вызывает другие объекты, то вы проверяете его, но проверяете, что были запущены макеты. Как именно вы это сделаете, зависит от макета пакета, но обычно вы найдете какой-то метод «ожидаем».

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

...