Как я могу использовать модульное тестирование, когда классы зависят друг от друга или от внешних данных? - PullRequest
5 голосов
/ 08 сентября 2010

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

Мой текущий проект - это приложение, которое собирает файлы в «Каталог». Затем Catalog может извлекать информацию из содержащихся в ней файлов, например миниатюры и другие свойства. Пользователи также могут пометить файлы другими метаданными, такими как «Автор» и «Заметки». Его легко сравнить с приложением для создания фотоальбомов, таким как Picasa или Adobe Lightroom.

Я разделил код для создания и манипулирования Catalog в отдельную DLL, которую я сейчас хотел бы протестировать. Тем не менее, большинство моих классов никогда не предназначены для самостоятельной реализации. Вместо этого все происходит через мой Catalog класс. Например, я не могу проверить свой класс File самостоятельно, так как File доступен только через Catalog.

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

//NOTE: The real version would have code to log the results and any exceptions thrown

//input data
string testCatalogALocation = "C:\TestCatalogA"
string testCatalogBLocation = "C:\TestCatalogB"
string testFileLocation = "C:\testfile.jpg"
string testFileName = System.IO.Path.GetFileName(testFileLocation);


//Test creating catalogs
Catalog catAtemp = Catalog(testCatalogALocation)
Catalog catBtemp = Catalog(testCatalogBLocation );


//test opening catalogs
Catalog catA = Catalog.OpenCatalog(testCatalogALocation);
Catalog catB = Catalog.OpenCatalog(testCatalogBLocation );


