Разработка интерфейса с TDD - PullRequest
       29

Разработка интерфейса с TDD

4 голосов
/ 13 февраля 2009

Я большой поклонник TDD и использую его для подавляющего большинства моих разработок в наши дни. Однако одна ситуация, с которой я сталкиваюсь довольно часто и никогда не находила того, что я считал «хорошим» ответом, - это что-то вроде следующего (надуманного) примера.

Предположим, у меня есть такой интерфейс (пишу на Java, но на самом деле это относится к любому языку OO):

public interface PathFinder {
    GraphNode[] getShortestPath(GraphNode start, GraphNode goal);

    int getShortestPathLength(GraphNode start, GraphNode goal);
}

Теперь предположим, что я хочу создать три реализации этого интерфейса. Давайте назовем их DijkstraPathFinder, DepthFirstPathFinder и AStarPathFinder.

Вопрос в том, как мне разработать эти три реализации с использованием TDD? Их открытый интерфейс будет одинаковым, и, по-видимому, я бы написал одинаковые тесты для каждого, поскольку результаты getShortestPath () и getShortestPathLength () должны быть согласованы для всех трех реализаций.

Кажется, мой выбор:

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

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

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

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

Ответы [ 5 ]

3 голосов
/ 13 февраля 2009

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

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

РЕДАКТИРОВАТЬ: Это полезно, так что когда вы добавляете другую реализацию интерфейса в будущем, у вас уже есть тесты для проверки того, что класс правильно реализует контракт интерфейса. Это может работать для чего-то столь же специфического, как ISortingStrategy, для чего-то более широкого, чем IDisposable.

2 голосов
/ 13 февраля 2009

У меня не возникло бы никаких проблем с вариантом 1, и имейте в виду, что рефакторинг является частью TDD, и обычно на этапе рефакторинга вы переходите к шаблону проектирования, такому как стратегия, поэтому я не буду расстраиваться из-за этого что без написания новых тестов.

Если вы хотите протестировать детали реализации каждого элемента PathFinder, вы можете рассмотреть возможность передачи фиктивных GraphNodes, которые каким-то образом могут помочь утверждать Dijkstra-ness или DepthFirst-ness и т. Д. Реализации. (Возможно, эти фиктивные GraphNodes могли бы записывать, как их обходят, или каким-то образом измерять производительность.) Может быть, это избыточное тестирование, но с другой стороны, если вы знаете, что вашей системе по каким-то причинам нужны эти три разные стратегии, было бы хорошо иметь тесты чтобы продемонстрировать почему - иначе почему бы просто не выбрать одну реализацию и выбросить другие?

2 голосов
/ 13 февраля 2009

нет ничего плохого в написании тестов для интерфейса и их повторном использовании для каждой реализации, например -

public class TestPathFinder : TestClass
{
    public IPathFinder _pathFinder;
    public IGraphNode _startNode;
    public IGraphNode _goalNode;

    public TestPathFinder() : this(null,null,null) { }
    public TestPathFinder(IPathFinder ipf, 
        IGraphNode start, IGraphNode goal) : base()
    {
        _pathFinder = ipf;
        _startNode = start;
        _goalNode = goal;
    }
}

TestPathFinder tpfDijkstra = new TestPathFinder(
    new DijkstraPathFinder(), n1, nN);
tpfDijkstra.RunTests();

//etc. - factory optional

Я бы сказал, что это решение с минимальными усилиями , которое очень соответствует принципам Agile / TDD.

1 голос
/ 13 февраля 2009

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

Конечно, ты.

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

СЦГ

1 голос
/ 13 февраля 2009

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

...