Модульное тестирование: автономные тесты против дублирования кода (сухой)

Я делаю свои первые шаги с модульным тестированием и не уверен в двух парадигмах, которые, похоже, противоречат сами себе на модульных тестах, а именно:

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

чтобы быть более конкретным, у меня есть импортер, который я хочу проверить. Импортер имеет функцию "импорт", принимая необработанные данные (например, из CSV) и возвращая объект определенного типа который также будет храниться в базе данных через ORM (LinqToSQL в этом случае).

теперь я хочу проверить несколько вещей, например, что возвращаемый объект не является нулевым, что обязательные поля не являются нулевыми или пустыми и что атрибуты имеют правильные значения. Я написал 3 модульных тестов для этого. Должен ли каждый тест импортировать и получить задание или это относится к общей логике настройки? С другой стороны,--13-->полагая, что это сообщение в блоге, последнее было бы плохой идеей, так как насколько я понимаю. Кроме того, не нарушит ли это самоограничение?

мой класс выглядит так:

[TestFixture]
public class ImportJob
{
    private TransactionScope scope;
    private CsvImporter csvImporter;

    private readonly string[] row = { "" };

    public ImportJob()
    {
        CsvReader reader = new CsvReader(new StreamReader(
                    @"C:SomePathunit_test.csv", Encoding.Default),
                    false, ';');
        reader.MissingFieldAction = MissingFieldAction.ReplaceByEmpty;

        int fieldCount = reader.FieldCount;
        row = new string[fieldCount];

        reader.ReadNextRecord();
        reader.CopyCurrentRecordTo(row);
    }

    [SetUp]
    public void SetUp()
    {
        scope = new TransactionScope();
        csvImporter = new CsvImporter();
    }

    [TearDown]
    public void TearDown()
    {
        scope.Dispose();
    }

    [Test]
    public void ImportJob_IsNotNull()
    {
        Job j = csvImporter.ImportJob(row);

        Assert.IsNotNull(j);
    }

    [Test]
    public void ImportJob_MandatoryFields_AreNotNull()
    {
        Job j = csvImporter.ImportJob(row);

        Assert.IsNotNull(j.Customer);
        Assert.IsNotNull(j.DateCreated);
        Assert.IsNotNull(j.OrderNo);
    }

    [Test]
    public void ImportJob_MandatoryFields_AreValid()
    {
        Job j = csvImporter.ImportJob(row);
        Customer c = csvImporter.GetCustomer("01-01234567");

        Assert.AreEqual(j.Customer, c);
        Assert.That(j.DateCreated.Date == DateTime.Now.Date);
        Assert.That(j.OrderNo == row[(int)Csv.RechNmrPruef]);

    }

    // etc. ...
}

Как видно, я делаю строку Job j = csvImporter.ImportJob(row); в каждом модульном тесте, так как они должны быть автономными. Но это нарушает сухой принцип и может привести к проблемам с производительностью в один прекрасный день.

какова наилучшая практика в этом случае?

5 ответов


Это зависит от того, насколько ваш сценарий является общим для вашего теста. В сообщении в блоге вы ссылались на основную жалобу, что метод установки сделал другую настройку для трех тестов, и это не может считаться лучшей практикой. В вашем случае у вас есть одна и та же настройка для каждого теста/сценария, а затем вы должны использовать общую настройку вместо дублирования кода в каждом тесте. Если позже вы обнаружите, что есть больше тестов, которые не используют эту настройку или требуют другой настройки общий доступ между набором тестов, а затем рефакторинг этих тестов в новый класс тестового случая. У вас также могут быть общие методы установки, которые не отмечены [SetUp], но вызываются в начале каждого теста, который нуждается в них:

[Test]
public void SomeTest()
{
   setupSomeSharedState();
   ...
}

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


ваши тестовые классы ничем не отличаются от обычных классов и должны рассматриваться как таковые: все хорошие практики (DRY, повторное использование кода и т. д.) должен применяться и там.


вы могли бы поставить Задание j = csvImporter.ImportJob (строка); в настройках. Таким образом, вы не будете повторять код.

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

проблема производительности не вызвана сухими нарушениями. Вы на самом деле должны настроить все для каждого теста. Это не модульные тесты, это интеграция тесты, вы полагаетесь на внешние файлы для запуска теста. Вы можете сделать ImportJob считанным из потока, а не напрямую открывать файл. Затем вы можете протестировать memorystream.


Если вы двигаетесь

Job j = csvImporter.ImportJob(row);

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

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


в одном из моих проектов мы договорились с командой, что мы не будем реализовывать логику инициализации в конструкторах модульных тестов. У нас есть атрибуты Setup, TestFixtureSetup, SetupFixture (начиная с версии 2.4 NUnit). Их достаточно почти для всех случаев, когда нам нужна инициализация. Мы заставляем разработчиков использовать один из этих атрибутов и явно определить, будем ли мы запускать этот код инициализации перед каждым тестом, перед всеми тестами в креплении или перед всеми тестами в пространство имен.

однако я не согласен с тем, что модульные тесты всегда должны подтверждать все хорошие практики, предполагаемые для обычной разработки. Это желательно, но не является правилом. Моя точка зрения, что в реальной жизни клиент не оплачивает модульные тесты. Заказчик оплачивает общее качество и функциональность продукта. Ему не интересно знать, предоставляете ли вы ему продукт без ошибок, покрывая 100% кода модульными тестами / автоматизированными тестами GUI или используя 3 руководства тестеры на одного разработчика, который будет нажимать на каждую часть экрана после каждой сборки. Модульные тесты не добавляют продукту бизнес-ценности, они позволяют экономить на разработке и тестировании и заставляют разработчиков писать лучший код. Так что это всегда зависит от вас - будете ли вы тратить дополнительное время на рефакторинг UT, чтобы сделать модульные тесты идеальными? Или вы потратите столько же времени, чтобы добавить новые функции для клиентов вашего продукта? Не забывайте также, что юнит-тесты должны быть как как можно проще. Как найти Золотое сечение?

Я полагаю, что это зависит от проекта, и PM или team lead должны планировать и оценивать качество модульных тестов, их полноту и охват кода, как если бы они оценивали все другие бизнес-функции вашего продукта. Мое мнение, что лучше иметь модульные тесты copy-paste, которые охватывают 80% производственного кода, а затем иметь очень хорошо разработанные и разделенные модульные тесты, которые охватывают только 20%.