using(FileStream fs = new FileStream(testFileLocation )
{
    //test importing a file
    catA.ImportFile(testFileName,fs);
}

//test retrieving a file
File testFile = catA.GetFile(System.IO.Path.GetFileName(testFileLocation));

//test copying between catalogs
catB.CopyFileTo(testFile);


//Clean Up after test
System.IO.Directory.Delete(testCatalogALocation);
System.IO.Directory.Delete(testCatalogBLocation);

Во-первых, я что-то упустил? Есть ли какой-нибудь способ для модульного тестирования такой программы? Во-вторых, есть ли какой-нибудь способ создать тест процедурного типа, подобный приведенному выше коду, но можно ли использовать преимущества инструментов тестирования, встроенных в Visual Studio? Позволит ли мне сделать «Общий тест» в VS2010?


Обновление

Спасибо всем за ответы. На самом деле мои классы на самом деле наследуют от ряда интерфейсов. Вот диаграмма классов для всех, кто заинтересован. На самом деле у меня больше интерфейсов, чем классов. Я просто упустил интерфейсы из моего примера для простоты.

Спасибо за все предложения по использованию насмешек. Я слышал этот термин в прошлом, но так и не понял, что такое «издевательство» до сих пор. Я понимаю, как я мог создать макет моего интерфейса IFile, который представляет один файл в каталоге. Я также понимаю, как я мог создать фиктивную версию своего интерфейса ICatalog, чтобы проверить взаимодействие двух каталогов.

Пока я не понимаю, как я могу протестировать свои конкретные реализации ICatalog, поскольку они тесно связаны с их внутренними источниками данных. На самом деле вся цель моих каталожных классов - читать, записывать и манипулировать их внешними данными / ресурсами.

Ответы [ 5 ]

9 голосов
/ 08 сентября 2010

Вы должны прочитать о SOLID принципах кода.В частности, 'D' в SOLID обозначает Принцип внедрения / инверсии зависимостей , здесь класс, который вы пытаетесь протестировать, не зависит от других конкретных классов и внешних реализаций, а вместо этого зависит отинтерфейсы и абстракции.Вы полагаетесь на Контейнер IoC (Inversion of Control) (такой как Unity , Ninject или Castle Windsor ) для динамического внедрения конкретной зависимости во время выполнения, ново время модульного тестирования вместо этого вы вводите макет / заглушку.

Например, рассмотрите следующий класс:

public class ComplexAlgorithm
{
    protected DatabaseAccessor _data;

    public ComplexAlgorithm(DatabaseAccessor dataAccessor)
    {
        _data = dataAccessor;
    }

    public int RunAlgorithm()
    {
        // RunAlgorithm needs to call methods from DatabaseAccessor
    }
}

Метод RunAlgorithm () должен поразить базу данных (через DatabaseAccessor), что затрудняет тестирование,Таким образом, вместо этого мы превращаем DatabaseAccessor в интерфейс.

public class ComplexAlgorithm
{
    protected IDatabaseAccessor _data;

    public ComplexAlgorithm(IDatabaseAccessor dataAccessor)
    {
        _data = dataAccessor;
    }

    // rest of class (snip)
}

Теперь ComplexAlgorithm зависит от интерфейса IDatabaseAccessor, который можно легко смоделировать, когда нам нужно выполнить модульное тестирование ComplexAlgorithm по отдельности.Например:

public class MyFakeDataAccessor : IDatabaseAccessor
{
    public IList<Thing> GetThings()
    {
        // Return a fake/pretend list of things for testing
        return new List<Thing>()
        {
            new Thing("Thing 1"),
            new Thing("Thing 2"),
            new Thing("Thing 3"),
            new Thing("Thing 4")
        };
    }

    // Other methods (snip)
}

[Test]
public void Should_Return_8_With_Four_Things_In_Database()
{
    // Arrange
    IDatabaseAccessor fakeData = new MyFakeDataAccessor();
    ComplexAlgorithm algorithm = new ComplexAlgorithm(fakeData);
    int expectedValue = 8;

    // Act
    int actualValue = algorithm.RunAlgorithm();

    // Assert
    Assert.AreEqual(expectedValue, actualValue);
}

По сути, мы «отделяем» два класса друг от друга.Разъединение - это еще один важный принцип разработки программного обеспечения для написания более понятного и надежного кода.

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

2 голосов
/ 08 сентября 2010

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

  1. Определите места, в которых вы зависите от внешних данных / ресурсов, и определите, есть ли у вас классы, которые изолируют каждую зависимость.

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

  3. Извлечение интерфейсов для классов, изолирующих внешние данные.

  4. Когда вы создаете свои классы, передайте внешние зависимости как интерфейсы, а не заставляйте класс создавать их сами.

  5. Создание тестовых реализаций ваших интерфейсов, которые не зависят от внешних ресурсов. Здесь вы также можете добавить «чувствительный» код для ваших тестов, чтобы убедиться, что используются соответствующие вызовы. Фреймворки-макеты могут быть очень полезны здесь, но это может быть хорошим упражнением для создания классов-заглушек вручную для простого проекта, так как это дает вам представление о том, что делают ваши тестовые классы. Ручные классы-заглушки, как правило, устанавливают открытые свойства, чтобы указать, когда / как вызываются методы, и имеют общие свойства, чтобы указать, как должны вести себя определенные вызовы.

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

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

2 голосов
/ 08 сентября 2010

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

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

Также читайте о внедрении зависимостей здесь

http://martinfowler.com/articles/injection.html

1 голос
/ 08 сентября 2010

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

Здесь D - Design D - входит в TDD. Это плохой дизайн - иметь тесно связанные классы. Эта неприятность проявляется сразу же, когда вы пытаетесь выполнить юнит-тестирование такого класса - и если вы начнете с юнит-тестов, вы никогда не окажетесь в такой ситуации. Написание тестируемого кода заставляет нас улучшать дизайн.

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

0 голосов
/ 09 сентября 2010

Теперь у вас есть Legacy Code.То есть: код, который был реализован без тестов.Для ваших начальных тестов я определенно проверил бы класс Catalog, пока вы не сломаете все эти зависимости.Итак, вашим первым набором тестов будут интеграционные / приемочные тесты.

Если вы не ожидаете, что какое-либо поведение изменится, оставьте это в покое, но если вы действительно внесете изменение, я предлагаю вам TDD изменение и сборка юнит-тестов с изменениями.

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...