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

StackOverflow https://stackoverflow.com/questions/39003

  •  09-06-2019
  •  | 
  •  

Вопрос

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

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

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

Это было полезно?

Решение

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

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

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

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());
    }        

}

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

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


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

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

Другие советы

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

  

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

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

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
  }
}

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

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

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

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

Пример:

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

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

@Emperor XLII

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

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

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

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

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

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

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

Лицензировано под: CC-BY-SA с атрибуция
Не связан с StackOverflow
scroll top