NUnit - Как протестировать все классы, которые реализуют определенный интерфейс - PullRequest
29 голосов
/ 02 сентября 2008

Если у меня есть интерфейс IFoo, и у меня есть несколько классов, которые его реализуют, какой самый лучший / самый элегантный / самый умный способ проверить все эти классы на интерфейсе?

Я бы хотел сократить дублирование тестового кода, но все же «оставаться верным» принципам модульного тестирования.

Что вы считаете лучшей практикой? Я использую NUnit, но я предполагаю, что примеры из любой среды модульного тестирования будут действительны

Ответы [ 7 ]

13 голосов
/ 02 сентября 2008

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

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

например. у вас есть следующий интерфейс:

public interface IFoo {

    public void CommonCode();

    public void SpecificCode();

}

Возможно, вы захотите создать абстрактный класс:

public abstract class AbstractFoo : IFoo {

    public void CommonCode() {
          SpecificCode();
    }

    public abstract void SpecificCode();

}

Тестирование это легко; реализовать абстрактный класс в тестовом классе либо как внутренний класс:

[TestFixture]
public void TestClass {

    private class TestFoo : AbstractFoo {
        boolean hasCalledSpecificCode = false;
        public void SpecificCode() {
            hasCalledSpecificCode = true;
        }
    }

    [Test]
    public void testCommonCallsSpecificCode() {
        TestFoo fooFighter = new TestFoo();
        fooFighter.CommonCode();
        Assert.That(fooFighter.hasCalledSpecificCode, Is.True());
    }
}

... или позвольте тестовому классу расширять сам абстрактный класс, если вам это нравится.

[TestFixture]
public void TestClass : AbstractFoo {

    boolean hasCalledSpecificCode;
    public void specificCode() {
        hasCalledSpecificCode = true;
    }

    [Test]
    public void testCommonCallsSpecificCode() {
        AbstractFoo fooFighter = this;
        hasCalledSpecificCode = false;
        fooFighter.CommonCode();
        Assert.That(fooFighter.hasCalledSpecificCode, Is.True());
    }        

}

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

Надеюсь, это имеет смысл для вас.


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

Многие модели опираются на эту модель поведения, например, ASP.NET , где необходимо реализовать хуки на странице или пользовательские элементы управления, такие как сгенерированный метод Page_Load, который вызывается событием Load, метод шаблона вызывает хуки за кулисами , Есть еще много примеров этого. В основном все, что вам нужно реализовать, используя слова «load», «init» или «render», вызывается методом шаблона.

11 голосов
/ 02 сентября 2008

Я не согласен с Джоном Лимджапом , когда он говорит:

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

Может быть много частей контракта, не указанных в типе возврата. Не зависящий от языка пример:

public interface List {

  // adds o and returns the list
  public List add(Object o);

  // removed the first occurrence of o and returns the list
  public List remove(Object o);

}

Ваши юнит-тесты на LinkedList, ArrayList, CircularlyLinkedList и всех остальных должны проверять не только то, что сами списки возвращаются, но и то, что они были должным образом изменены.

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

Если вам не нужны накладные расходы по контрактам, я рекомендую испытательные стенды в соответствии с рекомендациями Spoike :

abstract class BaseListTest {

  abstract public List newListInstance();

  public void testAddToList() {
    // do some adding tests
  }

  public void testRemoveFromList() {
    // do some removing tests
  }

}

class ArrayListTest < BaseListTest {
  List newListInstance() { new ArrayList(); }

  public void arrayListSpecificTest1() {
    // test something about ArrayLists beyond the List requirements
  }
}
3 голосов
/ 02 сентября 2008

Я не думаю, что это лучшая практика.

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

Если вы действительно хотите контролировать реализацию своего метода, у вас есть возможность:

  • Реализация его как метода в абстрактном классе и наследование от него. Вам все еще нужно будет унаследовать его в конкретном классе, но вы уверены, что, если он явно не переопределен, этот метод будет делать эту правильную вещь.
  • В .NET 3.5 / C # 3.0 реализация метода как метода расширения со ссылкой на Интерфейс

Пример:

public static ReturnType MethodName (this IMyinterface myImplementation, SomeObject someParameter)
{
    //method body goes here
}

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

1 голос
/ 14 сентября 2008

Как насчет иерархии классов [TestFixture]? Поместите общий тестовый код в базовый тестовый класс и наследуйте его в дочерних тестовых классах.

1 голос
/ 05 сентября 2008

@ Император XLII

Мне нравится звучание комбинаторных тестов в MbUnit, я попробовал технику абстрактного тестирования интерфейса базового класса с NUnit, и, хотя она работает, вам потребуется отдельное тестовое устройство для каждого интерфейса, реализуемого классом поскольку в C # нет множественного наследования - хотя можно использовать внутренние классы, что довольно круто). В действительности это хорошо, может быть даже выгодно, потому что группирует ваши тесты для реализации класса по интерфейсу. Но было бы хорошо, если бы рамки были умнее. Если бы я мог использовать атрибут, чтобы пометить класс как «официальный» тестовый класс для интерфейса, и среда поиска выполнила бы поиск в тестируемой сборке всех классов, которые реализуют интерфейс, и запустила эти тесты на нем.

Это было бы круто.

1 голос
/ 03 сентября 2008

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

  • Для xUnit.net я создал библиотеку Type Resolver для поиска всех реализаций определенного типа (расширения xUnit.net являются лишь тонкой оболочкой над Type Resolver функциональность, поэтому он может быть адаптирован для использования в других рамках).
  • В MbUnit вы можете использовать CombinatorialTest с UsingImplementations атрибутами для параметров.
  • Для других платформ может быть полезен упомянутый шаблон базового класса Spoike .

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

0 голосов
/ 02 сентября 2008

Я не использую NUnit, но я тестировал интерфейсы C ++. Сначала я протестировал бы класс TestFoo, который является его базовой реализацией, чтобы убедиться, что универсальный материал работает. Тогда вам просто нужно протестировать материал, который уникален для каждого интерфейса.

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