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

NUnit - как протестировать все классы, реализующие определенный интерфейс

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

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

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

4b9b3361

Ответ 1

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

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

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

public interface IFoo {

    public void CommonCode();

    public void SpecificCode();

}

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

public abstract class AbstractFoo : IFoo {

    public void CommonCode() {
          SpecificCode();
    }

    public abstract void SpecificCode();

}

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

[TextFixture]
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" вызывается методом шаблона.

Ответ 2

Я не согласен с Jon Limjap, когда он говорит:

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

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

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 и всех остальных должно быть проверено не только то, что сами списки возвращены, но и что они были правильно изменены.

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

Если вы не хотите накладных расходов на контракты, я рекомендую тестовые установки в соответствии с рекомендациями 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

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

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

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

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

Пример:

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

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

Ответ 4

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

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

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

Ответ 5

@Emperor XLII

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

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

Ответ 6

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

Ответ 7

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