Устранение дублирования в тестовой разработке - PullRequest
2 голосов
/ 05 марта 2011

Я разрабатываю небольшой стиль парсера класса TDD. Вот мои тесты:

    ...

    [TestMethod]
    public void Can_parse_a_float() {
        InitializeScanner("float a");
        Token expectedToken = new Token("float", "a");

        Assert.AreEqual(expectedToken, scanner.NextToken());
    }

    [TestMethod]
    public void Can_parse_an_int() {
        InitializeScanner("int a");
        Token expectedToken = new Token("int", "a");

        Assert.AreEqual(expectedToken, scanner.NextToken());            
    }

    [TestMethod]
    public void Can_parse_multiple_tokens() {
        InitializeScanner("int a float b");

        Token firstExpectedToken = new Token("int", "a");
        Token secondExpectedToken = new Token("float", "b");

        Assert.AreEqual(firstExpectedToken, scanner.NextToken());
        Assert.AreEqual(secondExpectedToken, scanner.NextToken());
    }

Что меня беспокоит, так это то, что последний тест использует те же строки кода, что и Can_parse_a_float() и Can_parse_an_int(). С одной стороны, он выполняет то, что оба эти метода не делают: что из строки исходного кода я могу получить несколько токенов. С другой стороны, если Can_parse_a_float() и Can_parse_an_int() потерпят неудачу, Can_parse_multiple_tokens() тоже потерпит неудачу.

Я чувствую, что здесь поставлено 4 гола:

  • Я хочу, чтобы мои тесты показали, что мои Parser анализируют целые числа
  • Я хочу, чтобы мои тесты показали, что мои Parser парситы плавают
  • Я хочу, чтобы мои тесты показали, что мой Parser может анализировать несколько целых чисел / чисел подряд
  • Я хочу, чтобы мои тесты также хорошо служили механизмом документирования (!)

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

Ответы [ 3 ]

4 голосов
/ 05 марта 2011

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

2 голосов
/ 06 марта 2011

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

  • Преобразование префикса входной строки в токен
  • Повторение вышеуказанной операции с отслеживанием "где я" во входной строке

Ваш текущий дизайн назначает эти два для одного объекта Parser. Вот почему вы не можете проверить две обязанности по отдельности. Лучшим вариантом было бы определить токенизатор для первой ответственности и парсер для второй. Извините, я слишком ржавый на C #, поэтому я буду писать Java.

Одна из проблем разработки токенизатора заключается в том, что он должен возвращать два значения: токен и количество использованной строки. В Java строки неизменны, поэтому я не могу изменить входной аргумент. Вместо этого я бы использовал StringReader, который представляет собой поток символов, которые вы можете использовать. Мой первый тест может быть:

@Test public void tokenizes_an_integer() {
    Tokenizer tokenizer = new Tokenizer();
    StringReader input = new StringReader("int a rest of the input");

    Token token = tokenizer.tokenize(input);

    assertEquals(new Token("a", "int"), token);
    assertEquals(" rest of the input", input.toString());
}

Когда это пройдет, я мог бы написать тест для второй ответственности:

@Test public void calls_tokenizer_repeatedly_consuming_the_input() {
    StringReader input = new StringReader("int a int b");
    Parser parser = new Parser(input, new Tokenizer());

    assertEquals(new Token("a", "int"), parser.nextToken());
    assertEquals(new Token("b", "int"), parser.nextToken());
    assertEquals(null, parser.nextToken());
}

Это лучше, но все же не идеально с точки зрения ремонтопригодности теста. Если вы решите изменить синтаксис токена «int», оба теста прервутся. В идеале вы хотели бы, чтобы сломался только первый. Одним из решений было бы использование поддельного токенизатора во втором тесте, который не зависит от реального.

Это то, что я все еще пытаюсь освоить. Одним из полезных ресурсов является книга «Растущее объектно-ориентированное программное обеспечение», которая очень хороша в отношении независимости и выразительности тестов.

Где печенье? : -)

1 голос
/ 06 марта 2011

Это иногда случалось и со мной. Если вы придерживаетесь цикла fail-pass-refactor, иногда вы обнаруживаете, что два теста фактически превращаются в один и тот же с точки зрения кода и логики, которые они выполняют.

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

Реальная единица измерения vale, которую обеспечивает ваш третий тест, - это описание и настаивание на правильном поведении нескольких токенов. Мне кажется, это уникальная спецификация в этом наборе тестов, поэтому я бы сохранил ее.

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